2026-03-04 17:55:55 +08:00
/ * *
* 双向视频通话应用主文件
* 负责初始化视频设备 、 建立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" ; // 信令管理
// 默认视频流尺寸
2026-02-27 18:35:40 +08:00
const defaultStreamWidth = 1280 ;
const defaultStreamHeight = 720 ;
2026-03-04 17:55:55 +08:00
// 预定义的视频分辨率列表
2026-02-27 18:35:40 +08:00
const streamSizeList =
[
2026-03-04 17:55:55 +08:00
{ 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
2026-02-27 18:35:40 +08:00
] ;
2026-03-04 17:55:55 +08:00
// 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' ) ; // 自定义高度输入
// 编解码器偏好设置
2026-02-27 18:35:40 +08:00
const codecPreferences = document . getElementById ( 'codecPreferences' ) ;
2026-03-04 17:55:55 +08:00
// 检查浏览器是否支持设置编解码器偏好
2026-02-27 18:35:40 +08:00
const supportsSetCodecPreferences = window . RTCRtpTransceiver &&
'setCodecPreferences' in window . RTCRtpTransceiver . prototype ;
2026-03-04 17:55:55 +08:00
const messageDiv = document . getElementById ( 'message' ) ; // 消息显示区域
messageDiv . style . display = 'none' ; // 初始隐藏消息区域
2026-02-27 18:35:40 +08:00
2026-03-04 17:55:55 +08:00
let useCustomResolution = false ; // 是否使用自定义分辨率
2026-02-27 18:35:40 +08:00
2026-03-04 17:55:55 +08:00
// 初始化输入选择和编解码器选择
2026-02-27 18:35:40 +08:00
setUpInputSelect ( ) ;
showCodecSelect ( ) ;
/** @type {SendVideo} */
2026-03-04 17:55:55 +08:00
let sendVideo = new SendVideo ( localVideo , remoteVideo ) ; // 视频处理实例
2026-02-27 18:35:40 +08:00
/** @type {RenderStreaming} */
2026-03-04 17:55:55 +08:00
let renderstreaming ; // WebRTC连接管理实例
let useWebSocket ; // 是否使用WebSocket信令
let connectionId ; // 连接ID
2026-02-27 18:35:40 +08:00
2026-03-04 17:55:55 +08:00
// 按钮事件绑定
2026-02-27 18:35:40 +08:00
const startButton = document . getElementById ( 'startVideoButton' ) ;
2026-03-04 17:55:55 +08:00
startButton . addEventListener ( 'click' , startVideo ) ; // 启动视频按钮
2026-02-27 18:35:40 +08:00
const setupButton = document . getElementById ( 'setUpButton' ) ;
2026-03-04 17:55:55 +08:00
setupButton . addEventListener ( 'click' , setUp ) ; // 设置连接按钮
2026-02-27 18:35:40 +08:00
const hangUpButton = document . getElementById ( 'hangUpButton' ) ;
2026-03-04 17:55:55 +08:00
hangUpButton . addEventListener ( 'click' , hangUp ) ; // 挂断按钮
2026-02-27 18:35:40 +08:00
2026-03-04 17:55:55 +08:00
// 页面卸载前清理
2026-02-27 18:35:40 +08:00
window . addEventListener ( 'beforeunload' , async ( ) => {
if ( ! renderstreaming )
return ;
2026-03-04 17:55:55 +08:00
await renderstreaming . stop ( ) ; // 停止WebRTC连接
2026-02-27 18:35:40 +08:00
} , true ) ;
2026-03-04 17:55:55 +08:00
// 初始化配置
2026-02-27 18:35:40 +08:00
setupConfig ( ) ;
2026-03-04 17:55:55 +08:00
/ * *
* 初始化服务器配置
* @ async
* @ returns { Promise < void > }
* /
2026-02-27 18:35:40 +08:00
async function setupConfig ( ) {
2026-03-04 17:55:55 +08:00
const res = await getServerConfig ( ) ; // 获取服务器配置
useWebSocket = res . useWebSocket ; // 设置是否使用WebSocket
showWarningIfNeeded ( res . startupMode ) ; // 显示启动模式警告
2026-02-27 18:35:40 +08:00
}
2026-03-04 17:55:55 +08:00
/ * *
* 根据启动模式显示警告信息
* @ param { string } startupMode - 启动模式 , 可能的值包括 "public" 和 "private"
* /
2026-02-27 18:35:40 +08:00
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 ;
}
}
2026-03-04 17:55:55 +08:00
/ * *
* 启动本地视频
* @ async
* @ returns { Promise < void > }
* /
2026-02-27 18:35:40 +08:00
async function startVideo ( ) {
2026-03-04 17:55:55 +08:00
// 禁用相关输入控件
2026-02-27 18:35:40 +08:00
videoSelect . disabled = true ;
audioSelect . disabled = true ;
videoResolutionSelect . disabled = true ;
cameraWidthInput . disabled = true ;
cameraHeightInput . disabled = true ;
startButton . disabled = true ;
let width = 0 ;
let height = 0 ;
2026-03-04 17:55:55 +08:00
// 根据选择的分辨率设置视频尺寸
2026-02-27 18:35:40 +08:00
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 ;
}
2026-03-04 17:55:55 +08:00
// 启动本地视频
2026-02-27 18:35:40 +08:00
await sendVideo . startLocalVideo ( videoSelect . value , audioSelect . value , width , height ) ;
2026-03-04 17:55:55 +08:00
// 启用设置按钮
2026-02-27 18:35:40 +08:00
setupButton . disabled = false ;
}
2026-03-04 17:55:55 +08:00
/ * *
* 设置WebRTC连接
* @ async
* @ returns { Promise < void > }
* /
2026-02-27 18:35:40 +08:00
async function setUp ( ) {
2026-03-04 17:55:55 +08:00
setupButton . disabled = true ; // 禁用设置按钮
hangUpButton . disabled = false ; // 启用挂断按钮
connectionId = textForConnectionId . value ; // 获取连接ID
codecPreferences . disabled = true ; // 禁用编解码器选择
2026-02-27 18:35:40 +08:00
2026-03-04 17:55:55 +08:00
// 创建信令实例
2026-02-27 18:35:40 +08:00
const signaling = useWebSocket ? new WebSocketSignaling ( ) : new Signaling ( ) ;
2026-03-04 17:55:55 +08:00
const config = getRTCConfiguration ( ) ; // 获取RTC配置
renderstreaming = new RenderStreaming ( signaling , config ) ; // 创建WebRTC连接管理实例
// 连接建立回调
2026-02-27 18:35:40 +08:00
renderstreaming . onConnect = ( ) => {
2026-03-04 17:55:55 +08:00
const tracks = sendVideo . getLocalTracks ( ) ; // 获取本地媒体轨道
2026-02-27 18:35:40 +08:00
for ( const track of tracks ) {
2026-03-04 17:55:55 +08:00
renderstreaming . addTransceiver ( track , { direction : 'sendonly' } ) ; // 添加发送轨道
2026-02-27 18:35:40 +08:00
}
2026-03-04 17:55:55 +08:00
setCodecPreferences ( ) ; // 设置编解码器偏好
showStatsMessage ( ) ; // 显示统计信息
2026-02-27 18:35:40 +08:00
} ;
2026-03-04 17:55:55 +08:00
// 连接断开回调
2026-02-27 18:35:40 +08:00
renderstreaming . onDisconnect = ( ) => {
2026-03-04 17:55:55 +08:00
hangUp ( ) ; // 挂断连接
2026-02-27 18:35:40 +08:00
} ;
2026-03-04 17:55:55 +08:00
// 轨道事件回调
2026-02-27 18:35:40 +08:00
renderstreaming . onTrackEvent = ( data ) => {
const direction = data . transceiver . direction ;
if ( direction == "sendrecv" || direction == "recvonly" ) {
2026-03-04 17:55:55 +08:00
sendVideo . addRemoteTrack ( data . track ) ; // 添加远程轨道
2026-02-27 18:35:40 +08:00
}
} ;
2026-03-04 17:55:55 +08:00
// 启动WebRTC连接
2026-02-27 18:35:40 +08:00
await renderstreaming . start ( ) ;
await renderstreaming . createConnection ( connectionId ) ;
}
2026-03-04 17:55:55 +08:00
/ * *
* 设置编解码器偏好
* /
2026-02-27 18:35:40 +08:00
function setCodecPreferences ( ) {
/** @type {RTCRtpCodecCapability[] | null} */
let selectedCodecs = null ;
2026-03-04 17:55:55 +08:00
2026-02-27 18:35:40 +08:00
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 ;
}
2026-03-04 17:55:55 +08:00
// 获取视频收发器并设置编解码器偏好
2026-02-27 18:35:40 +08:00
const transceivers = renderstreaming . getTransceivers ( ) . filter ( t => t . receiver . track . kind == "video" ) ;
if ( transceivers && transceivers . length > 0 ) {
transceivers . forEach ( t => t . setCodecPreferences ( selectedCodecs ) ) ;
}
}
2026-03-04 17:55:55 +08:00
/ * *
* 挂断WebRTC连接
* @ async
* @ returns { Promise < void > }
* /
2026-02-27 18:35:40 +08:00
async function hangUp ( ) {
2026-03-04 17:55:55 +08:00
clearStatsMessage ( ) ; // 清除统计信息
2026-02-27 18:35:40 +08:00
messageDiv . style . display = 'block' ;
messageDiv . innerText = ` Disconnect peer on ${ connectionId } . ` ;
2026-03-04 17:55:55 +08:00
hangUpButton . disabled = true ; // 禁用挂断按钮
setupButton . disabled = false ; // 启用设置按钮
// 删除连接并停止WebRTC
2026-02-27 18:35:40 +08:00
await renderstreaming . deleteConnection ( ) ;
await renderstreaming . stop ( ) ;
renderstreaming = null ;
2026-03-04 17:55:55 +08:00
remoteVideo . srcObject = null ; // 清除远程视频源
2026-02-27 18:35:40 +08:00
2026-03-04 17:55:55 +08:00
textForConnectionId . value = getRandom ( ) ; // 生成新的随机连接ID
2026-02-27 18:35:40 +08:00
connectionId = null ;
2026-03-04 17:55:55 +08:00
// 启用编解码器选择
2026-02-27 18:35:40 +08:00
if ( supportsSetCodecPreferences ) {
codecPreferences . disabled = false ;
}
}
2026-03-04 17:55:55 +08:00
/ * *
* 生成随机连接ID
* @ returns { string } 5 位随机数字字符串
* /
2026-02-27 18:35:40 +08:00
function getRandom ( ) {
const max = 99999 ;
const length = String ( max ) . length ;
const number = Math . floor ( Math . random ( ) * max ) ;
2026-03-04 17:55:55 +08:00
return ( Array ( length ) . join ( '0' ) + number ) . slice ( - length ) ; // 补零确保5位
2026-02-27 18:35:40 +08:00
}
2026-03-04 17:55:55 +08:00
/ * *
* 设置输入选择控件
* @ async
* @ returns { Promise < void > }
* /
2026-02-27 18:35:40 +08:00
async function setUpInputSelect ( ) {
2026-03-04 17:55:55 +08:00
// 获取媒体设备列表
2026-02-27 18:35:40 +08:00
const deviceInfos = await navigator . mediaDevices . enumerateDevices ( ) ;
2026-03-04 17:55:55 +08:00
// 填充视频设备选择
2026-02-27 18:35:40 +08:00
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' ) {
2026-03-04 17:55:55 +08:00
// 填充音频设备选择
2026-02-27 18:35:40 +08:00
const option = document . createElement ( 'option' ) ;
option . value = deviceInfo . deviceId ;
option . text = deviceInfo . label || ` mic ${ audioSelect . length + 1 } ` ;
audioSelect . appendChild ( option ) ;
}
}
2026-03-04 17:55:55 +08:00
// 填充视频分辨率选择
2026-02-27 18:35:40 +08:00
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 ) ;
}
2026-03-04 17:55:55 +08:00
// 添加自定义分辨率选项
2026-02-27 18:35:40 +08:00
const option = document . createElement ( 'option' ) ;
option . value = streamSizeList . length ;
option . text = 'Custom' ;
videoResolutionSelect . appendChild ( option ) ;
2026-03-04 17:55:55 +08:00
videoResolutionSelect . value = 1 ; // 默认选择1280 x 720
2026-02-27 18:35:40 +08:00
2026-03-04 17:55:55 +08:00
// 分辨率选择变化事件
2026-02-27 18:35:40 +08:00
videoResolutionSelect . addEventListener ( 'change' , ( event ) => {
const isCustom = event . target . value >= streamSizeList . length ;
cameraWidthInput . disabled = ! isCustom ;
cameraHeightInput . disabled = ! isCustom ;
useCustomResolution = isCustom ;
} ) ;
}
2026-03-04 17:55:55 +08:00
/ * *
* 显示编解码器选择
* /
2026-02-27 18:35:40 +08:00
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 ;
}
2026-03-04 17:55:55 +08:00
// 获取视频编解码器能力
2026-02-27 18:35:40 +08:00
const codecs = RTCRtpSender . getCapabilities ( 'video' ) . codecs ;
codecs . forEach ( codec => {
2026-03-04 17:55:55 +08:00
// 跳过冗余和FEC编解码器
2026-02-27 18:35:40 +08:00
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 ;
}
2026-03-04 17:55:55 +08:00
// 统计信息相关变量
let lastStats ; // 上次统计信息
let intervalId ; // 统计信息更新间隔ID
2026-02-27 18:35:40 +08:00
2026-03-04 17:55:55 +08:00
/ * *
* 显示统计信息
* /
2026-02-27 18:35:40 +08:00
function showStatsMessage ( ) {
2026-03-04 17:55:55 +08:00
// 每秒更新一次统计信息
2026-02-27 18:35:40 +08:00
intervalId = setInterval ( async ( ) => {
2026-03-04 17:55:55 +08:00
// 显示本地视频分辨率
2026-02-27 18:35:40 +08:00
if ( localVideo . videoWidth ) {
localVideoStatsDiv . innerHTML = ` <strong>Sending resolution:</strong> ${ localVideo . videoWidth } x ${ localVideo . videoHeight } px ` ;
}
2026-03-04 17:55:55 +08:00
// 显示远程视频分辨率
2026-02-27 18:35:40 +08:00
if ( remoteVideo . videoWidth ) {
remoteVideoStatsDiv . innerHTML = ` <strong>Receiving resolution:</strong> ${ remoteVideo . videoWidth } x ${ remoteVideo . videoHeight } px ` ;
}
if ( renderstreaming == null || connectionId == null ) {
return ;
}
2026-03-04 17:55:55 +08:00
// 获取WebRTC统计信息
2026-02-27 18:35:40 +08:00
const stats = await renderstreaming . getStats ( ) ;
if ( stats == null ) {
return ;
}
2026-03-04 17:55:55 +08:00
// 创建统计信息显示数组
2026-02-27 18:35:40 +08:00
const array = createDisplayStringArray ( stats , lastStats ) ;
if ( array . length ) {
messageDiv . style . display = 'block' ;
messageDiv . innerHTML = array . join ( '<br>' ) ;
}
lastStats = stats ;
} , 1000 ) ;
}
2026-03-04 17:55:55 +08:00
/ * *
* 清除统计信息
* /
2026-02-27 18:35:40 +08:00
function clearStatsMessage ( ) {
if ( intervalId ) {
2026-03-04 17:55:55 +08:00
clearInterval ( intervalId ) ; // 清除定时器
2026-02-27 18:35:40 +08:00
}
lastStats = null ;
intervalId = null ;
localVideoStatsDiv . innerHTML = '' ;
remoteVideoStatsDiv . innerHTML = '' ;
messageDiv . style . display = 'none' ;
messageDiv . innerHTML = '' ;
}