-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #572 from shiguredo/feature/add-example-replace-track
replace track のサンプルを追加する
- Loading branch information
Showing
6 changed files
with
251 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters