Skip to content

Commit

Permalink
Merge pull request #572 from shiguredo/feature/add-example-replace-track
Browse files Browse the repository at this point in the history
replace track のサンプルを追加する
  • Loading branch information
voluntas authored Nov 11, 2024
2 parents 3d10590 + 368dbeb commit cffbf17
Show file tree
Hide file tree
Showing 6 changed files with 251 additions and 2 deletions.
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@

### misc

- [ADD] リプレイストラックのサンプルを追加する
- @voluntas
- [ADD] メッセージングヘッダーの E2E テストを追加する
- @voluntas
- [ADD] npm に登録されている stable SDK の E2E テストを追加する
Expand Down
1 change: 1 addition & 0 deletions examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<li><a href="/recvonly/">視聴サンプル</a></li>
<li><a href="/check_stereo/">ステレオチェックサンプル</a></li>
<li><a href="/check_stereo_multi/">ステレオチェックサンプル(マルチストリーム)</a></li>
<li><a href="/replace_track/">track 入れ替えサンプル</a></li>
<li><a href="/spotlight_sendrecv/">スポットライト配信視聴サンプル</a></li>
<li><a href="/spotlight_sendonly/">スポットライト配信サンプル</a></li>
<li><a href="/spotlight_recvonly/">スポットライト視聴サンプル</a></li>
Expand Down
42 changes: 42 additions & 0 deletions examples/replace_track/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<html lang="ja">

<head>
<meta charset="utf-8">
<title>Sendrecv test</title>
</head>

<body>
<div class="container">
<h1>Replace track test</h1>
<h3 id="sdk-version"></h3>
<label for="channel-name">チャンネル名:</label>
<input type="text" id="channel-name" name="channel-name" value="replace_track"><br>
<label for="video-codec-type">ビデオコーデックを選択:</label>
<select id="video-codec-type">
<option value="" selected>未指定</option>
<option value="VP8">VP8</option>
<option value="VP9">VP9</option>
<option value="AV1">AV1</option>
<!--
<option value="H264">H264</option>
<option value="H265">H265</option>
-->
</select><br>
<button id="connect">connect</button>
<button id="replace-stream">replace stream</button>
<button id="disconnect">disconnect</button>
<button id="get-stats">getStats</button><br />
<div id="connection-id"></div>
<video id="local-video" autoplay="" playsinline="" controls="" muted=""
style="width: 320px; height: 240px; border: 1px solid black;"></video>
<div style="display: flex;">
<div id="remote-videos"></div>
</div>
<div id="stats-report" style="white-space: pre-wrap; font-family: monospace;"></div>
<div id="stats-report-json"></div>
</div>

<script type="module" src="./main.mts"></script>
</body>

</html>
203 changes: 203 additions & 0 deletions examples/replace_track/main.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import Sora, {
type SoraConnection,
type SignalingNotifyMessage,
type ConnectionPublisher,
type VideoCodecType,
type ConnectionOptions,
} from 'sora-js-sdk'

const getChannelName = (): string => {
const channelNameElement = document.querySelector<HTMLInputElement>('#channel-name')
const channelName = channelNameElement?.value
if (channelName === '' || channelName === undefined) {
throw new Error('channelName is empty')
}
return channelName
}

const getVideoCodecType = (): VideoCodecType | undefined => {
const videoCodecTypeElement = document.querySelector<HTMLSelectElement>('#video-codec-type')
const videoCodecType = videoCodecTypeElement?.value
if (videoCodecType === '') {
return undefined
}
return videoCodecType as VideoCodecType
}

document.addEventListener('DOMContentLoaded', async () => {
const SORA_SIGNALING_URL = import.meta.env.VITE_SORA_SIGNALING_URL
const SORA_CHANNEL_ID_PREFIX = import.meta.env.VITE_SORA_CHANNEL_ID_PREFIX || ''
const SORA_CHANNEL_ID_SUFFIX = import.meta.env.VITE_SORA_CHANNEL_ID_SUFFIX || ''
const ACCESS_TOKEN = import.meta.env.VITE_ACCESS_TOKEN || ''

let client: SoraClient

document.querySelector('#connect')?.addEventListener('click', async () => {
const channelName = getChannelName()
const videoCodecType = getVideoCodecType()

client = new SoraClient(
SORA_SIGNALING_URL,
SORA_CHANNEL_ID_PREFIX,
SORA_CHANNEL_ID_SUFFIX,
ACCESS_TOKEN,
channelName,
videoCodecType,
)

await client.connect()
})

document.querySelector('#replace-stream')?.addEventListener('click', async () => {
// audio: true, video: true なので要注意
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true })
await client.replaceStream(stream)
})

document.querySelector('#disconnect')?.addEventListener('click', async () => {
await client.disconnect()
})

