/** * 双向视频通话应用主文件 * 负责初始化视频设备、建立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} */ 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 = "

Warning

This sample is not working on Public Mode."; warningDiv.hidden = false; } } /** * 启动本地视频 * @async * @returns {Promise} */ 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} */ 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} */ 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} */ 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 RTCRtpTransceiver.setCodecPreferences.`; 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 = `Sending resolution: ${localVideo.videoWidth} x ${localVideo.videoHeight} px`; } // 显示远程视频分辨率 if (remoteVideo.videoWidth) { remoteVideoStatsDiv.innerHTML = `Receiving resolution: ${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('
'); } lastStats = stats; }, 1000); } /** * 清除统计信息 */ function clearStatsMessage() { if (intervalId) { clearInterval(intervalId); // 清除定时器 } lastStats = null; intervalId = null; localVideoStatsDiv.innerHTML = ''; remoteVideoStatsDiv.innerHTML = ''; messageDiv.style.display = 'none'; messageDiv.innerHTML = ''; }