├── CLAUDE.md ├── .npmrc ├── .github ├── copilot-instructions.md ├── dependabot.yml └── workflows │ ├── claude.yml │ ├── dependency-review.yml │ ├── ci.yml │ ├── deploy-r2.yml │ ├── e2e-test.yml │ └── release.yml ├── .vscode ├── extensions.json └── settings.json ├── public └── favicon.ico ├── .markdownlint.yaml ├── src ├── App.tsx ├── vite-env.d.ts ├── components │ ├── Header │ │ ├── CopyUrlButton.tsx │ │ ├── DebugButton.tsx │ │ └── index.tsx │ ├── DevtoolsPane │ │ ├── ReloadDevicesButton.tsx │ │ ├── UpdateMediaStreamButton.tsx │ │ ├── MediaStatsForm.tsx │ │ ├── AudioTrackForm.tsx │ │ ├── VideoTrackForm.tsx │ │ ├── TooltipFormLabel.tsx │ │ ├── ConnectButton.tsx │ │ ├── DisconnectButton.tsx │ │ ├── AudioForm.tsx │ │ ├── VideoForm.tsx │ │ ├── FakeVolumeForm.tsx │ │ ├── DisposeMediaButton.tsx │ │ ├── RequestMediaButton.tsx │ │ ├── ChannelIdForm.tsx │ │ ├── TooltipFormCheck.tsx │ │ ├── MicDeviceForm.tsx │ │ ├── ReconnectForm.tsx │ │ ├── MediaProcessorsNoiseSuppressionForm.tsx │ │ ├── CameraDeviceForm.tsx │ │ ├── ResizeModeForm.tsx │ │ ├── AspectRatioForm.tsx │ │ ├── ForceStereoOutputForm.tsx │ │ ├── AutoGainControlForm.tsx │ │ ├── EchoCancellationForm.tsx │ │ ├── NoiseSuppressionForm.tsx │ │ ├── AudioContentHintForm.tsx │ │ ├── VideoContentHintForm.tsx │ │ ├── AudioOutputForm.tsx │ │ ├── AudioInputForm.tsx │ │ ├── EchoCancellationTypeForm.tsx │ │ ├── VideoInputForm.tsx │ │ ├── FacingModeForm.tsx │ │ ├── BlurRadiusForm.tsx │ │ ├── SimulcastForm.tsx │ │ ├── SpotlightForm.tsx │ │ ├── RoleForm.tsx │ │ ├── SimulcastRidForm.tsx │ │ ├── SpotlightNumberForm.tsx │ │ ├── SpotlightFocusRidForm.tsx │ │ ├── SpotlightUnfocusRidForm.tsx │ │ ├── AudioCodecTypeForm.tsx │ │ ├── VideoCodecTypeForm.tsx │ │ ├── SimulcastRequestRidForm.tsx │ │ ├── Mp4FileForm.tsx │ │ ├── AudioBitRateForm.tsx │ │ ├── MetadataForm.tsx │ │ ├── FrameRateForm.tsx │ │ ├── VideoAV1ParamsForm.tsx │ │ ├── VideoVP9ParamsForm.tsx │ │ ├── VideoH264ParamsForm.tsx │ │ ├── VideoH265ParamsForm.tsx │ │ ├── ForwardingFilterForm.tsx │ │ ├── ForwardingFiltersForm.tsx │ │ ├── BundleIdForm.tsx │ │ ├── ClientIdForm.tsx │ │ ├── ResolutionForm.tsx │ │ ├── SignalingNotifyMetadataForm.tsx │ │ ├── DisplayResolutionForm.tsx │ │ ├── JSONInputField.tsx │ │ ├── VideoBitRateForm.tsx │ │ ├── AudioStreamingLanguageCodeForm.tsx │ │ ├── SignalingUrlCandidatesForm.tsx │ │ ├── DataChannelsForm.tsx │ │ ├── MediaTypeForm.tsx │ │ └── DataChannelForm.tsx │ ├── Footer │ │ ├── DebugButton.tsx │ │ └── index.tsx │ ├── DebugPane │ │ ├── CopyLogButton.tsx │ │ ├── Filter.tsx │ │ ├── LogMessages.tsx │ │ ├── SignalingMessages.tsx │ │ ├── PushMessages.tsx │ │ ├── NotifyMessages.tsx │ │ ├── Stats.tsx │ │ ├── CapabilitiesCodec.tsx │ │ ├── DataChannelMessagingMessages.tsx │ │ ├── SendDataChannelMessagingMessage.tsx │ │ ├── index.tsx │ │ ├── TimelineMessages.tsx │ │ └── Message.tsx │ ├── ClipboardIcon.tsx │ ├── Video │ │ ├── ResetSpotlightRidButton.tsx │ │ ├── ResetSpotlightRidBySendConnectionIdButton.tsx │ │ ├── SessionStatusBar.tsx │ │ ├── RequestSimulcastRidButton.tsx │ │ ├── ConnectionStatusBar.tsx │ │ ├── RequestSpotlightRidButton.tsx │ │ ├── RequestSpotlightRidBySendConnectionIdButton.tsx │ │ ├── Video.tsx │ │ ├── JitterBuffer.tsx │ │ ├── RemoteVideoCapabilities.tsx │ │ └── VolumeVisualizer.tsx │ └── AlertMessages.tsx ├── main.tsx ├── rpc.json ├── DevTools.tsx ├── rpc.ts └── constants.ts ├── .env.production ├── .mcp.json ├── .stylelintrc.json ├── tests ├── global-setup.ts ├── recvonly.test.ts ├── sendonly.test.ts └── sendrecv.test.ts ├── vitest.config.ts ├── .gitignore ├── .pnpmfile.cjs ├── global.d.ts ├── index.html ├── .env.template ├── AGENTS.md ├── vite.config.ts ├── playwright.config.ts ├── tsconfig.json ├── DEV.md ├── package.json ├── biome.jsonc ├── DOCUMENT.md └── canary.py /CLAUDE.md: -------------------------------------------------------------------------------- 1 | AGENTS.md -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true -------------------------------------------------------------------------------- /.github/copilot-instructions.md: -------------------------------------------------------------------------------- 1 | - 日本語で反応して 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["biomejs.biome"] 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shiguredo/sora-devtools/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | # line-length 2 | MD013: false 3 | 4 | # no-emphasis-as-heading 5 | MD036: false 6 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import type React from 'react' 2 | import DevTools from './DevTools.tsx' 3 | 4 | const App = (): React.JSX.Element => { 5 | return 6 | } 7 | 8 | export default App 9 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | VITE_ZUSTAND_DEVTOOLS=false 2 | VITE_SORA_SIGNALING_URL=ws://localhost:5000/signaling 3 | VITE_VIRTUAL_BACKGROUND_ASSETS_PATH=https://cdn.jsdelivr.net/npm/@shiguredo/virtual-background@latest/dist -------------------------------------------------------------------------------- /.mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "chrome-devtools": { 4 | "type": "stdio", 5 | "command": "npx", 6 | "args": [ 7 | "chrome-devtools-mcp@latest" 8 | ], 9 | "env": {} 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "syntax": "css", 3 | 4 | "extends": "stylelint-config-standard", 5 | 6 | "rules": { 7 | "color-hex-length": null, 8 | "at-rule-no-unknown": null, 9 | "indentation": 2, 10 | "no-descending-specificity": null 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/global-setup.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import type { FullConfig } from '@playwright/test' 3 | import dotenv from 'dotenv' 4 | 5 | dotenv.config({ path: path.resolve(process.cwd(), '.env.local') }) 6 | 7 | async function globalSetup(_config: FullConfig) {} 8 | 9 | export default globalSetup 10 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react' 2 | 3 | import { defineConfig } from 'vitest/config' 4 | 5 | export default defineConfig({ 6 | plugins: [react()], 7 | test: { 8 | include: ['src/app/app.test.ts', 'src/utils.test.ts', 'src/utils.pbt.test.ts'], 9 | globals: true, 10 | environment: 'jsdom', 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .next 2 | node_modules/ 3 | wasm.wasm 4 | next-env.d.ts 5 | *.tsbuildinfo 6 | .DS_Store 7 | .pnpm-store/ 8 | 9 | # .env 10 | .env* 11 | !.env.template 12 | !.env.production 13 | 14 | # playwright 15 | /test-results/ 16 | /playwright-report/ 17 | /blob-report/ 18 | /playwright/.cache/ 19 | 20 | # dist 21 | dist/ 22 | 23 | # claude 24 | .claude/ 25 | -------------------------------------------------------------------------------- /.pnpmfile.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | packageExtensions: { 3 | 'sora-js-sdk': { 4 | peerDependencyMeta: { 5 | 'sora-js-sdk': { 6 | optional: true, 7 | }, 8 | }, 9 | dependencies: { 10 | 'sora-js-sdk': ({ version }) => (version.includes('canary') ? 'canary' : 'latest'), 11 | }, 12 | }, 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_ZUSTAND_DEVTOOLS: string 5 | readonly VITE_SORA_SIGNALING_URL: string 6 | readonly VITE_VIRTUAL_BACKGROUND_ASSETS_PATH: string 7 | readonly VITE_NOISE_SUPPRESSION_ASSETS_PATH: string 8 | } 9 | 10 | interface ImportMeta { 11 | readonly env: ImportMetaEnv 12 | } 13 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | webkitAudioContext: AudioContext 3 | readonly CropTarget: { 4 | fromElement(element: Element): Promise 5 | } 6 | } 7 | 8 | interface MediaStreamTrack { 9 | cropTo(cropTarget: CropTarget): Promise 10 | } 11 | 12 | type CropTarget = { 13 | symbol: 'CropTarget' 14 | } 15 | 16 | declare global { 17 | let window: Window 18 | } 19 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Sora DevTools 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/components/Header/CopyUrlButton.tsx: -------------------------------------------------------------------------------- 1 | import type React from 'react' 2 | 3 | import { copyURL } from '@/app/actions' 4 | 5 | export const CopyUrlButton: React.FC = () => { 6 | const onClick = (): void => { 7 | copyURL() 8 | } 9 | return ( 10 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import 'bootstrap/dist/css/bootstrap.min.css' 4 | import './App.css' 5 | import App from './App.tsx' 6 | 7 | const rootElement = document.getElementById('root') 8 | 9 | if (!rootElement) { 10 | throw new Error('Root element not found') 11 | } 12 | 13 | const root = createRoot(rootElement) 14 | 15 | root.render( 16 | 17 | 18 | , 19 | ) 20 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | VITE_ZUSTAND_DEVTOOLS=true 2 | VITE_SORA_SIGNALING_URL=ws://localhost:5000/signaling 3 | VITE_VIRTUAL_BACKGROUND_ASSETS_PATH=https://cdn.jsdelivr.net/npm/@shiguredo/virtual-background@latest/dist 4 | 5 | # テストに利用する Sora の Signaling URL を指定してください 6 | E2E_TEST_SORA_SIGNALING_URL=ws://127.0.0.1:5000/signaling 7 | # テストに利用する Sora の ChannelID のプレフィックスを指定してください 8 | E2E_TEST_SORA_CHANNEL_ID_PREFIX=sora-js-sdk-e2e-test_ 9 | # テストに利用するアクセストークンを指定してください、不要であれば何の値でも問題ありません 10 | E2E_TEST_ACCESS_TOKEN=access_token -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | # AGENTS.md 2 | 3 | - 一切忖度しないこと 4 | - 常に日本語を利用すること 5 | 6 | ## レビューについて 7 | 8 | - レビューはかなり厳しくすること 9 | - レビューの表現は、シンプルにすること 10 | - レビューの表現は、日本語で行うこと 11 | - レビューの表現は、指摘内容を明確にすること 12 | - レビューの表現は、指摘内容を具体的にすること 13 | - レビューの表現は、指摘内容を優先順位をつけること 14 | - レビューの表現は、指摘内容を優先順位をつけて、重要なものから順に記載すること 15 | - ドキュメントは別に書いているので、ドキュメトに付いては考慮しないこと 16 | - 変更点とリリースノートの整合性を確認すること 17 | 18 | ## TypeScript 19 | 20 | - pnpm fmt と lint を実行してエラーがあったら修正すること 21 | - pnpm typecheck を実行してエラーがあったら修正すること 22 | - pnpm build を実行してエラーがあったら修正すること 23 | -------------------------------------------------------------------------------- /src/components/DevtoolsPane/ReloadDevicesButton.tsx: -------------------------------------------------------------------------------- 1 | import type React from 'react' 2 | 3 | import { setMediaDevices } from '@/app/actions' 4 | 5 | export const ReloadDevicesButton: React.FC = () => { 6 | const onClick = (): void => { 7 | setMediaDevices() 8 | } 9 | return ( 10 |
11 | 18 |
19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/components/DevtoolsPane/UpdateMediaStreamButton.tsx: -------------------------------------------------------------------------------- 1 | import type React from 'react' 2 | 3 | import { updateMediaStream } from '@/app/actions' 4 | 5 | export const UpdateMediaStreamButton: React.FC = () => { 6 | const onClick = (): void => { 7 | updateMediaStream() 8 | } 9 | return ( 10 |
11 | 18 |
19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "[javascript]": { 5 | "editor.defaultFormatter": "biomejs.biome" 6 | }, 7 | "[javascriptreact]": { 8 | "editor.defaultFormatter": "biomejs.biome" 9 | }, 10 | "[typescript]": { 11 | "editor.defaultFormatter": "biomejs.biome" 12 | }, 13 | "[typescriptreact]": { 14 | "editor.defaultFormatter": "biomejs.biome" 15 | }, 16 | "editor.codeActionsOnSave": { 17 | "quickfix.biome": "explicit", 18 | "source.organizeImports.biome": "explicit" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | -------------------------------------------------------------------------------- /src/components/Footer/DebugButton.tsx: -------------------------------------------------------------------------------- 1 | import type React from 'react' 2 | 3 | import { setDebug } from '@/app/actions' 4 | import { useSoraDevtoolsStore } from '@/app/store' 5 | 6 | export const DebugButton: React.FC = () => { 7 | const debug = useSoraDevtoolsStore((state) => state.debug) 8 | const onClick = (): void => { 9 | setDebug(!debug) 10 | } 11 | const className = debug ? 'btn btn-footer-debug-mode active' : 'btn btn-footer-debug-mode' 12 | return ( 13 |
14 | 17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/components/DebugPane/CopyLogButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { ClipboardIcon } from '@/components/ClipboardIcon' 4 | import { copy2clipboard } from '@/utils' 5 | 6 | type Props = { 7 | text: string 8 | disabled?: boolean 9 | } 10 | export const CopyLogButton = React.memo((props) => { 11 | const onClick = (event: React.MouseEvent): void => { 12 | copy2clipboard(props.text) 13 | event.currentTarget.blur() 14 | } 15 | if (props.disabled) { 16 | return
17 | } 18 | return ( 19 | 22 | ) 23 | }) 24 | -------------------------------------------------------------------------------- /src/components/Header/DebugButton.tsx: -------------------------------------------------------------------------------- 1 | import type React from 'react' 2 | 3 | import { setDebug } from '@/app/actions' 4 | import { useSoraDevtoolsStore } from '@/app/store' 5 | 6 | export const DebugButton: React.FC = () => { 7 | const debug = useSoraDevtoolsStore((state) => state.debug) 8 | const onClick = (): void => { 9 | setDebug(!debug) 10 | } 11 | const classNames = ['btn', 'btn-light', 'btn-header-debug-mode', 'btn-sm', 'ms-1'] 12 | if (debug) { 13 | classNames.push('active') 14 | } 15 | return ( 16 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/claude.yml: -------------------------------------------------------------------------------- 1 | name: Claude Assistant 2 | 3 | on: 4 | issue_comment: 5 | types: [created] 6 | pull_request_review_comment: 7 | types: [created] 8 | 9 | permissions: 10 | contents: read 11 | issues: write 12 | pull-requests: write 13 | actions: read 14 | 15 | env: 16 | CLAUDE_API_KEY: ${{ secrets.ANTHROPIC_API_KEY_SORA_OSS_SONNET }} 17 | CLAUDE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN_VOLUNTAS }} 18 | 19 | jobs: 20 | claude-response: 21 | runs-on: ubuntu-24.04 22 | timeout-minutes: 15 23 | steps: 24 | - uses: actions/checkout@v5 25 | - uses: shiguredo/github-actions/.github/actions/claude-code-action@main 26 | with: 27 | api_key: ${{ env.CLAUDE_API_KEY }} 28 | oauth_user: voluntas 29 | oauth_token: ${{ env.CLAUDE_OAUTH_TOKEN }} 30 | -------------------------------------------------------------------------------- /src/components/DevtoolsPane/MediaStatsForm.tsx: -------------------------------------------------------------------------------- 1 | import type React from 'react' 2 | import { FormGroup } from 'react-bootstrap' 3 | 4 | import { setMediaStats } from '@/app/actions' 5 | import { useSoraDevtoolsStore } from '@/app/store' 6 | 7 | import { TooltipFormCheck } from './TooltipFormCheck.tsx' 8 | 9 | export const MediaStatsForm: React.FC = () => { 10 | const mediaStats = useSoraDevtoolsStore((state) => state.mediaStats) 11 | const onChange = (event: React.ChangeEvent): void => { 12 | setMediaStats(event.target.checked) 13 | } 14 | return ( 15 | 16 | 17 | Show media stats 18 | 19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/components/DevtoolsPane/AudioTrackForm.tsx: -------------------------------------------------------------------------------- 1 | import type React from 'react' 2 | import { FormGroup } from 'react-bootstrap' 3 | 4 | import { setAudioTrack } from '@/app/actions' 5 | import { useSoraDevtoolsStore } from '@/app/store' 6 | 7 | import { TooltipFormCheck } from './TooltipFormCheck.tsx' 8 | 9 | export const AudioTrackForm: React.FC = () => { 10 | const audioTrack = useSoraDevtoolsStore((state) => state.audioTrack) 11 | const onChange = (event: React.ChangeEvent): void => { 12 | setAudioTrack(event.target.checked) 13 | } 14 | return ( 15 | 16 | 17 | Enable audio track 18 | 19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/components/DevtoolsPane/VideoTrackForm.tsx: -------------------------------------------------------------------------------- 1 | import type React from 'react' 2 | import { FormGroup } from 'react-bootstrap' 3 | 4 | import { setVideoTrack } from '@/app/actions' 5 | import { useSoraDevtoolsStore } from '@/app/store' 6 | 7 | import { TooltipFormCheck } from './TooltipFormCheck.tsx' 8 | 9 | export const VideoTrackForm: React.FC = () => { 10 | const videoTrack = useSoraDevtoolsStore((state) => state.videoTrack) 11 | const onChange = (event: React.ChangeEvent): void => { 12 | setVideoTrack(event.target.checked) 13 | } 14 | return ( 15 | 16 | 17 | Enable video track 18 | 19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/components/DevtoolsPane/TooltipFormLabel.tsx: -------------------------------------------------------------------------------- 1 | import type React from 'react' 2 | import { FormLabel, OverlayTrigger, Popover } from 'react-bootstrap' 3 | 4 | import { INSTRUCTIONS } from '@/constants' 5 | 6 | type Props = { 7 | kind: string 8 | children: React.ReactNode 9 | } 10 | export const TooltipFormLabel: React.FC = (props) => { 11 | const instruction = INSTRUCTIONS[props.kind] 12 | if (!instruction) { 13 | return {props.children} 14 | } 15 | return ( 16 | 20 | {instruction.description} 21 | 22 | } 23 | > 24 | {props.children} 25 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/rpc.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "2025.2.0/RequestSimulcastRid", 4 | "params": { 5 | "rid": "r1" 6 | } 7 | }, 8 | { 9 | "method": "2025.2.0/RequestSpotlightRid", 10 | "params": { 11 | "recv_connection_id": "", 12 | "spotlight_focus_rid": "r0", 13 | "spotlight_unfocus_rid": "none" 14 | } 15 | }, 16 | { 17 | "method": "2025.2.0/ResetSpotlightRid", 18 | "params": { 19 | "recv_connection_id": "" 20 | } 21 | }, 22 | { 23 | "method": "2025.2.0/PutSignalingNotifyMetadata", 24 | "params": { 25 | "metadata": { 26 | "spam": "egg" 27 | }, 28 | "push": true 29 | } 30 | }, 31 | { 32 | "method": "2025.2.0/PutSignalingNotifyMetadataItem", 33 | "params": { 34 | "key": "spam", 35 | "value": "ham", 36 | "push": true 37 | } 38 | } 39 | ] 40 | -------------------------------------------------------------------------------- /src/components/DevtoolsPane/ConnectButton.tsx: -------------------------------------------------------------------------------- 1 | import type React from 'react' 2 | 3 | import { connectSora } from '@/app/actions' 4 | import { useSoraDevtoolsStore } from '@/app/store' 5 | 6 | export const ConnectButton: React.FC = () => { 7 | const connectionStatus = useSoraDevtoolsStore((state) => state.soraContents.connectionStatus) 8 | const connect = (): void => { 9 | connectSora() 10 | } 11 | return ( 12 |
13 | 25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/components/ClipboardIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const ClipboardIcon = React.memo(() => { 4 | return ( 5 | 13 | ClipboardIcon 14 | 18 | 22 | 23 | ) 24 | }) 25 | -------------------------------------------------------------------------------- /src/components/DevtoolsPane/DisconnectButton.tsx: -------------------------------------------------------------------------------- 1 | import type React from 'react' 2 | 3 | import { disconnectSora } from '@/app/actions' 4 | import { useSoraDevtoolsStore } from '@/app/store' 5 | 6 | export const DisconnectButton: React.FC = () => { 7 | const connectionStatus = useSoraDevtoolsStore((state) => state.soraContents.connectionStatus) 8 | const disconnect = (): void => { 9 | disconnectSora() 10 | } 11 | return ( 12 |
13 | 25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/components/DebugPane/Filter.tsx: -------------------------------------------------------------------------------- 1 | import type React from 'react' 2 | import { FormControl, FormGroup, FormLabel } from 'react-bootstrap' 3 | 4 | import { setDebugFilterText } from '@/app/actions' 5 | import { useSoraDevtoolsStore } from '@/app/store' 6 | 7 | export const DebugFilter: React.FC = () => { 8 | const debugFilterText = useSoraDevtoolsStore((state) => state.debugFilterText) 9 | const onChange = (event: React.ChangeEvent): void => { 10 | setDebugFilterText(event.target.value) 11 | } 12 | return ( 13 | 14 | Filter: 15 | 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import react from '@vitejs/plugin-react' 3 | import { defineConfig } from 'vite' 4 | 5 | export default defineConfig({ 6 | plugins: [react()], 7 | build: { 8 | minify: true, 9 | target: 'esnext', 10 | rollupOptions: { 11 | input: { 12 | index: path.resolve(__dirname, './index.html'), 13 | }, 14 | output: { 15 | manualChunks: { 16 | react: ['react', 'react-dom', 'react-bootstrap'], 17 | zustand: ['zustand', 'immer'], 18 | 'mp4-media-stream': ['@shiguredo/mp4-media-stream'], 19 | 'noise-suppression': ['@shiguredo/noise-suppression'], 20 | 'virtual-background': ['@shiguredo/virtual-background'], 21 | 'sora-js-sdk': ['sora-js-sdk'], 22 | }, 23 | }, 24 | }, 25 | }, 26 | resolve: { 27 | alias: { 28 | '@': path.resolve(__dirname, './src'), 29 | }, 30 | }, 31 | }) 32 | -------------------------------------------------------------------------------- /src/components/DevtoolsPane/AudioForm.tsx: -------------------------------------------------------------------------------- 1 | import type React from 'react' 2 | import { FormGroup } from 'react-bootstrap' 3 | 4 | import { setAudio } from '@/app/actions' 5 | import { useSoraDevtoolsStore } from '@/app/store' 6 | import { isFormDisabled } from '@/utils' 7 | 8 | import { TooltipFormCheck } from './TooltipFormCheck.tsx' 9 | 10 | export const AudioForm: React.FC = () => { 11 | const audio = useSoraDevtoolsStore((state) => state.audio) 12 | const connectionStatus = useSoraDevtoolsStore((state) => state.soraContents.connectionStatus) 13 | const disabled = isFormDisabled(connectionStatus) 14 | const onChange = (event: React.ChangeEvent): void => { 15 | setAudio(event.target.checked) 16 | } 17 | return ( 18 | 19 | 20 | audio 21 | 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/components/DevtoolsPane/VideoForm.tsx: -------------------------------------------------------------------------------- 1 | import type React from 'react' 2 | import { FormGroup } from 'react-bootstrap' 3 | 4 | import { setVideo } from '@/app/actions' 5 | import { useSoraDevtoolsStore } from '@/app/store' 6 | import { isFormDisabled } from '@/utils' 7 | 8 | import { TooltipFormCheck } from './TooltipFormCheck.tsx' 9 | 10 | export const VideoForm: React.FC = () => { 11 | const video = useSoraDevtoolsStore((state) => state.video) 12 | const connectionStatus = useSoraDevtoolsStore((state) => state.soraContents.connectionStatus) 13 | const disabled = isFormDisabled(connectionStatus) 14 | const onChange = (event: React.ChangeEvent): void => { 15 | setVideo(event.target.checked) 16 | } 17 | return ( 18 | 19 | 20 | video 21 | 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. 4 | # 5 | # Source repository: https://github.com/actions/dependency-review-action 6 | # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement 7 | name: 'Dependency Review' 8 | on: [pull_request] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | dependency-review: 15 | runs-on: ubuntu-24.04 16 | steps: 17 | - name: 'Checkout Repository' 18 | uses: actions/checkout@v5 19 | - name: 'Dependency Review' 20 | uses: actions/dependency-review-action@v4 21 | -------------------------------------------------------------------------------- /src/components/DevtoolsPane/FakeVolumeForm.tsx: -------------------------------------------------------------------------------- 1 | import type React from 'react' 2 | import { Form, FormGroup } from 'react-bootstrap' 3 | 4 | import { setFakeVolume } from '@/app/actions' 5 | import { useSoraDevtoolsStore } from '@/app/store' 6 | 7 | import { TooltipFormLabel } from './TooltipFormLabel.tsx' 8 | 9 | export const FakeVolumeForm: React.FC = () => { 10 | const mediaType = useSoraDevtoolsStore((state) => state.mediaType) 11 | const fakeVolume = useSoraDevtoolsStore((state) => state.fakeVolume) 12 | const onChange = (event: React.ChangeEvent): void => { 13 | setFakeVolume(event.target.value) 14 | } 15 | if (mediaType !== 'fakeMedia') { 16 | return null 17 | } 18 | return ( 19 | 20 | fakeVolume: 21 | 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/components/DevtoolsPane/DisposeMediaButton.tsx: -------------------------------------------------------------------------------- 1 | import type React from 'react' 2 | 3 | import { disposeMedia } from '@/app/actions' 4 | import { useSoraDevtoolsStore } from '@/app/store' 5 | import { isFormDisabled } from '@/utils' 6 | 7 | export const DisposeMediaButton: React.FC = () => { 8 | const onClick = (): void => { 9 | disposeMedia() 10 | } 11 | const connectionStatus = useSoraDevtoolsStore((state) => state.soraContents.connectionStatus) 12 | const sora = useSoraDevtoolsStore((state) => state.soraContents.sora) 13 | const role = useSoraDevtoolsStore((state) => state.role) 14 | const disabled = role === 'recvonly' || sora !== null || isFormDisabled(connectionStatus) 15 | return ( 16 |
17 | 25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/components/DevtoolsPane/RequestMediaButton.tsx: -------------------------------------------------------------------------------- 1 | import type React from 'react' 2 | 3 | import { requestMedia } from '@/app/actions' 4 | import { useSoraDevtoolsStore } from '@/app/store' 5 | import { isFormDisabled } from '@/utils' 6 | 7 | export const RequestMediaButton: React.FC = () => { 8 | const onClick = (): void => { 9 | requestMedia() 10 | } 11 | const connectionStatus = useSoraDevtoolsStore((state) => state.soraContents.connectionStatus) 12 | const sora = useSoraDevtoolsStore((state) => state.soraContents.sora) 13 | const role = useSoraDevtoolsStore((state) => state.role) 14 | const disabled = role === 'recvonly' || sora !== null || isFormDisabled(connectionStatus) 15 | return ( 16 |
17 | 25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/components/Video/ResetSpotlightRidButton.tsx: -------------------------------------------------------------------------------- 1 | import type React from 'react' 2 | 3 | import { useSoraDevtoolsStore } from '@/app/store' 4 | import { rpc } from '@/rpc' 5 | 6 | export const ResetSpotlightRidButton: React.FC = () => { 7 | const conn = useSoraDevtoolsStore((state) => state.soraContents.sora) 8 | const connectionStatus = useSoraDevtoolsStore((state) => state.soraContents.connectionStatus) 9 | 10 | const onClick = async (): Promise => { 11 | if (!conn || connectionStatus !== 'connected') { 12 | return 13 | } 14 | 15 | await rpc( 16 | conn, 17 | '2025.2.0/ResetSpotlightRid', 18 | {}, 19 | { notification: false, showMethodAlert: true }, 20 | ) 21 | } 22 | 23 | return ( 24 |
25 | 32 |
33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /src/DevTools.tsx: -------------------------------------------------------------------------------- 1 | import type React from 'react' 2 | import { useEffect } from 'react' 3 | 4 | import { disconnectSora, setMediaDevices, unregisterServiceWorker } from '@/app/actions' 5 | import { setInitialParameter } from '@/app/actions' 6 | import { DebugPane } from '@/components/DebugPane' 7 | import { DevtoolsPane } from '@/components/DevtoolsPane' 8 | import { Footer } from '@/components/Footer' 9 | import { Header } from '@/components/Header' 10 | 11 | const Devtools: React.FC = () => { 12 | useEffect(() => { 13 | setInitialParameter() 14 | setMediaDevices() 15 | unregisterServiceWorker() 16 | return () => { 17 | disconnectSora() 18 | } 19 | }, []) 20 | return ( 21 | <> 22 |
23 |
24 |
25 |
26 | 27 | 28 |
29 |
30 |
31 |