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
-
-
-
-
-
-
-
-
-
-
-
Recvonly
-
-
-
-
-
-
-
-
-
-
-
-
-
\ 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]))
-
- //