初始化

This commit is contained in:
2026-04-29 15:18:30 +08:00
commit e47eee39ed
111 changed files with 44168 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
div#select, div#resolution {
margin: 1em;
}
button {
margin: 0 20px 5px 0;
vertical-align: top;
width: 155px;
}
div#buttons {
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
margin: 1em 0 1em 0;
padding: 1em 0 1em 0;
}
div#local {
margin: 0 20px 0 0;
}
div#preview {
border-bottom: 1px solid #eee;
margin: 0 0 1em 0;
padding: 0 0 0.5em 0;
}
div#preview>div {
display: inline-block;
vertical-align: top;
width: calc(50% - 20px);
}
div#connectionId {
margin: 1em;
}
h2 {
margin: 0 0 0.5em 0;
}
textarea {
color: #444;
font-size: 0.9em;
font-weight: 300;
width: calc(20% - 10px);
height: 1.3em;
line-height: 1.3;
vertical-align: middle;
}
video {
height: 225px;
}

View File

@@ -0,0 +1,83 @@
<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="icon" href="../images/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="../css/main.css" />
<link rel="stylesheet" href="css/style.css" />
<title>Bidirectional Sample</title>
</head>
<body>
<div id="container">
<h1>Bidirectional Sample</h1>
<div id="warning" hidden=true></div>
<div id="select">
<label for="videoSource">Video source: </label>
<select id="videoSource" autocomplete="off"></select>
<label for="audioSource">Audio source: </label>
<select id="audioSource" autocomplete="off"></select>
</div>
<div id="resolutionSelect">
<label for="videoResolution">Video resolution: </label><select id="videoResolution" autocomplete="off"></select>
</div>
<div id="resolutionInput">
<label for="cameraWidth">Camera width:</label><input id="cameraWidth" type="number" min="0" max="4096" autocomplete="off" disabled>
<label for="cameraHeight">Camera height:</label><input id="cameraHeight" type="number" min="0" max="4096" autocomplete="off" disabled>
</div>
<div id="buttons">
<button type="button" id="startVideoButton" autocomplete="off">Start Video</button>
<button type="button" id="setUpButton" autocomplete="off" disabled>Set Up</button>
<button type="button" id="hangUpButton" autocomplete="off" disabled>Hang Up</button>
</div>
<div id="preview">
<div id="local">
<h2>Local</h2>
<video id="localVideo" playsinline autoplay muted=true></video>
<div id="localVideoStats"></div>
</div>
<div id="remote">
<h2>Remote</h2>
<video id="remoteVideo" playsinline autoplay></video>
<div id="remoteVideoStats"></div>
</div>
</div>
<div class="box">
<span>Connection ID:</span>
<textarea id="textForConnectionId"></textarea>
</div>
<div class="box">
<span>Codec preferences:</span>
<select id="codecPreferences" autocomplete="off" disabled>
<option selected value="">Default</option>
</select>
</div>
<p>For more information about <code>Bidirectional</code> sample, see <a
href="https://docs.unity3d.com/Packages/com.unity.renderstreaming@3.1/manual/sample-bidirectional.html">Bidirectional
sample</a> document page.</p>
<div id="message"></div>
<section>
<a href="https://github.com/Unity-Technologies/UnityRenderStreaming/tree/develop/WebApp/public/bidirectional"
title="View source for this page on GitHub" id="viewSource">View source on GitHub</a>
</section>
</div>
</body>
<script type="text/javascript" src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<script src="https://unpkg.com/event-target@latest/min.js"></script>
<script src="https://unpkg.com/resize-observer-polyfill@1.5.0/dist/ResizeObserver.global.js"></script>
<script type="module" src="js/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,382 @@
/**
* 双向视频通话应用主文件
* 负责初始化视频设备、建立WebRTC连接、处理信令和显示视频流
*/
// 导入必要的模块
import { SendVideo } from "./sendvideo.js"; // 视频发送和接收处理
import { getServerConfig, getRTCConfiguration } from "../../js/config.js"; // 服务器配置和RTC配置
import { createDisplayStringArray } from "../../js/stats.js"; // 统计信息处理
import { RenderStreaming } from "../../module/renderstreaming.js"; // WebRTC连接管理
import { Signaling, WebSocketSignaling } from "../../module/signaling.js"; // 信令管理
// 默认视频流尺寸
const defaultStreamWidth = 1280;
const defaultStreamHeight = 720;
// 预定义的视频分辨率列表
const streamSizeList =
[
{ width: 640, height: 360 }, // 标清
{ width: 1280, height: 720 }, // 高清
{ width: 1920, height: 1080 }, // 全高清
{ width: 2560, height: 1440 }, // 2K
{ width: 3840, height: 2160 }, // 4K
{ width: 360, height: 640 }, // 竖屏标清
{ width: 720, height: 1280 }, // 竖屏高清
{ width: 1080, height: 1920 }, // 竖屏全高清
{ width: 1440, height: 2560 }, // 竖屏2K
{ width: 2160, height: 3840 }, // 竖屏4K
];
// DOM元素引用
const localVideo = document.getElementById('localVideo'); // 本地视频元素
const remoteVideo = document.getElementById('remoteVideo'); // 远程视频元素
const localVideoStatsDiv = document.getElementById('localVideoStats'); // 本地视频统计信息
const remoteVideoStatsDiv = document.getElementById('remoteVideoStats'); // 远程视频统计信息
const textForConnectionId = document.getElementById('textForConnectionId'); // 连接ID输入框
textForConnectionId.value = getRandom(); // 生成随机连接ID
const videoSelect = document.querySelector('select#videoSource'); // 视频设备选择
const audioSelect = document.querySelector('select#audioSource'); // 音频设备选择
const videoResolutionSelect = document.querySelector('select#videoResolution'); // 视频分辨率选择
const cameraWidthInput = document.querySelector('input#cameraWidth'); // 自定义宽度输入
const cameraHeightInput = document.querySelector('input#cameraHeight'); // 自定义高度输入
// 编解码器偏好设置
const codecPreferences = document.getElementById('codecPreferences');
// 检查浏览器是否支持设置编解码器偏好
const supportsSetCodecPreferences = window.RTCRtpTransceiver &&
'setCodecPreferences' in window.RTCRtpTransceiver.prototype;
const messageDiv = document.getElementById('message'); // 消息显示区域
messageDiv.style.display = 'none'; // 初始隐藏消息区域
let useCustomResolution = false; // 是否使用自定义分辨率
// 初始化输入选择和编解码器选择
setUpInputSelect();
showCodecSelect();
/** @type {SendVideo} */
let sendVideo = new SendVideo(localVideo, remoteVideo); // 视频处理实例
/** @type {RenderStreaming} */
let renderstreaming; // WebRTC连接管理实例
let useWebSocket; // 是否使用WebSocket信令
let connectionId; // 连接ID
// 按钮事件绑定
const startButton = document.getElementById('startVideoButton');
startButton.addEventListener('click', startVideo); // 启动视频按钮
const setupButton = document.getElementById('setUpButton');
setupButton.addEventListener('click', setUp); // 设置连接按钮
const hangUpButton = document.getElementById('hangUpButton');
hangUpButton.addEventListener('click', hangUp); // 挂断按钮
// 页面卸载前清理
window.addEventListener('beforeunload', async () => {
if(!renderstreaming)
return;
await renderstreaming.stop(); // 停止WebRTC连接
}, true);
// 初始化配置
setupConfig();
/**
* 初始化服务器配置
* @async
* @returns {Promise<void>}
*/
async function setupConfig() {
const res = await getServerConfig(); // 获取服务器配置
useWebSocket = res.useWebSocket; // 设置是否使用WebSocket
showWarningIfNeeded(res.startupMode); // 显示启动模式警告
}
/**
* 根据启动模式显示警告信息
* @param {string} startupMode - 启动模式,可能的值包括"public"和"private"
*/
function showWarningIfNeeded(startupMode) {
const warningDiv = document.getElementById("warning");
if (startupMode == "public") {
warningDiv.innerHTML = "<h4>Warning</h4> This sample is not working on Public Mode.";
warningDiv.hidden = false;
}
}
/**
* 启动本地视频
* @async
* @returns {Promise<void>}
*/
async function startVideo() {
// 禁用相关输入控件
videoSelect.disabled = true;
audioSelect.disabled = true;
videoResolutionSelect.disabled = true;
cameraWidthInput.disabled = true;
cameraHeightInput.disabled = true;
startButton.disabled = true;
let width = 0;
let height = 0;
// 根据选择的分辨率设置视频尺寸
if (useCustomResolution) {
width = cameraWidthInput.value ? cameraWidthInput.value : defaultStreamWidth;
height = cameraHeightInput.value ? cameraHeightInput.value : defaultStreamHeight;
} else {
const size = streamSizeList[videoResolutionSelect.value];
width = size.width;
height = size.height;
}
// 启动本地视频
await sendVideo.startLocalVideo(videoSelect.value, audioSelect.value, width, height);
// 启用设置按钮
setupButton.disabled = false;
}
/**
* 设置WebRTC连接
* @async
* @returns {Promise<void>}
*/
async function setUp() {
setupButton.disabled = true; // 禁用设置按钮
hangUpButton.disabled = false; // 启用挂断按钮
connectionId = textForConnectionId.value; // 获取连接ID
codecPreferences.disabled = true; // 禁用编解码器选择
// 创建信令实例
const signaling = useWebSocket ? new WebSocketSignaling() : new Signaling();
const config = getRTCConfiguration(); // 获取RTC配置
renderstreaming = new RenderStreaming(signaling, config); // 创建WebRTC连接管理实例
// 连接建立回调
renderstreaming.onConnect = () => {
const tracks = sendVideo.getLocalTracks(); // 获取本地媒体轨道
for (const track of tracks) {
renderstreaming.addTransceiver(track, { direction: 'sendonly' }); // 添加发送轨道
}
setCodecPreferences(); // 设置编解码器偏好
showStatsMessage(); // 显示统计信息
};
// 连接断开回调
renderstreaming.onDisconnect = () => {
hangUp(); // 挂断连接
};
// 轨道事件回调
renderstreaming.onTrackEvent = (data) => {
const direction = data.transceiver.direction;
if (direction == "sendrecv" || direction == "recvonly") {
sendVideo.addRemoteTrack(data.track); // 添加远程轨道
}
};
// 启动WebRTC连接
await renderstreaming.start();
await renderstreaming.createConnection(connectionId);
}
/**
* 设置编解码器偏好
*/
function setCodecPreferences() {
/** @type {RTCRtpCodecCapability[] | null} */
let selectedCodecs = null;
if (supportsSetCodecPreferences) {
const preferredCodec = codecPreferences.options[codecPreferences.selectedIndex];
if (preferredCodec.value !== '') {
const [mimeType, sdpFmtpLine] = preferredCodec.value.split(' ');
const { codecs } = RTCRtpSender.getCapabilities('video');
const selectedCodecIndex = codecs.findIndex(c => c.mimeType === mimeType && c.sdpFmtpLine === sdpFmtpLine);
const selectCodec = codecs[selectedCodecIndex];
selectedCodecs = [selectCodec];
}
}
if (selectedCodecs == null) {
return;
}
// 获取视频收发器并设置编解码器偏好
const transceivers = renderstreaming.getTransceivers().filter(t => t.receiver.track.kind == "video");
if (transceivers && transceivers.length > 0) {
transceivers.forEach(t => t.setCodecPreferences(selectedCodecs));
}
}
/**
* 挂断WebRTC连接
* @async
* @returns {Promise<void>}
*/
async function hangUp() {
clearStatsMessage(); // 清除统计信息
messageDiv.style.display = 'block';
messageDiv.innerText = `Disconnect peer on ${connectionId}.`;
hangUpButton.disabled = true; // 禁用挂断按钮
setupButton.disabled = false; // 启用设置按钮
// 删除连接并停止WebRTC
await renderstreaming.deleteConnection();
await renderstreaming.stop();
renderstreaming = null;
remoteVideo.srcObject = null; // 清除远程视频源
textForConnectionId.value = getRandom(); // 生成新的随机连接ID
connectionId = null;
// 启用编解码器选择
if (supportsSetCodecPreferences) {
codecPreferences.disabled = false;
}
}
/**
* 生成随机连接ID
* @returns {string} 5位随机数字字符串
*/
function getRandom() {
const max = 99999;
const length = String(max).length;
const number = Math.floor(Math.random() * max);
return (Array(length).join('0') + number).slice(-length); // 补零确保5位
}
/**
* 设置输入选择控件
* @async
* @returns {Promise<void>}
*/
async function setUpInputSelect() {
// 获取媒体设备列表
const deviceInfos = await navigator.mediaDevices.enumerateDevices();
// 填充视频设备选择
for (let i = 0; i !== deviceInfos.length; ++i) {
const deviceInfo = deviceInfos[i];
if (deviceInfo.kind === 'videoinput') {
const option = document.createElement('option');
option.value = deviceInfo.deviceId;
option.text = deviceInfo.label || `camera ${videoSelect.length + 1}`;
videoSelect.appendChild(option);
} else if (deviceInfo.kind === 'audioinput') {
// 填充音频设备选择
const option = document.createElement('option');
option.value = deviceInfo.deviceId;
option.text = deviceInfo.label || `mic ${audioSelect.length + 1}`;
audioSelect.appendChild(option);
}
}
// 填充视频分辨率选择
for (let i = 0; i < streamSizeList.length; i++) {
const streamSize = streamSizeList[i];
const option = document.createElement('option');
option.value = i;
option.text = `${streamSize.width} x ${streamSize.height}`;
videoResolutionSelect.appendChild(option);
}
// 添加自定义分辨率选项
const option = document.createElement('option');
option.value = streamSizeList.length;
option.text = 'Custom';
videoResolutionSelect.appendChild(option);
videoResolutionSelect.value = 1; // 默认选择1280 x 720
// 分辨率选择变化事件
videoResolutionSelect.addEventListener('change', (event) => {
const isCustom = event.target.value >= streamSizeList.length;
cameraWidthInput.disabled = !isCustom;
cameraHeightInput.disabled = !isCustom;
useCustomResolution = isCustom;
});
}
/**
* 显示编解码器选择
*/
function showCodecSelect() {
if (!supportsSetCodecPreferences) {
messageDiv.style.display = 'block';
messageDiv.innerHTML = `Current Browser does not support <a href="https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpTransceiver/setCodecPreferences">RTCRtpTransceiver.setCodecPreferences</a>.`;
return;
}
// 获取视频编解码器能力
const codecs = RTCRtpSender.getCapabilities('video').codecs;
codecs.forEach(codec => {
// 跳过冗余和FEC编解码器
if (['video/red', 'video/ulpfec', 'video/rtx'].includes(codec.mimeType)) {
return;
}
const option = document.createElement('option');
option.value = (codec.mimeType + ' ' + (codec.sdpFmtpLine || '')).trim();
option.innerText = option.value;
codecPreferences.appendChild(option);
});
codecPreferences.disabled = false;
}
// 统计信息相关变量
let lastStats; // 上次统计信息
let intervalId; // 统计信息更新间隔ID
/**
* 显示统计信息
*/
function showStatsMessage() {
// 每秒更新一次统计信息
intervalId = setInterval(async () => {
// 显示本地视频分辨率
if (localVideo.videoWidth) {
localVideoStatsDiv.innerHTML = `<strong>Sending resolution:</strong> ${localVideo.videoWidth} x ${localVideo.videoHeight} px`;
}
// 显示远程视频分辨率
if (remoteVideo.videoWidth) {
remoteVideoStatsDiv.innerHTML = `<strong>Receiving resolution:</strong> ${remoteVideo.videoWidth} x ${remoteVideo.videoHeight} px`;
}
if (renderstreaming == null || connectionId == null) {
return;
}
// 获取WebRTC统计信息
const stats = await renderstreaming.getStats();
if (stats == null) {
return;
}
// 创建统计信息显示数组
const array = createDisplayStringArray(stats, lastStats);
if (array.length) {
messageDiv.style.display = 'block';
messageDiv.innerHTML = array.join('<br>');
}
lastStats = stats;
}, 1000);
}
/**
* 清除统计信息
*/
function clearStatsMessage() {
if (intervalId) {
clearInterval(intervalId); // 清除定时器
}
lastStats = null;
intervalId = null;
localVideoStatsDiv.innerHTML = '';
remoteVideoStatsDiv.innerHTML = '';
messageDiv.style.display = 'none';
messageDiv.innerHTML = '';
}

View File

@@ -0,0 +1,53 @@
import * as Logger from "../../module/logger.js";
export class SendVideo {
constructor(localVideoElement, remoteVideoElement) {
this.localVideo = localVideoElement;
this.remoteVideo = remoteVideoElement;
}
/**
* @param {MediaTrackConstraints} videoSource
* @param {MediaTrackConstraints} audioSource
* @param {number} videoWidth
* @param {number} videoHeight
*/
async startLocalVideo(videoSource, audioSource, videoWidth, videoHeight) {
try {
const constraints = {
video: { deviceId: videoSource ? { exact: videoSource } : undefined },
audio: { deviceId: audioSource ? { exact: audioSource } : undefined }
};
if (videoWidth != null || videoWidth != 0) {
constraints.video.width = videoWidth;
}
if (videoHeight != null || videoHeight != 0) {
constraints.video.height = videoHeight;
}
const localStream = await navigator.mediaDevices.getUserMedia(constraints);
this.localVideo.srcObject = localStream;
await this.localVideo.play();
} catch (err) {
Logger.error(`mediaDevice.getUserMedia() error:${err}`);
}
}
/**
* @returns {MediaStreamTrack[]}
*/
getLocalTracks() {
return this.localVideo.srcObject.getTracks();
}
/**
* @param {MediaStreamTrack} track
*/
addRemoteTrack(track) {
if (this.remoteVideo.srcObject == null) {
this.remoteVideo.srcObject = new MediaStream();
}
this.remoteVideo.srcObject.addTrack(track);
}
}