diff --git a/CHANGES.md b/CHANGES.md index 01f7fd8b..701dc224 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -34,6 +34,9 @@ - @voluntas - [ADD] 新しい examples を追加する - @voluntas +- [CHANGE] examples を削除する + - へ移動 + - @voluntas - [CHANGE] Node.js 18 をビルドとテストから落とす - @voluntas - [CHANGE] examples を e2e-tests に変更する diff --git a/README.md b/README.md index cb3c7510..1d778d3d 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,21 @@ Please read before use. 利用前に をお読みください。 +## 条件 + +- WebRTC SFU Sora 2024.1.0 以降 +- TypeScript 5.1 以降 + ## 使い方 使い方は [Sora JavaScript SDK ドキュメント](https://sora-js-sdk.shiguredo.jp/) を参照してください。 +## サンプル + +サンプルは [sora-js-sdk-examples](https://github.com/shiguredo/sora-js-sdk-examples) を参照してください。 + +## インストール + ### npm ```bash @@ -33,11 +44,6 @@ npm install sora-js-sdk pnpm add sora-js-sdk ``` -## 条件 - -- WebRTC SFU Sora 2024.1.0 以降 -- TypeScript 5.1 以降 - ### Node.js の条件 - Sora JavaScript SDK 2024.2.x までは **Node.js 18.0 以降** を要求します @@ -50,18 +56,6 @@ pnpm add sora-js-sdk > - Firefox 113 以降 > - Safari 16.4 以降 -## サンプル - -サンプルを Vite にて起動できます。 - -```bash -# .env.local を作成して適切な値を設定してください -$ cp .env.template .env.local -$ pnpm install -$ pnpm run build -$ pnpm run dev -``` - ## E2E (End to End) テスト Playwright を利用した E2E テストを実行できます。 diff --git a/e2e-tests/data_channel_signaling_only/index.html b/e2e-tests/data_channel_signaling_only/index.html index eb2965d0..f0c7cdfb 100644 --- a/e2e-tests/data_channel_signaling_only/index.html +++ b/e2e-tests/data_channel_signaling_only/index.html @@ -2,6 +2,7 @@ + DataChannelSignalingOnly test diff --git a/e2e-tests/index.html b/e2e-tests/index.html index 653e4f3b..8b3a53fe 100644 --- a/e2e-tests/index.html +++ b/e2e-tests/index.html @@ -2,6 +2,7 @@ + Sora JavaScript SDK example diff --git a/e2e-tests/recvonly/index.html b/e2e-tests/recvonly/index.html index efa98655..705aab3c 100644 --- a/e2e-tests/recvonly/index.html +++ b/e2e-tests/recvonly/index.html @@ -2,6 +2,7 @@ + Recvonly test diff --git a/e2e-tests/sendonly/index.html b/e2e-tests/sendonly/index.html index 883b7018..2003eb6b 100644 --- a/e2e-tests/sendonly/index.html +++ b/e2e-tests/sendonly/index.html @@ -2,6 +2,7 @@ + Sendonly test diff --git a/e2e-tests/sendonly_audio/index.html b/e2e-tests/sendonly_audio/index.html index ba37129b..62414064 100644 --- a/e2e-tests/sendonly_audio/index.html +++ b/e2e-tests/sendonly_audio/index.html @@ -3,6 +3,7 @@ + Sendonly Audio test diff --git a/e2e-tests/sendrecv/index.html b/e2e-tests/sendrecv/index.html index ad144a5a..71ab4a92 100644 --- a/e2e-tests/sendrecv/index.html +++ b/e2e-tests/sendrecv/index.html @@ -2,6 +2,7 @@ + Sendrecv test diff --git a/e2e-tests/simulcast/index.html b/e2e-tests/simulcast/index.html index e39a21eb..7d57edbd 100644 --- a/e2e-tests/simulcast/index.html +++ b/e2e-tests/simulcast/index.html @@ -2,6 +2,7 @@ + Simulcast test diff --git a/e2e-tests/spotlight_recvonly/index.html b/e2e-tests/spotlight_recvonly/index.html index e57de8d5..d082bca0 100644 --- a/e2e-tests/spotlight_recvonly/index.html +++ b/e2e-tests/spotlight_recvonly/index.html @@ -2,6 +2,7 @@ + Spotlight Recvonly test diff --git a/e2e-tests/spotlight_sendonly/index.html b/e2e-tests/spotlight_sendonly/index.html index 4dd64007..8021200d 100644 --- a/e2e-tests/spotlight_sendonly/index.html +++ b/e2e-tests/spotlight_sendonly/index.html @@ -2,6 +2,7 @@ + Spotlight Sendonly test diff --git a/e2e-tests/spotlight_sendrecv/index.html b/e2e-tests/spotlight_sendrecv/index.html index 0ca6b928..590c067a 100644 --- a/e2e-tests/spotlight_sendrecv/index.html +++ b/e2e-tests/spotlight_sendrecv/index.html @@ -2,6 +2,7 @@ + Spotlight Sendrecv test diff --git a/e2e-tests/whep/index.html b/e2e-tests/whep/index.html index 14ddff1d..af028efe 100644 --- a/e2e-tests/whep/index.html +++ b/e2e-tests/whep/index.html @@ -4,8 +4,7 @@ - + Sora WHEP Client diff --git a/e2e-tests/whip/index.html b/e2e-tests/whip/index.html index 9477457c..c5bb88a9 100644 --- a/e2e-tests/whip/index.html +++ b/e2e-tests/whip/index.html @@ -3,10 +3,8 @@ - - - Sora WHEP Client + + Sora WHIP Client diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index 14732d02..00000000 --- a/examples/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Sora JavaScript SDK サンプル - -## 使い方 - -```bash -$ git clone git@github.com:shiguredo/sora-js-sdk.git -$ cd sora-js-sdk -# .env.local を作成して適切な値を設定してください -$ cp .env.template .env.local -$ pnpm install -$ pnpm build -$ pnpm dev -``` diff --git a/examples/check_stereo/index.html b/examples/check_stereo/index.html deleted file mode 100644 index 590b4bfc..00000000 --- a/examples/check_stereo/index.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - - Stereo Check シンプルサンプル - - - - - -

