bricks/bricks/rtc.js
2024-10-12 11:44:26 +08:00

465 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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];
}
};