[*] 优化logger

This commit is contained in:
acgist
2023-02-04 18:00:27 +08:00
parent e477378d43
commit 96c949513c
46 changed files with 150 additions and 1082 deletions

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,39 @@
@charset "UTF-8";
/**文本选择*/
::selection{background:#222;color:#fff;}
/**字体大小*/
@media screen and (min-width:800px){html{font-size:16px;}}
@media screen and (min-width:1200px){html{font-size:18px;}}
@media screen and (min-width:1600px){html{font-size:20px;}}
/**默认样式*/
*{margin:0;padding:0;border:none;outline:none;box-sizing:content-box;}
html{background:#EBEBEB;}
html,body{font-family:Arial,Consolas,SimSun,"宋体";color:#222;font-weight:normal;}
body{width:100%;height:100%;-webkit-tap-highlight-color:rgba(0,0,0,0);font-size:1rem;line-height:1.4em;}
a{color:#1155AA;text-decoration:none;}
a:link{text-decoration:none;}
a:hover{color:#4477EE;text-decoration:none;}
a:visited{text-decoration:none;}
img{border:0;}
ol,ul,li{list-style:none;}
input[type=text],textarea{box-shadow:0px 0px 3px 0px rgba(0,0,0,0.1) inset;border:1px solid rgba(0,0,0,0.1)!important;}
input[type=text]:focus,textarea:focus,input[type=text]:hover,textarea:hover{border:1px solid #1155AA!important;}
input::-webkit-calendar-picker-indicator{color:#1155AA;background:none;}
/**容器*/
.taoyao{text-align:center;}
/**直播*/
.taoyao .live > .video{width:100%;height:100%;}
.taoyao .live .handler{position:fixed;width:100%;bottom:2rem;font-size:2rem;}
/**会议*/
.taoyao .handler a{cursor:pointer;}
.taoyao > .handler{font-size:2rem;padding:1rem 0;width:100%;}
.taoyao .list{width:90vw;margin:auto;}
.taoyao .meeting{float:left;overflow:hidden;position:relative;width:calc(25% - 2rem);border:1rem solid #fff;}
.taoyao .me,.taoyao .meeting:hover{border-color:#060;}
.taoyao .meeting > .video{height:15vw;}
.taoyao .meeting > .video video{width:100%;height:100%;}
.taoyao .meeting .handler{position:absolute;bottom:0rem;text-align:center;width:100%;background:rgba(0,0,0,0.2);padding:0.2rem 0;}
.taoyao .meeting .handler a{color:#fff;}
.taoyao .meeting .handler a:hover{color:#060!important;}
.taoyao .meeting .handler a.expel:hover{color:#C00!important;}
.taoyao .meeting .handler a.record.active{color:#C00;}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 297 KiB

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>桃夭</title>
<link rel="stylesheet" type="text/css" href="./css/style.css" />
<script type="text/javascript" src="./javascript/taoyao.js"></script>
<style type="text/css">
a{width:50%;height:100%;position:fixed;text-align:center;line-height:100%;font-size:4rem;display:flex;align-items:center;justify-content:center;}
a:last-child{left:50%;}
a:hover{color:#fff;background:#060;}
</style>
</head>
<body>
<a href="./live.html">直播</a>
<a href="./meeting.html">会议</a>
</body>
</html>

View File

@@ -0,0 +1,835 @@
/**
* 桃夭WebRTC终端核心功能
*
* 代码注意:
* 1. undefined判断使用两个等号其他情况使用三个。
*/
/** 兼容 */
const RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate || window.webkitRTCIceCandidate;
const RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
const RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription || window.webkitRTCSessionDescription;
/** 默认音频配置 */
const defaultAudioConfig = {
// 设备
// deviceId : '',
// 音量0~1
volume: 0.5,
// 延迟大小单位毫秒500毫秒以内较好
latency: 0.4,
// 采样数16
sampleSize: 16,
// 采样率8000|16000|32000|48000
sampleRate: 32000,
// 声道数量1|2
channelCount : 1,
// 是否开启自动增益true|false
autoGainControl: false,
// 是否开启降噪功能true|false
noiseSuppression: true,
// 是否开启回音消除true|false
echoCancellation: true,
// 消除回音方式system|browser
echoCancellationType: 'system'
};
/** 默认视频配置 */
const defaultVideoConfig = {
// 设备
// deviceId: '',
// 宽度
width: 1280,
// 高度
height: 720,
// 帧率
frameRate: 24,
// 选摄像头user|left|right|environment
facingMode: 'environment'
}
/** 默认RTCPeerConnection配置 */
const defaultRPCConfig = {
// ICE代理的服务器
iceServers: null,
// 传输通道绑定策略balanced|max-compat|max-bundle
bundlePolicy: 'balanced',
// RTCP多路复用策略require|negotiate
rtcpMuxPolicy: 'require',
// ICE传输策略all|relay
iceTransportPolicy: 'all'
// ICE候选个数
// iceCandidatePoolSize: 8
}
/** 信令配置 */
const signalConfig = {
/** 当前终端SN */
sn: 'taoyao',
/** 当前版本 */
version: '1.0.0',
// 信令授权
username: 'taoyao',
password: 'taoyao'
};
/** 信令协议 */
const signalProtocol = {
/** 直播信令 */
live: {
},
/** 媒体信令 */
media: {
/** 发布 */
publish: 5000,
/** 订阅 */
subscribe: 5002,
/** 候选 */
offer: 5997,
/** Answer */
answer: 5998,
/** 候选 */
candidate: 5999
},
/** 终端信令 */
client: {
/** 注册 */
register: 2000,
/** 下发配置 */
config: 2004,
/** 心跳 */
heartbeat: 2005,
/** 重启终端 */
reboot: 2997,
},
/** 会议信令 */
meeting: {
/** 创建会议信令 */
create: 4000,
/** 进入会议信令 */
enter: 4002,
},
/** 平台信令 */
platform: {
/** 异常 */
error: 1999
},
/** 当前索引 */
index: 100000,
/** 最小索引 */
minIndex: 100000,
/** 最大索引 */
maxIndex: 999999,
/** 生成索引 */
buildId: function() {
if(this.index++ >= this.maxIndex) {
this.index = this.minIndex;
}
return Date.now() + '' + this.index;
},
/** 生成信令消息 */
buildProtocol: function(pid, body, id) {
let message = {
header: {
v: signalConfig.version,
id: id || this.buildId(),
sn: signalConfig.sn,
pid: pid,
},
'body': body
};
return message;
}
};
/** 信令通道 */
const signalChannel = {
/** 桃夭 */
taoyao: null,
/** 通道 */
channel: null,
/** 地址 */
address: null,
/** 回调 */
callback: null,
/** 回调事件 */
callbackMapping: new Map(),
/** 心跳时间 */
heartbeatTime: 30 * 1000,
/** 心跳定时器 */
heartbeatTimer: null,
/** 重连定时器 */
reconnectTimer: null,
/** 防止重复重连 */
lockReconnect: false,
/** 当前重连时间 */
connectionTimeout: 5 * 1000,
/** 最小重连时间 */
minReconnectionDelay: 5 * 1000,
/** 最大重连时间 */
maxReconnectionDelay: 60 * 1000,
/** 重连失败时间增长倍数 */
reconnectionDelayGrowFactor: 2,
/** 心跳 */
heartbeat: function() {
let self = this;
if(self.heartbeatTimer) {
clearTimeout(self.heartbeatTimer);
}
self.heartbeatTimer = setTimeout(function() {
// 电池navigator.getBattery()
if (self.channel && self.channel.readyState === WebSocket.OPEN) {
self.push(signalProtocol.buildProtocol(
signalProtocol.client.heartbeat,
{
signal: 100,
battery: 100
}
));
self.heartbeat();
} else {
console.warn('发送心跳失败', self.channel);
}
}, self.heartbeatTime);
},
/** 连接 */
connect: function(address, callback, reconnection = true) {
let self = this;
self.address = address;
self.callback = callback;
return new Promise((resolve, reject) => {
console.debug('连接信令通道', address);
self.channel = new WebSocket(address);
self.channel.onopen = function(e) {
console.debug('打开信令通道', e);
// 注册终端
self.push(signalProtocol.buildProtocol(
signalProtocol.client.register,
{
ip: null,
mac: null,
signal: 100,
battery: 100,
username: signalConfig.username,
password: signalConfig.password
}
));
// 重置时间
self.connectionTimeout = self.minReconnectionDelay
// 开始心跳
self.heartbeat();
// 成功回调
resolve(e);
};
self.channel.onclose = function(e) {
console.error('信令通道关闭', self.channel, e);
if(reconnection) {
self.reconnect();
}
reject(e);
};
self.channel.onerror = function(e) {
console.error('信令通道异常', self.channel, e);
if(reconnection) {
self.reconnect();
}
reject(e);
};
/**
* 回调策略:
* 1. 如果注册请求回调同时执行结果返回true不再执行后面所有回调。
* 2. 如果注册全局回调同时执行结果返回true不再执行后面所有回调。
* 3. 如果前面所有回调没有返回true执行默认回调。
*/
self.channel.onmessage = function(e) {
console.debug('信令通道消息', e.data);
let done = false;
let data = JSON.parse(e.data);
// 请求回调
if(self.callbackMapping.has(data.header.id)) {
try {
done = self.callbackMapping.get(data.header.id)(data);
} finally {
self.callbackMapping.delete(data.header.id);
}
}
// 全局回调
if(self.callback) {
done = self.callback(data);
}
// 默认回调
if(!done) {
self.defaultCallback(data);
}
};
});
},
/** 重连 */
reconnect: function() {
let self = this;
if (self.lockReconnect) {
return;
}
self.lockReconnect = true;
// 关闭旧的通道
if(self.channel && self.channel.readyState === WebSocket.OPEN) {
self.channel.close();
self.channel = null;
}
if(self.reconnectTimer) {
clearTimeout(self.reconnectTimer);
}
// 打开定时重连
self.reconnectTimer = setTimeout(function() {
console.info('信令通道重连', self.address, new Date());
self.connect(self.address, self.callback, true);
self.lockReconnect = false;
}, self.connectionTimeout);
if (self.connectionTimeout >= self.maxReconnectionDelay) {
self.connectionTimeout = self.maxReconnectionDelay;
} else {
self.connectionTimeout = self.connectionTimeout * self.reconnectionDelayGrowFactor
}
},
/** 发送消息 */
push: function(data, callback) {
// 注册回调
if(data && callback) {
this.callbackMapping.set(data.header.id, callback);
}
// 发送消息
if(data && data.header) {
this.channel.send(JSON.stringify(data));
} else {
this.channel.send(data);
}
},
/** 关闭通道 */
close: function() {
clearTimeout(this.heartbeatTimer);
},
/** 默认回调 */
defaultCallback: function(data) {
console.debug('没有适配信令消息默认处理', data);
switch(data.header.pid) {
case signalProtocol.media.publish:
this.defaultMediaPublish(data);
break;
case signalProtocol.media.subscribe:
this.defaultMediaSubscribe(data);
break;
case signalProtocol.media.offer:
this.defaultMediaOffer(data);
break;
case signalProtocol.media.answer:
this.defaultMediaAnswer(data);
break;
case signalProtocol.media.candidate:
this.defaultMediaCandidate(data);
break;
case signalProtocol.client.register:
break;
case signalProtocol.client.config:
this.defaultClientConfig(data);
break;
case signalProtocol.client.heartbeat:
break;
case signalProtocol.client.reboot:
this.defaultClientReboot(data);
break;
case signalProtocol.meeting.create:
break;
case signalProtocol.meeting.enter:
this.defaultMeetingEnter(data);
break;
case signalProtocol.platform.error:
console.error('信令发生错误', data);
break;
}
},
/** 终端默认回调 */
defaultClientConfig: function(data) {
let self = this;
// 配置终端
self.taoyao
.configMedia(data.body.media.audio, data.body.media.video)
.configWebrtc(data.body.webrtc);
// 打开媒体通道
let videoId = self.taoyao.videoId;
if(videoId) {
self.taoyao.buildLocalMedia()
.then(stream => {
self.taoyao.buildMediaChannel(videoId, stream);
})
.catch(e => console.error('打开终端媒体失败', e));
console.debug('自动打开媒体通道', videoId);
} else {
console.debug('没有配置本地媒体信息跳过自动打开媒体通道');
}
},
defaultClientReboot: function(data) {
console.info('重启终端');
location.reload();
},
/** 默认媒体回调 */
defaultMediaPublish: function(data) {
},
defaultMediaSubscribe: function(data) {
let self = this;
const from = data.body.from;
const remote = this.taoyao.remoteClientFilter(from, true);
remote.localMediaChannel.createOffer().then(description => {
console.debug('Local Create Offer', description);
remote.localMediaChannel.setLocalDescription(description);
self.push(signalProtocol.buildProtocol(
signalProtocol.media.offer,
{
to: from,
sdp: {
sdp: description.sdp,
type: description.type
}
}
));
});
},
defaultMediaOffer: function(data) {
let self = this;
const from = data.body.from;
const remote = this.taoyao.remoteClientFilter(from, true);
remote.remoteMediaChannel.setRemoteDescription(new RTCSessionDescription(data.body.sdp));
remote.remoteMediaChannel.createAnswer().then(description => {
console.debug('Remote Create Answer', description);
remote.remoteMediaChannel.setLocalDescription(description);
self.push(signalProtocol.buildProtocol(
signalProtocol.media.answer,
{
to: from,
sdp: {
sdp: description.sdp,
type: description.type
}
}
));
});
},
defaultMediaAnswer: function(data) {
const from = data.body.from;
const remote = this.taoyao.remoteClientFilter(from, true);
remote.localMediaChannel.setRemoteDescription(new RTCSessionDescription(data.body.sdp));
},
defaultMediaCandidate: function(data) {
if(!this.taoyao.checkCandidate(data.body.candidate)) {
console.debug('候选缺失要素', data);
return;
}
console.debug('Set ICE Candidate', data.body);
const from = data.body.from;
const remote = this.taoyao.remoteClientFilter(from, true);
if(data.body.type === 'local') {
remote.remoteMediaChannel.addIceCandidate(new RTCIceCandidate(data.body.candidate));
} else if(data.body.type === 'remote'){
remote.localMediaChannel.addIceCandidate(new RTCIceCandidate(data.body.candidate));
} else if(data.body.type === 'mesh') {
remote.localMediaChannel.addIceCandidate(new RTCIceCandidate(data.body.candidate));
// remote.remoteMediaChannel.addIceCandidate(new RTCIceCandidate(data.body.candidate));
} else {
console.warn('不支持的候选类型', data.body.type);
}
},
/** 会议默认回调 */
defaultMeetingEnter: function(data) {
this.taoyao.mediaSubscribe(data.body.sn);
}
};
/** 终端 */
function TaoyaoClient(
taoyao,
sn,
shareMediaChannel,
audioEnabled,
videoEnabled
) {
/** 桃夭 */
this.taoyao = taoyao;
/** 终端标识 */
this.sn = sn;
/** 视频对象 */
this.video = null;
/** 媒体信息 */
this.stream = null;
this.audioTrack = [];
this.videoTrack = [];
/** 媒体状态:是否含有 */
this.audioStatus = false;
this.videoStatus = false;
this.recordStatus = false;
/** 本地媒体通道 */
this.localMediaChannel = null;
/** 远程媒体通道 */
this.remoteMediaChannel = null;
/** 是否共享媒体通道 */
this.shareMediaChannel = shareMediaChannel;
/** 媒体状态:是否播放 */
this.audioEnabled = audioEnabled == undefined ? true : audioEnabled;
this.videoEnabled = videoEnabled == undefined ? true : videoEnabled;
/** 重置 */
this.reset = function() {
}
/** 播放视频 */
this.play = async function() {
await this.video.play();
return this;
};
/** 重新加载 */
this.load = async function() {
await this.video.load();
return this;
}
/** 暂停视频 */
this.pause = async function() {
await this.video.pause();
return this;
};
/** 关闭视频 */
this.close = async function() {
await this.video.close();
// TODO释放连接
return this;
};
/** 设置媒体 */
// TODOstream判断
this.buildStream = async function(videoId, stream, track) {
if(!this.video && videoId) {
this.video = document.getElementById(videoId);
}
if(!this.video) {
throw new Error('视频对象无效:' + videoId);
}
if(!this.stream) {
this.stream = new MediaStream();
this.video.srcObject = this.stream;
}
if(track) {
if(track.kind === 'audio') {
this.buildAudioTrack(track);
}
if(track.kind === 'video') {
this.buildVideoTrack(track);
}
} else if(stream) {
let audioTrack = stream.getAudioTracks();
let videoTrack = stream.getVideoTracks();
// TODO验证API试试修改媒体
// audioTrack.getSettings
// audioTrack.getCapabilities
// audioTrack.applyCapabilities
if(audioTrack && audioTrack.length) {
audioTrack.forEach(v => this.buildAudioTrack(v));
}
if(videoTrack && videoTrack.length) {
videoTrack.forEach(v => this.buildVideoTrack(v));
}
} else {
throw new Error('无效媒体信息');
}
console.debug('设置媒体', this.video, this.stream, this.audioTrack, this.videoTrack);
await this.load();
await this.play();
return this;
};
/** 设置音频流 */
this.buildAudioTrack = function(track) {
// 关闭旧的
// 创建新的
track.sn = this.sn;
this.audioStatus = true;
this.audioTrack.push(track);
if(this.audioEnabled) {
this.stream.addTrack(track);
}
};
/** 设置视频流 */
this.buildVideoTrack = function(track) {
// 关闭旧的
// 创建新的
track.sn = this.sn;
this.videoStatus = true;
this.videoTrack.push(track);
if(this.videoEnabled) {
this.stream.addTrack(track);
}
};
/** 打开媒体通道 */
this.openMediaChannel = function() {
if(this.shareMediaChannel) {
this.localMediaChannel = this.taoyao.localMediaChannel;
this.remoteMediaChannel = this.taoyao.remoteMediaChannel;
} else {
let self = this;
// 本地通道
let mediaChannel = new RTCPeerConnection(defaultRPCConfig);
self.taoyao.localClient.audioTrack.forEach(v => mediaChannel.addTrack(v, self.taoyao.localClient.stream));
self.taoyao.localClient.videoTrack.forEach(v => mediaChannel.addTrack(v, self.taoyao.localClient.stream));
mediaChannel.ontrack = function(e) {
console.debug('Mesh Media Track', self.sn, e);
let remote = self.taoyao.remoteClientFilter(self.sn);
// TODO判断数量
remote.buildStream(remote.sn, e.streams[0], e.track);
};
mediaChannel.onicecandidate = function(e) {
// TODO判断给谁
let to = self.taoyao.remoteClient.map(v => v.sn)[0];
if(!self.taoyao.checkCandidate(e.candidate)) {
console.debug('Send Mesh ICE Candidate Fail', e);
return;
}
console.debug('Send Mesh ICE Candidate', to, e);
self.taoyao.push(signalProtocol.buildProtocol(
signalProtocol.media.candidate,
{
to: to,
type: 'mesh',
candidate: e.candidate
}
));
};
this.localMediaChannel = mediaChannel;
this.remoteMediaChannel = mediaChannel;
}
};
}
/** 桃夭 */
function Taoyao(
videoId,
webSocket,
localClientAudioEnabled,
localClientVideoEnabled
) {
/** 本地视频ID */
this.videoId = videoId;
/** WebRTC配置 */
this.webrtc = null;
/** WebSocket地址 */
// "wss://192.168.1.100:8888/websocket.signal"
this.webSocket = webSocket || 'wss://' + location.host + '/websocket.signal';
/** 设备状态 */
this.audioEnabled = true;
this.videoEnabled = true;
/** 媒体配置 */
this.audioConfig = defaultAudioConfig;
this.videoConfig = defaultVideoConfig;
/** 发送信令 */
this.push = null;
/** 本地终端 */
this.localClient = null;
this.localMediaChannel = null;
/** 本地媒体状态 */
this.localClientAudioEnabled = localClientAudioEnabled == undefined ? false : localClientAudioEnabled;
this.localClientVideoEnabled = localClientVideoEnabled == undefined ? true : localClientVideoEnabled;
/** 远程终端 */
this.remoteClient = [];
this.remoteMediaChannel = null;
/** 是否共享媒体通道 */
this.shareMediaChannel = true;
/** 信令通道 */
this.signalChannel = null;
/** 媒体配置 */
this.configMedia = function(audio = {}, video = {}) {
this.audioConfig = {...this.audioConfig, ...audio};
this.videoConfig = {...this.videoConfig, ...video};
console.debug('终端媒体配置', this.audioConfig, this.videoConfig);
return this;
};
/** WebRTC配置 */
this.configWebrtc = function(config = {}) {
this.webrtc = config;
this.webSocket = this.webrtc.signal.address;
this.shareMediaChannel = this.webrtc.framework === 'MOON';
defaultRPCConfig.iceServers = this.webrtc.stun.map(v => ({'urls': v}));
console.debug('WebRTC配置', this.webrtc, defaultRPCConfig);
return this;
};
/** 打开信令通道 */
this.buildChannel = function(callback) {
signalChannel.taoyao = this;
this.signalChannel = signalChannel;
// 不能直接this.push = this.signalChannel.push这样导致this对象错误
this.push = function(data, pushCallback) {
this.signalChannel.push(data, pushCallback);
};
return this.signalChannel.connect(this.webSocket, callback);
};
/** 打开本地媒体 */
this.buildLocalMedia = function() {
let self = this;
return new Promise((resolve, reject) => {
if(
navigator.mediaDevices &&
navigator.mediaDevices.getUserMedia &&
navigator.mediaDevices.enumerateDevices
) {
navigator.mediaDevices.enumerateDevices()
.then(list => {
let audioEnabled = false;
let videoEnabled = false;
list.forEach(v => {
console.debug('终端媒体设备', v, v.kind, v.label);
switch(v.kind) {
case 'audioinput':
audioEnabled = true;
break;
case 'videoinput':
videoEnabled = true;
break;
default:
console.debug('没有适配设备', v.kind, v.label);
break;
}
});
if(!audioEnabled) {
console.warn('终端没有音频输入设备');
self.audioEnabled = false;
}
if(!videoEnabled) {
console.warn('终端没有视频输入设备');
self.videoEnabled = false;
}
console.debug('打开终端媒体', self.audioEnabled, self.videoEnabled, self.audioConfig, self.videoConfig);
navigator.mediaDevices.getUserMedia({
audio: self.audioEnabled ? self.audioConfig : false,
video: self.videoEnabled ? self.videoConfig : false
})
.then(resolve)
.catch(reject);
})
.catch(e => {
console.error('检查终端设备异常', e);
self.videoEnabled = false;
self.videoEnabled = false;
});
} else {
throw new Error('不支持的终端设备');
}
});
};
/** 远程终端过滤 */
this.remoteClientFilter = function(sn, autoBuild) {
if(sn === signalConfig.sn) {
console.warn('远程终端等于本地终端');
return this.localClient;
}
let array = this.remoteClient.filter(v => v.sn === sn);
let remote = null;
if(array.length > 0) {
remote = array[0];
} else if(autoBuild) {
remote = new TaoyaoClient(this, sn, this.shareMediaChannel);
remote.openMediaChannel();
this.remoteClient.push(remote);
}
return remote;
};
/** 关闭:关闭媒体 */
this.close = function() {
// TODO释放资源
};
/** 关机:关闭媒体、关闭信令 */
this.shutdown = function() {
this.close();
};
/** 打开媒体通道 */
this.buildMediaChannel = async function(localVideoId, stream) {
let self = this;
// 本地视频
this.localClient = new TaoyaoClient(this, signalConfig.sn, this.shareMediaChannel, this.localClientAudioEnabled, this.localClientVideoEnabled);
await this.localClient.buildStream(localVideoId, stream);
if(this.shareMediaChannel) {
// 本地通道
this.localMediaChannel = new RTCPeerConnection(defaultRPCConfig);
this.localClient.audioTrack.forEach(v => this.localMediaChannel.addTrack(v, this.localClient.stream));
this.localClient.videoTrack.forEach(v => this.localMediaChannel.addTrack(v, this.localClient.stream));
this.localMediaChannel.ontrack = function(e) {
console.debug('Local Media Track', e);
};
this.localMediaChannel.onicecandidate = function(e) {
// TODO判断给谁
let to = self.remoteClient.map(v => v.sn)[0];
if(!self.checkCandidate(e.candidate)) {
console.debug('Send Local ICE Candidate Fail', e);
return;
}
console.debug('Send Local ICE Candidate', to, e);
self.push(signalProtocol.buildProtocol(
signalProtocol.media.candidate,
{
to: to,
type: 'local',
candidate: e.candidate
}
));
};
// 远程通道
this.remoteMediaChannel = new RTCPeerConnection(defaultRPCConfig);
this.remoteMediaChannel.ontrack = function(e) {
console.debug('Remote Media Track', e.track.sn, e);
let remote = self.taoyao.remoteClientFilter(e.track.sn);
// TODO判断数量
remote.buildStream(remote.sn, e.streams[0], e.track);
};
this.remoteMediaChannel.onicecandidate = function(e) {
// TODO判断给谁
let to = self.remoteClient.map(v => v.sn)[0];
if(!self.checkCandidate(e.candidate)) {
console.debug('Send Remote ICE Candidate Fail', e);
return;
}
console.debug('Send Remote ICE Candidate', to, e);
self.push(signalProtocol.buildProtocol(
signalProtocol.media.candidate,
{
to: to,
type: 'remote',
candidate: e.candidate
}
));
};
console.debug('打开共享媒体通道', this.localMediaChannel, this.remoteMediaChannel);
}
return this;
};
/** 校验candidate */
this.checkCandidate = function(candidate) {
if(
!candidate ||
!candidate.candidate ||
candidate.sdpMid === null ||
candidate.sdpMid === null ||
candidate.sdpMLineIndex === undefined ||
candidate.sdpMLineIndex === undefined
) {
return false;
}
return true;
};
/** 媒体信令 */
this.mediaSubscribe = function(sn, callback) {
let self = this;
self.remoteClientFilter(sn, true);
self.push(signalProtocol.buildProtocol(
signalProtocol.media.subscribe,
{
to: sn
}
), callback);
};
/** 会议信令 */
this.meetingCreate = function(callback) {
let self = this;
self.push(signalProtocol.buildProtocol(
signalProtocol.meeting.create
), callback);
}
this.meetingEnter = function(id, callback) {
let self = this;
self.push(signalProtocol.buildProtocol(
signalProtocol.meeting.enter,
{
id: id
}
), callback);
};
};

View File

@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>直播</title>
<link rel="stylesheet" type="text/css" href="./css/font.min.css" />
<link rel="stylesheet" type="text/css" href="./css/style.css" />
<script type="text/javascript" src="./javascript/taoyao.js"></script>
</head>
<body>
<div class="taoyao" id="taoyao">
<div class="live">
<div class="video">
</div>
<div class="handler">
<a class="audio icon-volume-medium" title="音频状态"></a>
<a class="video icon-play2" title="视频状态"></a>
<a class="record icon-radio-checked" title="录制视频"></a>
<a class="kick icon-cancel-circle" title="退出直播"></a>
<a class="close icon-switch" title="关闭直播"></a>
</div>
</div>
</div>
<script type="text/javascript">
const live = document.getElementById('taoyao');
</script>
</body>
</html>

View File

@@ -0,0 +1,128 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>会议</title>
<link rel="stylesheet" type="text/css" href="./css/font.min.css" />
<link rel="stylesheet" type="text/css" href="./css/style.css" />
<script src="https://unpkg.com/vue@2.6.11/dist/vue.js"></script>
<script type="text/javascript" src="./javascript/taoyao.js"></script>
</head>
<body>
<div class="taoyao" id="app">
<div class="handler">
<a class="create icon-make-group" title="创建会议" @click="create"></a>
<a class="invite icon-address-book" title="邀请会议" @click="invite"></a>
<a class="enter icon-enter" title="进入会议" @click="enter"></a>
<a class="leave icon-exit" title="离开会议" @click="leave"></a>
<a class="close icon-switch" title="关闭会议" @click="close"></a>
</div>
<div class="list">
<div class="meeting me">
<div class="video">
<video id="local"></video>
</div>
<div class="handler">
<a class="audio icon-volume-medium" title="音频状态" @click="audioSelf"></a>
<a class="video icon-play2" title="视频状态" @click="videoSelf"></a>
<a class="record icon-radio-checked" title="录制视频" @click="recordSelf"></a>
</div>
</div>
<div class="meeting" v-for="client in this.remoteClient" :key="client.sn">
<div class="video">
<video v-bind:id="client.sn"></video>
</div>
<div class="handler">
<a class="audio" title="音频状态" v-bind:class="client.audioStatus?'icon-volume-medium':'icon-volume-mute2'" @click="audio(client.sn)"></a>
<a class="video" title="视频状态" v-bind:class="client.videoStatus?'icon-play2':'icon-stop'" @click="video(client.sn)"></a>
<a class="record icon-radio-checked" title="录制视频" v-bind:class="client.recordStatus?'active':''" @click="record(client.sn)"></a>
<a class="expel icon-cancel-circle" title="踢出会议" @click="expel(client.sn)"></a>
</div>
</div>
</div>
</div>
<script type="text/javascript">
const vue = new Vue({
el: "#app",
data: {
taoyao: null,
remoteClient: [],
meetingId: null
},
mounted() {
// 设置帐号
let sn = prompt('你的账号', signalConfig.sn);
signalConfig.sn = sn;
// 加载桃夭
let self = this;
this.taoyao = new Taoyao('local');
this.remoteClient = this.taoyao.remoteClient;
// 打开信令通道
this.taoyao
.buildChannel(self.callback)
.then(e => console.debug('信令通道连接成功'));
},
beforeDestroy() {
},
methods: {
// 信令回调true表示已经处理false表示没有处理
callback: function(data) {
let self = this;
switch(data.header.pid) {
}
return false;
},
// 创建会议
create: function(event) {
let self = this;
this.taoyao.meetingCreate(data => {
self.taoyao.meetingEnter(data.body.id);
return true;
});
},
// 会议邀请
invite: function(sn) {
},
// 进入会议
enter: function(sn) {
let id = prompt('房间标识');
let password = prompt('房间密码');
if(id) {
this.taoyao.meetingEnter(id);
}
},
// 离开会议
leave: function(sn) {
},
// 关闭会议
close: function(sn) {
},
// 控制音频
audio: function(sn) {
this.client(sn).audio = !this.client(sn).audio;
},
// 控制视频
video: function(sn) {
this.client(sn).video = !this.client(sn).video;
},
// 录制视频
record: function(sn) {
this.client(sn).record = !this.client(sn).record;
},
// 踢出会议
expel: function(sn) {
},
// 控制音频
audioSelf: function(sn) {
},
// 控制视频
videoSelf: function(sn) {
},
// 录制视频
recordSelf: function(sn) {
}
}
});
</script>
</body>
</html>