document.querySelector('#get-stats')?.addEventListener('click', async () => {
const statsReport = await client.getStats()
const statsDiv = document.querySelector('#stats-report') as HTMLElement
const statsReportJsonDiv = document.querySelector('#stats-report-json')
if (statsDiv && statsReportJsonDiv) {
let statsHtml = ''
const statsReportJson: Record<string, unknown>[] = []
// biome-ignore lint/complexity/noForEach: <explanation>
statsReport.forEach((report) => {
statsHtml += `<h3>Type: ${report.type}</h3><ul>`
const reportJson: Record<string, unknown> = { id: report.id, type: report.type }
for (const [key, value] of Object.entries(report)) {
if (key !== 'type' && key !== 'id') {
statsHtml += `<li><strong>${key}:</strong> ${value}</li>`
reportJson[key] = value
}
}
statsHtml += '</ul>'
statsReportJson.push(reportJson)
})
statsDiv.innerHTML = statsHtml
// データ属性としても保存(オプション)
statsDiv.dataset.statsReportJson = JSON.stringify(statsReportJson)
}
})
})

class SoraClient {
private debug = false

private channelId: string
private metadata: { access_token: string }
private options: ConnectionOptions

private sora: SoraConnection
private connection: ConnectionPublisher

private stream: MediaStream

constructor(
signalingUrl: string,
channelIdPrefix: string,
channelIdSuffix: string,
accessToken: string,
channelName: string,
videoCodecType: VideoCodecType | undefined,
) {
this.sora = Sora.connection(signalingUrl, this.debug)
this.channelId = `${channelIdPrefix}${channelName}${channelIdSuffix}`
this.metadata = { access_token: accessToken }
this.options = {}

if (videoCodecType !== undefined) {
this.options = { ...this.options, videoCodecType: videoCodecType }
}

this.stream = new MediaStream()

this.connection = this.sora.sendrecv(this.channelId, this.metadata, this.options)

this.connection.on('notify', this.onnotify.bind(this))
this.connection.on('track', this.ontrack.bind(this))
this.connection.on('removetrack', this.onremovetrack.bind(this))
}

async connect() {
await this.connection.connect(this.stream)
const localVideo = document.querySelector<HTMLVideoElement>('#local-video')
if (localVideo) {
localVideo.srcObject = this.stream
}
}

async replaceStream(stream: MediaStream) {
if (stream.getAudioTracks().length > 0) {
await this.connection.replaceAudioTrack(this.stream, stream.getAudioTracks()[0])
}
if (stream.getVideoTracks().length > 0) {
await this.connection.replaceVideoTrack(this.stream, stream.getVideoTracks()[0])
}
this.stream = stream
}

async disconnect() {
await this.connection.disconnect()

// お掃除
const localVideo = document.querySelector<HTMLVideoElement>('#local-video')
if (localVideo) {
localVideo.srcObject = null
}
// お掃除
const remoteVideos = document.querySelector('#remote-videos')
if (remoteVideos) {
remoteVideos.innerHTML = ''
}
}

getStats(): Promise<RTCStatsReport> {
if (this.connection.pc === null) {
return Promise.reject(new Error('PeerConnection is not ready'))
}
return this.connection.pc.getStats()
}

private onnotify(event: SignalingNotifyMessage): void {
if (
event.event_type === 'connection.created' &&
this.connection.connectionId === event.connection_id
) {
const connectionIdElement = document.querySelector('#connection-id')
if (connectionIdElement) {
connectionIdElement.textContent = event.connection_id
}
}
}

private ontrack(event: RTCTrackEvent): void {
const stream = event.streams[0]
const remoteVideoId = `remote-video-${stream.id}`
const remoteVideos = document.querySelector('#remote-videos')
if (remoteVideos && !remoteVideos.querySelector(`#${remoteVideoId}`)) {
const remoteVideo = document.createElement('video')
remoteVideo.id = remoteVideoId
remoteVideo.style.border = '1px solid red'
remoteVideo.autoplay = true
remoteVideo.playsInline = true
remoteVideo.controls = true
remoteVideo.width = 320
remoteVideo.height = 240
remoteVideo.srcObject = stream
remoteVideos.appendChild(remoteVideo)
}
}

private onremovetrack(event: MediaStreamTrackEvent): void {
const target = event.target as MediaStream
const remoteVideo = document.querySelector(`#remote-video-${target.id}`)
if (remoteVideo) {
document.querySelector('#remote-videos')?.removeChild(remoteVideo)
}
}
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@
"vite": "5.4.10",
"vitest": "2.1.4"
},
"packageManager": "pnpm@9.11.0",
"packageManager": "pnpm@9.12.3",
"engines": {
"node": ">=18"
}
}
}
1 change: 1 addition & 0 deletions vite.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export default defineConfig({
recvonly: resolve(__dirname, 'examples/recvonly/index.html'),
check_stereo: resolve(__dirname, 'examples/check_stereo/index.html'),
check_stereo_multi: resolve(__dirname, 'examples/check_stereo_multi/index.html'),
replace_track: resolve(__dirname, 'examples/replace_track/index.html'),
simulcast: resolve(__dirname, 'examples/simulcast/index.html'),
spotlight_sendrecv: resolve(__dirname, 'examples/spotlight_sendrecv/index.html'),
spotlight_sendonly: resolve(__dirname, 'examples/spotlight_sendonly/index.html'),
Expand Down

0 comments on commit cffbf17

Please sign in to comment.