465 lines
12 KiB
JavaScript
465 lines
12 KiB
JavaScript
var bricks = window.bricks || {}
|
||
|
||
bricks.VideoBox = class extends bricks.JsWidget {
|
||
create(){
|
||
this.dom_element = this._create('video');
|
||
this.set_attribute('autoplay', true);
|
||
this.set_attribute('muted', true);
|
||
}
|
||
get_stream(){
|
||
return this.stream;
|
||
}
|
||
set_stream(stream){
|
||
this.stream = stream
|
||
this.dom_element.srcObject = this.stream;
|
||
}
|
||
}
|
||
|
||
bricks.Signaling = class {
|
||
/*
|
||
{
|
||
signaling_url:
|
||
info:
|
||
connect_opts:
|
||
onclose:
|
||
onlogin:
|
||
}
|
||
*/
|
||
constructor(opts){
|
||
this.signaling_url = opts.signaling_url;
|
||
this.info = opts.info;
|
||
this.connect_opts = opts.connect_opts;
|
||
this.peers = [];
|
||
this.sessions = {};
|
||
this.handlers = {};
|
||
this.sessionhandlers = {};
|
||
this.init_websocket();
|
||
this.hb_task = null;
|
||
this.heartbeat_period = opts.heartbeat_period;
|
||
this.onclose = opts.onclose;
|
||
this.onlogin = opts.onlogin;
|
||
this.onopen = opts.onopen;
|
||
if (!this.heartbeat_period){
|
||
this.heartbeat_period = 0;
|
||
}
|
||
}
|
||
|
||
init_websocket(){
|
||
this.socket = new WebSocket(this.signaling_url);
|
||
this.socket.onmessage = this.signaling_recvdata.bind(this);
|
||
this.socket.onopen = this.login.bind(this);
|
||
this.socket.onclose = this.reconnect.bind(this);
|
||
this.socket.onerror = this.reconnect.bind(this);
|
||
}
|
||
reconnect(){
|
||
console.log('eror happened');
|
||
if (this.hb_task){
|
||
this.hb_task.cancel();
|
||
this.hb_task = null;
|
||
}
|
||
if (this.onclose){
|
||
this.onclose();
|
||
}
|
||
return;
|
||
try {
|
||
this.socket.close();
|
||
} catch(e){
|
||
console.log('error,', e);
|
||
}
|
||
this.init_websocket();
|
||
}
|
||
async signaling_recvdata(event){
|
||
var datapkg = JSON.parse(event.data);
|
||
var data = datapkg.data;
|
||
console.log('ws recv data=', data);
|
||
if (data.session) {
|
||
var sessionid = data.session.sessionid;
|
||
var sessiontype = data.session.sessiontype;
|
||
var handler = this.handlers[sessionid]
|
||
if (!handler){
|
||
var k = this.sessionhandlers[sessiontype];
|
||
if (!k){
|
||
throw 'recvdata_handler() exception(' + sessiontype + ')';
|
||
}
|
||
var h = new k(this, data.session, this.connect_opts);
|
||
handler = h.recvdata_handler.bind(h);
|
||
this.add_handler(sessionid, handler);
|
||
}
|
||
await handler(data);
|
||
} else {
|
||
await this.recvdata_handler(data);
|
||
}
|
||
}
|
||
add_handler(key, handler){
|
||
this.handlers[key] = handler;
|
||
}
|
||
add_sessionhandler(sessiontype, handler){
|
||
this.sessionhandlers[sessiontype] = handler;
|
||
}
|
||
async recvdata_handler(data){
|
||
console.log('recv data=', data);
|
||
if (data.type == 'online'){
|
||
data.online.forEach(p => {
|
||
var d = this.peers[p.id];
|
||
if (!d) d = {};
|
||
d = bricks.extend(d, p);
|
||
this.peers[p.id] = d;
|
||
});
|
||
if (this.onlogin){
|
||
this.onlogin(data.online);
|
||
}
|
||
return;
|
||
}
|
||
if (data.type == 'new_session'){
|
||
var peers = {};
|
||
Object.keys(this.peers).forEach(k =>{
|
||
if (k != data.msgfrom.id){
|
||
peers[k] = this.peers[k];
|
||
}
|
||
});
|
||
return;
|
||
}
|
||
console.log('recv data=', data, 'NOT HANDLED');
|
||
}
|
||
new_session(sessiontype, peer){
|
||
var k = this.sessionhandlers[sessiontype];
|
||
if (!k){
|
||
throw 'new_session() exception(' + sessiontype + ')';
|
||
}
|
||
var sessionid = bricks.uuid();
|
||
var session = {
|
||
sessiontype:sessiontype,
|
||
sessionid:sessionid
|
||
}
|
||
var d = {
|
||
type:'new_session',
|
||
session: session
|
||
};
|
||
this.send_data(d);
|
||
var opts = bricks.extend({}, this.connect_options);
|
||
opts.peer_info = peer;
|
||
var h = new k(this, session, opts);
|
||
this.add_handler(sessionid, h.recvdata_handler.bind(h) );
|
||
return h
|
||
}
|
||
login(){
|
||
console.log('login send', this.heartbeat_period)
|
||
var d = {
|
||
type:'login',
|
||
}
|
||
this.send_data(d);
|
||
if (this.heartbeat_period > 0){
|
||
console.log('call login again in', this.heartbeat_period, ' seconds');
|
||
this.hb_task = schedule_once(this.login.bind(this), this.heartbeat_period);
|
||
}
|
||
}
|
||
|
||
logout(){
|
||
var d= {
|
||
type:'logout',
|
||
}
|
||
this.send_data(d);
|
||
}
|
||
send_data(d){
|
||
d.msgfrom = this.info;
|
||
var s = JSON.stringify(d);
|
||
console.log('send_data()', s);
|
||
this.socket.send(s);
|
||
}
|
||
socket_send(s){
|
||
this.socket.send(s);
|
||
}
|
||
}
|
||
|
||
bricks.RTCP2PConnect = class {
|
||
/*
|
||
opts:{
|
||
ice_servers:
|
||
peer_info:
|
||
auto_callaccept: true or false
|
||
media_options: { video:trur or false, audio:true or false }
|
||
data_connect: true or false
|
||
}
|
||
*/
|
||
|
||
constructor(signaling, session, opts){
|
||
this.id = bricks.uuid();
|
||
this.signaling = signaling;
|
||
this.session = session;
|
||
this.requester = false;
|
||
this.opts = opts;
|
||
this.peers = {};
|
||
this.signal_handlers = {};
|
||
this.local_stream = null;
|
||
this.localVideo = null;
|
||
this.add_handler('sessioncreated', this.h_sessioncreated.bind(this));
|
||
this.add_handler('callrequest', this.h_callrequest.bind(this));
|
||
this.add_handler('callaccepted', this.h_callaccepted.bind(this));
|
||
this.add_handler('offer', this.h_offer.bind(this));
|
||
this.add_handler('answer', this.h_answer.bind(this));
|
||
this.add_handler('icecandidate', this.h_icecandidate.bind(this));
|
||
this.add_handler('sessionquit', this.h_sessionquit.bind(this));
|
||
}
|
||
|
||
add_handler(type, f){
|
||
this.signal_handlers[type] = f;
|
||
}
|
||
get_handler(typ){
|
||
return this.signal_handlers[typ];
|
||
}
|
||
|
||
async p2pconnect(peer){
|
||
await this.getLocalStream();
|
||
var p = this.peers[peer.id];
|
||
if (!p){
|
||
await this.createPeerConnection(peer);
|
||
} else {
|
||
aconsole.log(peer, 'connect exists', this);
|
||
}
|
||
console.log('p2pconnect() called, this=', this, 'peer=', peer);
|
||
}
|
||
async h_sessioncreated(data){
|
||
await this.p2pconnect(this.opts.peer_info, 'requester');
|
||
if (this.opts.peer_info){
|
||
var d = {
|
||
type:'callrequest',
|
||
msgto:this.opts.peer_info
|
||
}
|
||
this.signaling_send(d);
|
||
this.requester = true;
|
||
}
|
||
}
|
||
async h_callrequest(data){
|
||
if (this.opts.auto_callaccept || true){
|
||
await this.p2pconnect(data.msgfrom, 'responser');
|
||
var d = {
|
||
type:'callaccepted',
|
||
msgto:data.msgfrom
|
||
};
|
||
this.signaling_send(d);
|
||
return;
|
||
}
|
||
}
|
||
async h_callaccepted(data){
|
||
await this.send_offer(data.msgfrom, true);
|
||
}
|
||
async h_offer(data){
|
||
console.log('h_offer(), this=', this, 'peer=', data.msgfrom);
|
||
var pc = this.peers[data.msgfrom.id].pc;
|
||
var offer = new RTCSessionDescription(data.offer);
|
||
await pc.setRemoteDescription(offer);
|
||
var answer = await pc.createAnswer();
|
||
await pc.setLocalDescription(answer);
|
||
this.signaling_send({
|
||
type:'answer',
|
||
answer:pc.localDescription,
|
||
msgto:data.msgfrom
|
||
});
|
||
if (this.peers[data.msgfrom.id].role != 'requester'){
|
||
await this.send_offer(data.msgfrom);
|
||
}
|
||
|
||
}
|
||
async h_answer(data){
|
||
var desc = new RTCSessionDescription(data.answer);
|
||
var pc = this.peers[data.msgfrom.id].pc;
|
||
await pc.setRemoteDescription(desc);
|
||
}
|
||
async h_icecandidate(data){
|
||
var candidate = new RTCIceCandidate(data.candidate);
|
||
var pc = this.peers[data.msgfrom.id].pc;
|
||
await pc.addIceCandidate(candidate);
|
||
}
|
||
async h_sessionquit(data){
|
||
var pc = this.peers[data.msgfrom.id].pc;
|
||
pc.close();
|
||
}
|
||
async send_offer(peer, initial){
|
||
console.log('send_offer(), peers=', this.peers, 'peer=', peer);
|
||
var pc = this.peers[peer.id].pc;
|
||
if (initial){
|
||
this.peers[peer.id].role = 'requester';
|
||
} else {
|
||
this.peers[peer.id].role = 'responser';
|
||
}
|
||
var offer = await pc.createOffer();
|
||
await pc.setLocalDescription(offer);
|
||
var d = {
|
||
type:'offer',
|
||
offer:pc.localDescription,
|
||
msgto:peer
|
||
};
|
||
this.signaling_send(d);
|
||
}
|
||
|
||
send_candidate(peer, event){
|
||
console.log('send_candidate called, peer=', peer, 'event=', event);
|
||
if (event.candidate) {
|
||
var candidate = event.candidate;
|
||
this.signaling_send({
|
||
type: 'icecandidate',
|
||
msgto:peer,
|
||
candidate: candidate
|
||
});
|
||
}
|
||
}
|
||
|
||
signaling_send(d){
|
||
d.session = this.session;
|
||
this.signaling.send_data(d);
|
||
}
|
||
|
||
async recvdata_handler(data){
|
||
console.log('recvdata=', data, this.signal_handlers);
|
||
var f = this.get_handler(data.type);
|
||
if (f) {
|
||
await f(data);
|
||
return;
|
||
}
|
||
console.log('recvdata=', data, 'NOT HANDLED');
|
||
}
|
||
|
||
async ice_statechange(peer, event){
|
||
var pc = this.peers[peer.id].pc;
|
||
console.log(`oniceconnectionstatechange, pc.iceConnectionState is ${pc.iceConnectionState}.`);
|
||
}
|
||
|
||
async connection_statechange(peer, event){
|
||
var pc = this.peers[peer.id].pc;
|
||
console.log(`${peer.id} state changed. new state=${pc.connectionState}`);
|
||
console.log('state=', pc.connectionState, typeof(pc.connectionState));
|
||
if (pc.connectionState == 'disconnected'){
|
||
if (this.opts.on_pc_disconnected){
|
||
this.opts.on_pc_disconnected(peer);
|
||
}
|
||
return;
|
||
}
|
||
if (pc.connectionState == 'connected'){
|
||
console.log('state is connected, data_connect=',
|
||
this.opts.data_connect);
|
||
}
|
||
}
|
||
|
||
async dc_accepted(peer, event){
|
||
console.log('accept datachannel ....');
|
||
this.peers[peer.id].dc = event.channel;
|
||
await this.dc_created(peer, this.peers[peer.id].dc);
|
||
}
|
||
async dc_created(peer, dc){
|
||
console.log('dc_created.....', dc);
|
||
dc.onmessage = this.datachannel_message.bind(peer);
|
||
dc.onopen = this.datachannel_open(peer);
|
||
dc.onclose = this.datachannel_close(peer);
|
||
}
|
||
|
||
async datachannel_message(peer, event){
|
||
console.log('datachannel_message():', this, arguments);
|
||
var dc = this.peers[peer.id].dc;
|
||
if (this.opts.on_dc_messaage){
|
||
await this.opts.on_dc_message(dc, event.data);
|
||
}
|
||
}
|
||
async datachannel_open(peer){
|
||
console.log('datachannel_open():', this, arguments);
|
||
var dc = this.peers[peer.id].dc
|
||
dc.send('test');
|
||
if (this.opts.on_dc_open){
|
||
await this.opts.on_dc_open(dc);
|
||
}
|
||
}
|
||
async datachannel_close(peer){
|
||
console.log('datachannel_close():', this, arguments);
|
||
var dc = this.peers[peer.id].dc
|
||
if (this.opts.on_dc_close){
|
||
await this.opts.on_dc_close(dc);
|
||
}
|
||
}
|
||
async createPeerConnection(peer){
|
||
const configuration = {
|
||
iceServers:this.opts.ice_servers
|
||
}
|
||
console.log('RTCPC configuration=', configuration);
|
||
var pc = new RTCPeerConnection(configuration);
|
||
if (this.local_stream){
|
||
this.local_stream.getTracks()
|
||
.forEach(track => {
|
||
pc.addTrack(track, this.local_stream);
|
||
});
|
||
}
|
||
this.peers[peer.id] = bricks.extend({}, peer);
|
||
this.peers[peer.id].pc = pc;
|
||
this.peers[peer.id].role = '';
|
||
var remoteVideo = new bricks.VideoBox();
|
||
this.peers[peer.id].video = remoteVideo;
|
||
if (role == 'requester'){
|
||
this.peers[peer.id].dc = pc.createDataChannel('chat', {ordered:true});
|
||
var dc = this.peers[peer.id].dc;
|
||
await this.dc_created(peer, this.peers[peer.id].dc);
|
||
if(this.opts.on_pc_connected){
|
||
this.opts.on_pc_connected(peer);
|
||
}
|
||
console.log('dc created', this.peers[peer.id].dc);
|
||
}
|
||
pc.onicecandidate = this.send_candidate.bind(this, peer);
|
||
pc.oniceconnectionstatechange = this.ice_statechange.bind(this, peer);
|
||
pc.onconnectionstatechange = this.connection_statechange.bind(this, peer);
|
||
pc.ondatachanel = this.dc_accepted.bind(this, peer);
|
||
pc.ontrack = event => {
|
||
remoteVideo.set_stream(event.streams[0]);
|
||
}
|
||
}
|
||
async changeLocalVideoStream(peer, new_stream){
|
||
var pc = this.peers[peer.id].pc;
|
||
const senders = pc.getSenders();
|
||
const oldVideoTrackSender = senders.find(sender => sender.track && sender.track.kind === 'video');
|
||
if (oldVideoTrackSender) {
|
||
pc.removeTrack(oldVideoTrackSender);
|
||
}
|
||
new_stream.getTracks().forEach(track => {
|
||
if (track.kind === 'video') {
|
||
pc.addTrack(track, newStream);
|
||
}
|
||
});
|
||
await this.send_offer(peer, true);
|
||
}
|
||
async getLocalStream() {
|
||
if (this.opts.media_options){
|
||
try {
|
||
var mediaOptions = this.opts.media_options;
|
||
this.local_stream = await navigator.mediaDevices.getUserMedia(mediaOptions);
|
||
this.localVideo = new bricks.VideoBox();
|
||
this.localVideo.set_stream(this.local_stream);
|
||
} catch (error) {
|
||
console.error('获取本地媒体流失败:', error);
|
||
}
|
||
try {
|
||
this.local_screen = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: false });
|
||
} catch (error) {
|
||
console.error('获取本地Screen失败:', error);
|
||
}
|
||
}
|
||
}
|
||
peer_close(peer){
|
||
var pc = this.peers[peer.id].pc;
|
||
var video = this.peers[peer.id].video;
|
||
pc.getReceivers().forEach(receiver => {
|
||
if (receiver.track){
|
||
receiver.track.stop();
|
||
}
|
||
});
|
||
pc.getSenders().forEach(receiver => {
|
||
if (receiver.track){
|
||
receiver.track.stop();
|
||
}
|
||
});
|
||
video.srcObject.getTracks().forEach(track => track.stop());
|
||
var dc = this.peers[peer.id].dc;
|
||
if (dc){
|
||
dc.close();
|
||
}
|
||
pc.close();
|
||
delete self.peers[peer.id];
|
||
}
|
||
};
|
||
|