Stereo Check サンプル

-
-

Sendonly

- -
- - -
- -
-
-

Waveform

-
-
- -
-
-
-

Recvonly

-
- -
- -
-
-

Waveform

-
- -
- -
- - - - - \ No newline at end of file diff --git a/examples/check_stereo/main.mts b/examples/check_stereo/main.mts deleted file mode 100644 index 57a2c1c7..00000000 --- a/examples/check_stereo/main.mts +++ /dev/null @@ -1,440 +0,0 @@ -import Sora, { - type SoraConnection, - type SignalingNotifyMessage, - type ConnectionPublisher, - type ConnectionSubscriber, -} from 'sora-js-sdk' - -document.addEventListener('DOMContentLoaded', async () => { - // 環境変数の読み込み - const signalingUrl = import.meta.env.VITE_SORA_SIGNALING_URL - - const uuid = crypto.randomUUID() - - // Sora クライアントの初期化 - const sendonly = new SendonlyClient(signalingUrl, uuid) - const recvonly = new RecvonlyClient(signalingUrl, uuid) - - // デバイスリストの取得と設定 - await updateDeviceLists() - - // デバイスの変更を監視 - navigator.mediaDevices.addEventListener('devicechange', updateDeviceLists) - - document.querySelector('#sendonly-connect')?.addEventListener('click', async () => { - const audioInputSelect = document.querySelector('#sendonly-audio-input') - const selectedAudioDeviceId = audioInputSelect?.value - const stream = await navigator.mediaDevices.getUserMedia({ - video: false, - audio: { - deviceId: selectedAudioDeviceId ? { exact: selectedAudioDeviceId } : undefined, - echoCancellation: false, - noiseSuppression: false, - autoGainControl: false, - channelCount: 2, - sampleRate: 48000, - sampleSize: 16, - }, - }) - await sendonly.connect(stream) - }) - - document.querySelector('#recvonly-connect')?.addEventListener('click', async () => { - await recvonly.connect() - }) -}) - -// デバイスリストを更新する関数 -async function updateDeviceLists() { - const devices = await navigator.mediaDevices.enumerateDevices() - - const audioInputSelect = document.querySelector('#sendonly-audio-input') - - if (audioInputSelect) { - audioInputSelect.innerHTML = '' - const audioInputDevices = devices.filter((device) => device.kind === 'audioinput') - for (const device of audioInputDevices) { - const option = document.createElement('option') - option.value = device.deviceId - option.text = device.label || `マイク ${audioInputSelect.length + 1}` - audioInputSelect.appendChild(option) - } - } -} - -class SendonlyClient { - private debug = false - private channelId: string - private options: object = {} - - private sora: SoraConnection - private connection: ConnectionPublisher - - private canvas: HTMLCanvasElement | null = null - private canvasCtx: CanvasRenderingContext2D | null = null - - private channelCheckInterval: number | undefined - - constructor(signalingUrl: string, channelId: string) { - this.sora = Sora.connection(signalingUrl, this.debug) - - this.channelId = channelId - - this.connection = this.sora.sendonly(this.channelId, undefined, this.options) - - this.connection.on('notify', this.onnotify.bind(this)) - - this.initializeCanvas() - } - - async connect(stream: MediaStream): Promise { - const audioTrack = stream.getAudioTracks()[0] - if (!audioTrack) { - throw new Error('Audio track not found') - } - - await this.connection.connect(stream) - this.analyzeAudioStream(new MediaStream([audioTrack])) - - // チャネル数の定期チェックを開始 - this.startChannelCheck() - } - - async getChannels(): Promise { - if (!this.connection.pc) { - return undefined - } - const sender = this.connection.pc.getSenders().find((sender) => sender.track?.kind === 'audio') - if (!sender) { - return undefined - } - return sender.getParameters().codecs[0].channels - } - - private initializeCanvas() { - this.canvas = document.querySelector('#sendonly-waveform') - if (this.canvas) { - this.canvasCtx = this.canvas.getContext('2d') - } - } - - analyzeAudioStream(stream: MediaStream) { - const audioContext = new AudioContext({ sampleRate: 48000, latencyHint: 'interactive' }) - const source = audioContext.createMediaStreamSource(stream) - const splitter = audioContext.createChannelSplitter(2) - const analyserL = audioContext.createAnalyser() - const analyserR = audioContext.createAnalyser() - - source.connect(splitter) - splitter.connect(analyserL, 0) - splitter.connect(analyserR, 1) - - analyserL.fftSize = 2048 - analyserR.fftSize = 2048 - - const bufferLength = analyserL.frequencyBinCount - const dataArrayL = new Float32Array(bufferLength) - const dataArrayR = new Float32Array(bufferLength) - - const analyze = () => { - analyserL.getFloatTimeDomainData(dataArrayL) - analyserR.getFloatTimeDomainData(dataArrayR) - - this.drawWaveforms(dataArrayL, dataArrayR) - - let difference = 0 - for (let i = 0; i < dataArrayL.length; i++) { - difference += Math.abs(dataArrayL[i] - dataArrayR[i]) - } - - const isStereo = difference !== 0 - const result = isStereo ? 'Stereo' : 'Mono' - - // differenceの値を表示する要素を追加 - const differenceElement = document.querySelector('#sendonly-difference-value') - if (differenceElement) { - differenceElement.textContent = `Difference: ${difference.toFixed(6)}` - } - - // sendonly-stereo 要素に結果を反映 - const sendonlyStereoElement = document.querySelector('#sendonly-stereo') - if (sendonlyStereoElement) { - sendonlyStereoElement.textContent = result - } - - requestAnimationFrame(analyze) - } - - analyze() - - if (audioContext.state === 'suspended') { - audioContext.resume() - } - } - - private drawWaveforms(dataArrayL: Float32Array, dataArrayR: Float32Array) { - if (!this.canvasCtx || !this.canvas) return - - const width = this.canvas.width - const height = this.canvas.height - const bufferLength = dataArrayL.length - - this.canvasCtx.fillStyle = 'rgb(240, 240, 240)' - this.canvasCtx.fillRect(0, 0, width, height) - const drawChannel = (dataArray: Float32Array, color: string, offset: number) => { - if (!this.canvasCtx) return - - this.canvasCtx.lineWidth = 3 - this.canvasCtx.strokeStyle = color - this.canvasCtx.beginPath() - - const sliceWidth = (width * 1.0) / bufferLength - let x = 0 - - for (let i = 0; i < bufferLength; i++) { - const v = dataArray[i] - const y = height / 2 + v * height * 0.8 + offset - - if (i === 0) { - this.canvasCtx?.moveTo(x, y) - } else { - this.canvasCtx?.lineTo(x, y) - } - - x += sliceWidth - } - - this.canvasCtx?.lineTo(width, height / 2 + offset) - this.canvasCtx?.stroke() - } - - // 左チャンネル(青)を少し上にずらして描画 - this.canvasCtx.globalAlpha = 0.7 - drawChannel(dataArrayL, 'rgb(0, 0, 255)', -10) - - // 右チャンネル(赤)を少し下にずらして描画 - this.canvasCtx.globalAlpha = 0.7 - drawChannel(dataArrayR, 'rgb(255, 0, 0)', 10) - - // モノラルかステレオかを判定して表示 - const isMonaural = this.isMonaural(dataArrayL, dataArrayR) - this.canvasCtx.fillStyle = 'black' - this.canvasCtx.font = '20px Arial' - this.canvasCtx.fillText(isMonaural ? 'Monaural' : 'Stereo', 10, 30) - } - - private isMonaural(dataArrayL: Float32Array, dataArrayR: Float32Array): boolean { - const threshold = 0.001 - for (let i = 0; i < dataArrayL.length; i++) { - if (Math.abs(dataArrayL[i] - dataArrayR[i]) > threshold) { - return false - } - } - return true - } - - private onnotify(event: SignalingNotifyMessage) { - // 自分の connection_id を取得する - if ( - event.event_type === 'connection.created' && - this.connection.connectionId === event.connection_id - ) { - const connectionIdElement = document.querySelector('#sendonly-connection-id') - if (connectionIdElement) { - connectionIdElement.textContent = event.connection_id - } - } - } - - private startChannelCheck() { - this.channelCheckInterval = window.setInterval(async () => { - const channels = await this.getChannels() - const channelElement = document.querySelector('#sendonly-channels') - if (channelElement) { - channelElement.textContent = - channels !== undefined ? `getParameters codecs channels: ${channels}` : 'undefined' - } - }, 1000) // 1秒ごとにチェック - } -} - -class RecvonlyClient { - private debug = false - private channelId: string - private options: object = { - video: false, - audio: true, - } - - private sora: SoraConnection - private connection: ConnectionSubscriber - - private canvas: HTMLCanvasElement | null = null - private canvasCtx: CanvasRenderingContext2D | null = null - - constructor(signalingUrl: string, channelId: string) { - this.channelId = channelId - - this.sora = Sora.connection(signalingUrl, this.debug) - - this.connection = this.sora.recvonly(this.channelId, undefined, this.options) - - this.connection.on('notify', this.onnotify.bind(this)) - this.connection.on('track', this.ontrack.bind(this)) - - this.initializeCanvas() - } - - async connect(): Promise { - const forceStereoOutputElement = document.querySelector('#forceStereoOutput') - const forceStereoOutput = forceStereoOutputElement ? forceStereoOutputElement.checked : false - this.connection.options.forceStereoOutput = forceStereoOutput - - await this.connection.connect() - } - - private initializeCanvas() { - this.canvas = document.querySelector('#recvonly-waveform') - if (this.canvas) { - this.canvasCtx = this.canvas.getContext('2d') - } - } - - analyzeAudioStream(stream: MediaStream) { - const audioContext = new AudioContext({ sampleRate: 48000, latencyHint: 'interactive' }) - const source = audioContext.createMediaStreamSource(stream) - const splitter = audioContext.createChannelSplitter(2) - const analyserL = audioContext.createAnalyser() - const analyserR = audioContext.createAnalyser() - - source.connect(splitter) - splitter.connect(analyserL, 0) - splitter.connect(analyserR, 1) - - analyserL.fftSize = 2048 - analyserR.fftSize = 2048 - - const bufferLength = analyserL.frequencyBinCount - const dataArrayL = new Float32Array(bufferLength) - const dataArrayR = new Float32Array(bufferLength) - - const analyze = () => { - analyserL.getFloatTimeDomainData(dataArrayL) - analyserR.getFloatTimeDomainData(dataArrayR) - - this.drawWaveforms(dataArrayL, dataArrayR) - - let difference = 0 - for (let i = 0; i < dataArrayL.length; i++) { - difference += Math.abs(dataArrayL[i] - dataArrayR[i]) - } - - const isStereo = difference !== 0 - const result = isStereo ? 'Stereo' : 'Mono' - - // differenceの値を表示する要素を追加 - const differenceElement = document.querySelector('#recvonly-difference-value') - if (differenceElement) { - differenceElement.textContent = `Difference: ${difference.toFixed(6)}` - } - - // 既存のコード - const recvonlyStereoElement = document.querySelector('#recvonly-stereo') - if (recvonlyStereoElement) { - recvonlyStereoElement.textContent = result - } - - requestAnimationFrame(analyze) - } - - analyze() - - if (audioContext.state === 'suspended') { - audioContext.resume() - } - } - - private drawWaveforms(dataArrayL: Float32Array, dataArrayR: Float32Array) { - if (!this.canvasCtx || !this.canvas) return - - const width = this.canvas.width - const height = this.canvas.height - const bufferLength = dataArrayL.length - - this.canvasCtx.fillStyle = 'rgb(240, 240, 240)' - this.canvasCtx.fillRect(0, 0, width, height) - const drawChannel = (dataArray: Float32Array, color: string, offset: number) => { - if (!this.canvasCtx) return - - this.canvasCtx.lineWidth = 3 - this.canvasCtx.strokeStyle = color - this.canvasCtx.beginPath() - - const sliceWidth = (width * 1.0) / bufferLength - let x = 0 - - for (let i = 0; i < bufferLength; i++) { - const v = dataArray[i] - const y = height / 2 + v * height * 0.8 + offset - - if (i === 0) { - this.canvasCtx?.moveTo(x, y) - } else { - this.canvasCtx?.lineTo(x, y) - } - - x += sliceWidth - } - - this.canvasCtx?.lineTo(width, height / 2 + offset) - this.canvasCtx?.stroke() - } - - this.canvasCtx.globalAlpha = 0.7 - drawChannel(dataArrayL, 'rgb(0, 0, 255)', -10) - drawChannel(dataArrayR, 'rgb(255, 0, 0)', 10) - - const isMonaural = this.isMonaural(dataArrayL, dataArrayR) - this.canvasCtx.fillStyle = 'black' - this.canvasCtx.font = '20px Arial' - this.canvasCtx.fillText(isMonaural ? 'Monaural' : 'Stereo', 10, 30) - } - - private isMonaural(dataArrayL: Float32Array, dataArrayR: Float32Array): boolean { - const threshold = 0.001 - for (let i = 0; i < dataArrayL.length; i++) { - if (Math.abs(dataArrayL[i] - dataArrayR[i]) > threshold) { - return false - } - } - return true - } - - private onnotify(event: SignalingNotifyMessage) { - // 自分の connection_id を取得する - if ( - event.event_type === 'connection.created' && - this.connection.connectionId === event.connection_id - ) { - const connectionIdElement = document.querySelector('#recvonly-connection-id') - if (connectionIdElement) { - connectionIdElement.textContent = event.connection_id - } - } - } - - private ontrack(event: RTCTrackEvent) { - // Sora の場合、event.streams には MediaStream が 1 つだけ含まれる - const stream = event.streams[0] - if (event.track.kind === 'audio') { - this.analyzeAudioStream(new MediaStream([event.track])) - - //