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>' + '视频流' + ' <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>' + '共享流' + ' <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>