Web/vanillaVersion.html (800 lines of code) (raw):

<!doctype html> <html> <head> <title>DingRTC WebRTC Demo</title> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, height=device-height, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" /> <!-- cdn jquery版本为1.10.2 --> <script src="https://dingrtc.oss-cn-zhangjiakou.aliyuncs.com/sdk/web/jquery.min.js"></script> <style> body { height: 100%; width: 100%; margin: 0; padding: 0; } ul, li { padding: 0; margin: 0; list-style: none; } .login { min-height: 100vh; position: relative; background-color: #006eff; } .login .main { width: 420px; background-color: #fff; padding: 30px 26px 24px 26px; box-sizing: border-box; position: absolute; top: 40%; left: 50%; -webkit-transform: translate(-50%, -60%); transform: translate(-50%, -50%); font-size: 14px; } .main .main-title { font-size: 30px; text-align: center; color: #333; letter-spacing: 1px; } .main .main-input { margin: 33px 0 13px 0; } .main-input input { padding: 0 14px; width: 100%; box-sizing: border-box; line-height: 38.5px; height: 38.5px; color: #888; border: solid 1px #ddd; position: relative; margin-bottom: 10px; touch-action: none; } .main .main-button button { padding: 0 14px; width: 100%; box-sizing: border-box; line-height: 38.5px; height: 38.5px; background-color: #006eff; border: solid 1px #006eff; color: white; letter-spacing: 1px; } .container-box { margin-left: 200px; margin-right: 300px; position: relative; } .local-video { margin: 0 calc(50 / 1080 * 100vh); position: relative; max-width: 50vw; } .remote-user-list { position: absolute; left: 0; top: 0; width: 200px; height: 100%; background: #333; color: #fff; } .remote-user-list h2 { font-size: 20px; line-height: 36px; font-weight: weight; text-align: center; } .remote-user-list .user-ul { display: block; } .remote-user-list .user-ul .user-ul-li { position: relative; font-size: 20px; height: 40px; line-height: 40px; text-align: center; } .remote-user-list .user-ul .user-ul-li:hover { background: #666; } .remote-user-list .menu { position: absolute; z-index: 100; left: 200px; text-align: left; top: 1px; background-color: #333333; border-radius: 8px; } .remote-user-list .menu li { padding: 0 10px; min-width: 165px; height: 40px; line-height: 40px; color: #ddd; text-indent: 10px; cursor: pointer; } .remote-user-list .menu li span { cursor: pointer; } .remote-user-list .menu li span:hover { color: #38f; } .local-display-name { height: 50px; background: #333; width: 100%; /* position: absolute; */ color: #fff; font-weight: normal; } .local-display-name span { margin: 0 30px; line-height: 50px; font-size: 15px; } .publisher { margin: 20px 0 20px calc(50 / 1080 * 100vh); } .publisher input[type='text'] { border: 2px solid #456879; border-radius: 10px; height: 40px; width: 95px; } .publisher button { font-family: arial; color: #ffffff !important; font-size: 12px; width: 68px; height: 26px; background: deepskyblue; border: none; outline: none; } .publisher button:hover { background: #006eff; cursor: pointer; } .streamType label { font-family: arial; font-size: 16px; } .deviceState { margin-left: 20px; } .deviceState label { font-family: arial; font-size: 16px; } .video-container { position: absolute; top: 0px; right: 0px; bottom: 0px; width: 260px; background-color: #eee; color: #fff; padding: 20px; overflow-y: scroll; } .video-container::-webkit-scrollbar { display: none; } .video-container .remote-subscriber { margin: 10px 0; position: relative; } .video-container video { background: #000; width: 100%; height: 195px; } .video-container .display-name { position: absolute; top: 35px; left: 30px; padding: 5px; background: rgba(0, 0, 0, 0.1); color: white; } .alert { display: none; position: fixed; top: 8%; left: 50%; min-width: 300px; max-width: 600px; transform: translateX(-50%); z-index: 99999; text-align: center; padding: 10px 15px; border-radius: 3px; } .alert-success { color: #3c763d; background-color: #dff0d8; border-color: #d6e9c6; } .alert-info { color: #31708f; background-color: #d9edf7; border-color: #bce8f1; } .alert-warning { color: #8a6d3b; background-color: #fcf8e3; border-color: #faebcc; } .alert-danger { color: #a94442; background-color: #f2dede; border-color: #ebccd1; } </style> </head> <body> <div class="login"> <div class="main"> <div class="main-title"> <span>音视频通信</span> </div> <div class="main-input"> <input type="text" placeholder="请输入房间号" /> </div> <div class="main-button"> <button>进入房间</button> </div> </div> </div> <div class="main-web" style="display: none"> <div class="remote-user-list"> <h2>房间成员</h2> <ul class="user-ul"></ul> </div> <div class="container-box"> <div class="local-display-name"> <span class="username">User: <b></b></span> <span class="channelid">Channel Id: <b></b></span> <span class="streamstate">推流状态:<b></b></span> <button class="leave-room">离开房间</button> </div> <div class="publisher"> <button class="select-preview">关闭预览</button> <span class="streamType" style="display: none"> <label for="cameraPublish">推视频流</label> <input id="cameraPublish" value="cameraPublish" type="checkbox" name="streamType" /> <label for="screenPublish">推共享流</label> <input id="screenPublish" value="screenPublish" type="checkbox" name="streamType" /> <label for="audioPublish">推音频流</label> <input id="audioPublish" value="audioPublish" type="checkbox" name="streamType" /> </span> <span class="deviceState" style="display: none"> <label for="cameraOpen">开摄像头</label> <input id="cameraOpen" value="cameraOpen" type="checkbox" name="deviceState" /> <label for="micOpen">开麦克风</label> <input id="micOpen" value="micOpen" type="checkbox" name="deviceState" /> </span> </div> <div class="local-video"></div> </div> <div class="video-container"></div> </div> <div class="alert"></div> </body> </html> <!-- 请使用官网最新sdk在线地址,或者从官网下载最新sdk文件,添加到同层目录来本地引用 --> <script src="https://dingrtc.oss-cn-zhangjiakou.aliyuncs.com/sdk/web/index.umd.3.6.0.js"></script> <script> /** * 请找到 generateAuthInfo方法,配置业务服务请求域名获取token,appId,userId等 */ var channelId; var userName; var subscribedMap = {}; var RtcEngine = DingRTC.default; var client; var cameraTrack = null; var micTrack = null; var screenTrack = null; try { client = RtcEngine.createClient(); } catch (error) { console.log(error); alert('请使用官网最新Web SDK文件'); } if (!RtcEngine.checkSystemRequirements()) { alert('当前环境不支持RTC通信,请检查URL协议是否为https或使用最新版Chrome浏览器'); } RtcEngine.createMicrophoneAndCameraTracks({ dimension: 'VD_640x480', frameRate: 15 }, {}).then( ([videoTrack, audioTrack]) => { cameraTrack = videoTrack; micTrack = audioTrack; }, ); function request(api, params) { var res; // const APP_SERVER_DOMAIN = 'http://localhost:3001'; 请修改为真实使用的业务服务器域名 const url = `${APP_SERVER_DOMAIN}${api}?${Object.keys(params) .map((key) => `${key}=${params[key]}`) .join('&')}`; return new Promise((resolve, reject) => { fetch(url) .then((response) => { res = response; if (response.ok) { return response.json(); } else { return response.text(); } }) .then((data) => { if (res?.ok) { if (data.code && data.code !== 200) { reject(data); } else { resolve(data.data); } } else { reject(data); } }) .catch(reject); }); } // 从业务服务器获取生成的token function getAppToken(userId, appId, channelId) { // 请修改为真实使用的参数 const loginParam = { }; return request('login', loginParam); } /** * 显示提示信息,并打印 * @param {String} text 要显示的信息 * @param {String} type 信息类型(默认成功) * @param {Number} delay 延迟时间(默认2s) */ function showAlert(text, type, delay) { if (!text) return; var _type = type ? 'alert-' + type : 'alert-success'; var _delay = delay || 2000; $('.alert') .html(text) .addClass(_type) .show() .delay(_delay) .fadeOut('normal', () => { $('.alert').removeClass(_type); }); if (_type === 'warning') { console.warn(text); } else if (_type === 'danger') { console.error(text); } else { console.log(text); } } function initEvents() { /** * remote用户加入房间 user-joined * 更新在线用户列表 */ client.on('user-joined', (user) => { updateUserList(); showAlert(user.userName + '加入房间', 'success'); }); // 设备状态变化 client.on('user-info-updated', (uid, msg) => { showAlert(`uid: ${uid}: 设备状态变化 ${msg} `); }); /** * 远端用户推流推发布事件 user-published,仅处理视频流发布 * 音频合流订阅在入会时进行订阅后无须再处理个人的音频流发布 */ client.on('user-published', (user, mediaType, auxiliary) => { showAlert(`${user.userName} 发布 ${auxiliary ? 'screen' : mediaType}`, 'success'); if (mediaType === 'video') { client.subscribe(user.userId, 'video', auxiliary).then((track) => { subscribedMap[user.userId] = Object.assign(subscribedMap[user.userId] || {}, { [auxiliary ? 'screen' : 'camera']: true, }); const dom = getDisplayRemoteContainer(user.userId, auxiliary ? 'screen' : 'camera'); track.play(dom); }); } }); /** * 远端用户推流结束事件 user-unpublished,仅处理视频流发布 * 远端用户取消音频推流时会自动从房间音频合流内移除 * 视频推流列表删除该用户对应类型视频 * 移除用户视图 * 初始化订阅状态 */ client.on('user-unpublished', (user, mediaType, auxiliary) => { showAlert(`${user.userName} 取消发布 ${auxiliary ? 'screen' : mediaType}`, 'success'); if (mediaType === 'video') { removeDom(user.userId, auxiliary ? 'screen' : 'camera'); delete subscribedMap[user.userId][auxiliary ? 'screen' : 'camera']; } }); /** * 链接状态变化时回调,disconnected 时代表离会 */ client.on('connection-state-changed', (current, prev, reason) => { console.log(`connection-state-change ${current} ${reason || ''}`); if (current === 'disconnected') { if (reason !== 'leave') { showAlert(msg, 'danger'); clearRoom(); } } }); /** * 检测到用户离开频道 * 更新用户列表 * 移除用户视图 */ client.on('user-left', (user) => { delete subscribedMap[userId]; updateUserList(); removeDom(user.userId); showAlert(user.userName + '离开房间', 'success'); }); } /** * 加入房间 * 触发:输入房间号、单击加入房间按钮 * 获取鉴权信息 * 更新页面信息 * 默认开启预览 * 加入房间 * 添加事件监听 * 本地默认自动推视频流(视频流 + 音频流) * 发布本地流 * 订阅已发布到会中的视频流和 房间音频合流 */ async function joinRoom() { //1. 获取频道鉴权令牌参数 为了防止被盗用建议该方法在服务端获取 var authInfo = await generateAuthInfo(channelId); $('.local-display-name .username b').text(userName); $('.local-display-name .channelid b').text(channelId); $('.local-display-name .streamstate b').text('当前未推流'); //2.本地摄像头预览 cameraTrack.play($('.local-video')[0]); //3. 加入房间 默认推音频视频流 client .join(authInfo) .then( () => { updateUserList(); showAlert('加入房间成功', 'success'); initEvents(); }, (error) => { showAlert('[加入房间失败]' + error.message, 'danger'); throw error; }, ) .then(() => { // 4. 发布本地流 client.publish([cameraTrack, micTrack]).then( (res) => { getPublishState('success'); $('#cameraOpen').attr('checked', !cameraTrack?.muted); $('#micOpen').attr('checked', !micTrack?.muted); $('.streamType').show(); $('.deviceState').show(); }, (error) => { getPublishState('danger'); showAlert('[推流失败]' + error.message, 'danger'); }, ); // 5.订阅已发布到房间的远端流, 其中mcu 代表 房间音频合流,订阅mcu音频流后无须再单独订阅个人音频 const subParams = [{ uid: 'mcu', mediaType: 'audio', auxiliary: false }]; for (const user of client.remoteUsers) { if (user.hasAuxiliary) { subParams.push({ uid: user.userId, mediaType: 'video', auxiliary: true }); } if (user.hasVideo) { subParams.push({ uid: user.userId, mediaType: 'video', auxiliary: false }); } } const subTask = client.batchSubscribe(subParams).then((batchSubscribeResult) => { for (const { error, track, uid, auxiliary } of batchSubscribeResult) { if (error) { showAlert( `[订阅${uid} ${auxiliary ? 'screenShare' : 'camera'} 失败]` + error.message, 'danger', ); continue; } if (track.trackMediaType === 'audio') { const audioTrack = track; audioTrack.play(); } else { subscribedMap[uid] = Object.assign(subscribedMap[uid] || {}, { [auxiliary ? 'screen' : 'camera']: true, }); const dom = getDisplayRemoteContainer(uid, auxiliary ? 'screen' : 'camera'); track.play(dom); } } }); }); } /** * 更新远端在线用户列表 */ function updateUserList() { $('.user-ul').empty(); let userList = client.remoteUsers; let frg = document.createDocumentFragment(); userList.map((user) => { let html = $("<li class='user-ul-li'>" + user.userName + "<ul class='menu'></ul></li>"); $(html) .bind('mouseover', user.userId, showUserMenu) .bind('mouseleave', () => { $(event.currentTarget).find('.menu').hide(); }); frg.append(html[0]); }); $('.user-ul').append($(frg)); } /* * 获取测试token * @param {*} channelId 频道号 * @return {object} authinfo */ async function generateAuthInfo(channelId) { var appId = 'xxxxxx'; // 修改为自己的appid 该方案仅为开发测试使用,正式上线需要使用服务端的AppServer userId = 'xxxxx4xxxyxxx'.replace(/[xy]/g, (c) => { var r = (Math.random() * 16) | 0, v = c == 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); // 可以自定义 userName = 'web_' + userId.substr(0, 4); const { token } = await getAppToken(userId, appId, channelId); return { appId, uid: userId, token, userName, channel: channelId, }; } /** * 获取当前远端用户的视频流菜单 */ function showUserMenu(evt) { let userId = evt.data; if (!$(event.target).eq(0).hasClass('user-ul-li')) { return; } $('.menu').hide(); $(event.target).find('.menu').empty().show(); let userInfo = client.remoteUsers.find((item) => item.userId === userId); var html = ''; if (!userInfo.hasAuxiliary && !userInfo.hasVideo) { html = $('<li>该用户未推流</li>'); $(event.target).find('.menu').append(html[0]); } else { var frg = document.createDocumentFragment(); if (userInfo.hasVideo) { let subState = subscribedMap[userId].camera ? '取消订阅' : '订阅'; html = $('<li>' + '视频流' + '&nbsp;<span>' + subState + '</span></li>'); $(html).find('span').off('click').on('click', { userId, label: 'camera' }, toggleSub); frg.append(html[0]); } if (userInfo.hasAuxiliary) { let subState = subscribedMap[userId].screen ? '取消订阅' : '订阅'; html = $('<li>' + '共享流' + '&nbsp;<span>' + subState + '</span></li>'); $(html).find('span').off('click').on('click', { userId, label: 'screen' }, toggleSub); frg.append(html[0]); } $(event.target).find('.menu').append($(frg)); } } /** * 订阅&取消订阅 */ function toggleSub(evt) { var v = evt.data; if (subscribedMap[v.userId][v.label]) { client.unsubscribe(v.userId, 'video', v.label === 'screen').then(() => { removeDom(v.userId, v.label); subscribedMap[v.userId][v.label] = false; console.log('取消订阅'); }); } else { client.subscribe(v.userId, 'video', v.label === 'screen').then((track) => { const dom = getDisplayRemoteContainer(v.userId, v.label); subscribedMap[v.userId][v.label] = true; track.play(dom); console.log('订阅成功'); }); } $('.menu').hide(); } /** * 创建获取订阅的远端视频流的容器标签 */ function getDisplayRemoteContainer(userId, label) { var label = label === 'camera' ? 'camera' : 'screen'; var id = userId + '_' + label; var videoWrapper = $('#' + id); var userInfo = client.remoteUsers.filter((item) => item.userId === userId); if (videoWrapper.length == 0) { videoWrapper = $( "<div class='remote-subscriber' id=" + id + "><div class='display-name'></div></div>", ); $('.video-container').append(videoWrapper); } const displayName = userInfo[0].userName + '_' + label; videoWrapper.find('.display-name').text(displayName); return videoWrapper[0]; } /** * 移除视频容器dom */ function removeDom(userId, label) { if (userId) { if (!label) { $('#' + userId + '_camera').remove(); $('#' + userId + '_screen').remove(); } else { label = label === 'camera' ? 'camera' : 'screen'; $('#' + userId + '_' + label).remove(); } } } /** * 根据推流状态设置当前推流UI */ var getPublishState = (type) => { var streamstate = $('.streamstate b').text(); const isCameraPublished = client.localTracks.includes(cameraTrack); const isScreenPublished = client.localTracks.includes(screenTrack); const isMicPublished = client.localTracks.includes(micTrack); const streamStateBoxes = $('.publisher .streamType input'); const streamStates = []; if (isCameraPublished) { streamStates.push('视频流'); } $('#cameraPublish').attr('checked', isCameraPublished); if (isScreenPublished) { streamStates.push('共享流'); } $('#screenPublish').attr('checked', isScreenPublished); if (isMicPublished) { streamStates.push('音频流'); } $('#audioPublish').attr('checked', isMicPublished); const streamState = streamStates.join('+') || '当前未推流'; showAlert('推流状态:' + streamState, type); $('.streamstate b').text(streamState); $('.streamType').show(); }; /** * 进入房间 */ $('.main-button button').click(() => { var value = $('.main-input input').val(); if (!value) { showAlert('请输入房间号', 'danger'); return; } channelId = value; joinRoom(); $('.login').hide(); $('.main-web').show(); }); /** * 控制预览 */ $('.publisher .select-preview').click(function (e) { var localVideo = $('.local-video'); if ($(this).text() === '开启预览') { $(this).text('处理中...'); cameraTrack.play(localVideo[0]); $(this).text('关闭预览'); } else if ($(this).text() === '关闭预览') { $(this).text('处理中...'); cameraTrack.stop(); $(this).text('开启预览'); } }); /** * 清理本地事件 */ function clearRoom() { client.leave(); client.removeAllListeners(); subscribedMap = {}; $('.user-ul').empty(); $('.video-container').empty(); $('.main-web').hide(); $('.login').show(); } // 离开房间 $('.leave-room').click(clearRoom); /** * 控制推流选项 */ $('.publisher .streamType input').click(async function (e) { var config = $(this).val(); var isChecked = $(this).prop('checked'); var track; if (config === 'cameraPublish') { track = cameraTrack; } else if (config === 'screenPublish') { if (!screenTrack) { try { const [videoTrack] = await RtcEngine.createScreenVideoTrack({ dimension: 'VD_1280x720', frameRate: 15, }); screenTrack = videoTrack; // 处理用户停止共享视频 screenTrack.on('track-ended', () => { client.unpublish(screenTrack); screenTrack = null; getPublishState('success'); }); } catch (e) { throw e; } } track = screenTrack; } else if (config === 'audioPublish') { track = micTrack; } if (isChecked) { $('.streamType').hide(); client .publish(track) .then(() => { getPublishState('success'); }) .catch((error) => { getPublishState('danger'); }); } else { client.unpublish(track).then(() => { getPublishState('success'); }); } }); /** * 控制设备静音选项 */ $('.publisher .deviceState input').click(async function (e) { var config = $(this).val(); var isChecked = $(this).prop('checked'); var track; if (config === 'cameraOpen') { track = cameraTrack; } else if (config === 'micOpen') { track = micTrack; } await track.setMuted(!track.muted); }); /** * 页面刷新时调用离会 */ window.onbeforeunload = function (e) { clent.leave(); }; </script>