bricks/bricks/rtc.js
2024-10-11 12:31:53 +08:00

793 lines
20 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 handler = new k(this, data.session, this.connect_opts);
}
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];
}
});
}
}
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.signaling = signaling;
this.session = session;
this.requester = false;
this.opts = opts;
this.peers = {};
this.signal_handlers = {};
this.localStream = 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(type){
return this.signal_handlers[type];
}
async p2pconnect(peer){
await this.getLocalStream();
await this.createPeerConnection(peer);
}
async h_sessioncreated(data){
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){
var d = {
type:'callaccepted',
msgto:data.msgfrom
};
this.signaling_send(d);
await this.p2pconnect(data.msgfrom);
return;
}
this.dispatch('callrequest', data.msgfrom);
}
async h_callaccepted(data){
await this.p2pconnect(data.msgfrom);
await this.send_offer(data.msgfrom);
}
async h_offer(data){
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.signal_send({
type:'answer',
answer:pc.localDescription,
msgto: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){
pc = this.peers[peer.id].pc;
var offer = await pc.createOffer();
await pc.setLocalDescription(offer);
var d = {
type:'offer',
offer:pc.localDescription,
msgto:peer
};
this.signal_send(d);
}
send_candidate(peer, 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.signal_handlers[data.type];
if (f) {
await f(data);
}
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;
if (pc.connectionState == 'disconnected'){
var w = new bricks.Error({
title:'Error',
message:'lose connection',
timeout:4
});
w.open();
return;
}
if (pc.connectionState == 'connected'){
if (this.opts.data_connect){
var dc = pc.createDataChannel();
if (this.requester && this.opts.data_connect){
await this.createDataChannel(peer);
}
}
console.log(`${peer.id} connect losed. ${pc.connectionState}`);
}
}
async createDataChannel(peer){
var pc = this.peers[peer.id].pc;
dc = pc.createDataChannel('data');
this.peers[peer.id].dc = dc;
dc.onmessage = this.datachannel_message.bind(this, peer);
dc.onopen = this.datachannel_open(this, peer);
dc.onclose = this.datachannl_close(this, peer);
}
async datachannel_message(peer, msg){
var dc = this.peers[peer.id].dc;
if (this.opts.on_dc_messaage){
await this.opts.on_dc_message(dc, msg);
}
}
async datachannel_open(peer){
var dc = this.peers[peer.id].dc
if (this.opts.on_dc_open){
await this.opts.on_dc_open(dc);
}
}
async datachannel_close(peer){
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
}
var pc = RTCPeerConnection(configuration);
this.localStream.getTracks()
.forEach(track => {
pc.addTrack(track, this.localStream);
});
this.peers[peer.id] = peer;
this.peers[peer.id][pc] = pc;
var remoteVideo = new VideoBox();
this.peers[peer.id][video] = remoteVideo;
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 = event => {
var dc = event.channel;
this.peers[peer.id].dc = dc;
dc.onmessage = this.data_message_recv.bind(this, peer);
dc.onopen = this.datachannel_opened(this, peer);
dc.onclose = this.datachannl_closed(this, peer);
}
pc.ontrack = event => {
remoteVideo.set_stream(event.streams[0]);
}
}
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 VideoBox();
this.localVideo.set_stream(stream);
} catch (error) {
console.error('获取本地媒体流失败:', error);
}
}
}
}
bricks.RTCClient = class extends bricks.VBox {
/*
{
"signaling_url":
"ice_url":
"mediaOptions":{
"audio":true,
"video":true
}
"auto_call_accept":false
"info":{
"id":
"name":
}
}
*/
constructor(opts){
opts.width = '100%';
super(opts);
this.build_head();
this.videobox = new bricks.ResponsableBox({css:'filler'});
this.videobox.set_style('position', 'relative');
this.localVideo = new bricks.VideoBox({});
this.remoteVideo = new bricks.VideoBox({});
this.videobox.add_widget(this.localVideo);
this.videobox.add_widget(this.remoteVideo);
this.add_widget(this.videobox);
this.videosize_switch();
this.pc;
this.onlineList= [];
this.init_websocket();
}
videosize_switch(){
if(! this.local_small){
this.local_small = true;
this.localVideo.set_css('smallvideo');
this.remoteVideo.set_css('bigvideo');
} else {
this.local_small = false;
this.localVideo.set_css('bigvideo');
this.remoteVideo.set_css('smallvideo');
}
}
async captureScreen() {
let mediaStream = null;
try {
mediaStream = await navigator.mediaDevices.getDisplayMedia({
video: {
cursor: "always"
},
audio: false
});
return mediaStream;
} catch (ex) {
console.log("Error occurred", ex);
}
}
init_websocket(){
this.socket = new WebSocket(this.signaling_url);
this.socket.onmessage = this.signaling_message_handle.bind(this);
this.socket.onopen = this.signaling_login.bind(this);
this.socket.onclose = function(){
console.log('888888 this.socket is closed', this.socket);
if (this.parent){
this.init_websockset();
}
};
this.socket.onerror = function(){
try {
this.socket.close();
} catch(e){
console.log('error,', e);
}
};
}
build_head(){
var box = new bricks.HBox({height:'50px'});
this.add_widget(box);
this.build_phone(box);
this.peer_w = new bricks.Text({text:''});
box.add_widget(this.peer_w);
}
build_phone(box){
var opts = {
states:[
{
state:'free',
url:bricks_resource('imgs/free_phone.png')
},{
state:'using',
url:bricks_resource('imgs/using_phone.png')
}
],
state:'free',
rate:2
}
this.phone = new bricks.StatedIcon(opts);
this.phone.bind('click', this.phone_action.bind(this));
box.add_widget(this.phone);
}
phone_action(){
if (this.phone.state == 'free'){
this.choose_peer();
} else {
this.call_close();
}
}
choose_peer(){
var items = [];
if (this.onlineList.length < 1){
var w = new bricks.Error({title:'Error',
timeout:4,
message:'no peer logined'
});
w.open();
return;
}
this.onlineList.forEach( p => {
var m = {
name:p.id,
label:p.name
}
items.push(m);
});
var menu = new bricks.Menu({
width:'300px',
height:'400px',
items:items
});
menu.set_css('popup');
bricks.Body.add_widget(menu);
menu.bind('command', this.call_peer.bind(this));
this.menu = menu;
}
async call_peer(event){
console.log('event params=', event.params);
bricks.Body.remove_widget(this.menu);
var peer_info = {
id:event.params.name,
name:event.params.label
}
this.call_request(peer_info);
}
signaling_login(){
var d = {
type:'login',
info:this.info
}
this.socket_send(JSON.stringify(d));
}
signaling_logout(){
var d= {
type:'logout',
info:this.info
}
this.socket_send(JSON.stringify(d));
}
// 获取本地媒体流
async getLocalStream() {
try {
var mediaOptions = this.mediaOptions || {
video: true,
audio: true
};
const stream = await navigator.mediaDevices.getUserMedia(mediaOptions);
this.localVideo.set_stream(stream);
return stream;
} catch (error) {
console.error('获取本地媒体流失败:', error);
}
}
async call_request(peer_info){
var d = {
type:'callRequest',
to:peer_info
}
this.peer_info = peer_info;
this.role = 'requester';
this.socket_send(JSON.stringify(d));
}
async send_offer(){
var offer = await this.pc.createOffer();
await this.pc.setLocalDescription(offer);
console.log('########send_offer =', offer, this.pc.localDescription);
this.socket_send(JSON.stringify({
type:'offer',
offer:this.pc.localDescription,
to:this.peer_info
}));
}
send_candidate(peer_info, event){
if (event.candidate) {
var candidate = event.candidate;
console.log('send_candidate(), candidate=', candidate);
this.socket_send(JSON.stringify({
type: 'iceCandidate',
to:peer_info,
candidate: candidate
}));
}
}
async need_conform_call(peer_info){
var w = new bricks.Conform({title:'请确认',
message:'来自' + peer_info.name + '的连接请求'});
w.bind('conformed', this.call_accepted.bind(this, peer_info));
w.bind('cancelled', this.call_rejected.bind(this.peer_info));
w.open();
}
async call_accepted(peer_info){
if (this.peer_info){
this.call_close();
}
this.peer_info = peer_info
this.role = 'responser'
await this.createPeerConnection(this.peer_info);
this.socket_send(JSON.stringify({
type: 'callAccepted',
to: peer_info
}));
}
async call_close(active){
this.role = null;
this.pc.onicecandidate = null;
this.pc.ontrack = null;
this.pc.close();
if (! active){
this.socket_send(JSON.stringify({
type:'disconnect',
to:this.peer_info
}));
}
this.peer_info = null;
if (this.pc.getLocalStreams().length > 0) {
for (let stream of this.pc.getLocalStreams()) {
if (stream.getTracks) {
for (let track of stream.getTracks()) {
track.stop();
}
}
}
}
this.localVideo.get_stream().getTracks().forEach(track => track.stop());
this.phone.set_state('free');
this.peer_w.set_text('');
}
call_rejected(peer_info){
this.socket_send(JSON.stringify({
type: 'callRejected',
to: peer_info
}));
}
socket_send(s){
try {
this.socket.send(s);
} catch (e){
console.log('socket=', this.socket);
this.init_websocket();
this.socket.send(s);
}
}
async signaling_message_handle(event){
var datapkg = JSON.parse(event.data);
var d = datapkg.data;
console.log('data received from server', 'd=', d);
switch (d.type){
case 'onlineList':
this.onlineList = d.onlineList;
console.log('onlineList branch exe');
break;
case 'callRequest':
if (this.auto_call_accept){
this.call_accepted(d.msgfrom);
return
}
this.need_conform_call(d.msgfrom);
console.log('callRequest branch exe');
break;
case 'offer':
var offer = new RTCSessionDescription(d.offer);
await this.pc.setRemoteDescription(offer);
var answer = await this.pc.createAnswer();
await this.pc.setLocalDescription(answer);
console.log('###### send_answer=', answer, 'desc=', this.pc.localDescription);
this.socket_send(JSON.stringify({
type:'answer',
answer:this.pc.localDescription,
to:this.peer_info
}));
console.log('offer branch exe');
if (this.role == 'responser'){
this.send_offer();
}
break;
case 'answer':
console.log('#### receive_answer: d.msgfrom=', d.msgfrom, 'peer_info=', this.peer_info);
if (d.msgfrom.id == this.peer_info.id){
var desc = new RTCSessionDescription(d.answer);
await this.pc.setRemoteDescription(desc);
console.log('answer branch exe');
}
break;
case 'iceCandidate':
if (d.msgfrom.id == this.peer_info.id){
var candidate = new RTCIceCandidate(d.candidate);
await this.pc.addIceCandidate(candidate);
console.log('iceCandidate branch exe');
}
break;
case 'callAccepted':
await this.createPeerConnection(this.peer_info);
console.log('callAccepted branch exe');
break;
case 'callRejected':
this.peer_info = null;
console.log('callRejected branch exe');
break;
case 'disconnect':
this.call_close(true);
console.log('disconnect branch exe');
break;
default:
console.log(d.type, 'no branch defined');
break;
}
if (this.pc){
console.log('signal_status=', this.pc.signalingState,
'iceconnectstate=', this.pc.iceConnectionState,
'connectstate=', this.pc.connectionState,
'cantrickleIceCandidate=', this.pc.canTrickleIceCandidates,
'peerConnectionState=', this.pc.peerConnectionState,
'icegatheringstate=', this.pc.iceGatheringState);
}
}
// 创建 PeerConnection
async createPeerConnection(to_info) {
this.phone.set_state('using');
this.peer_w.set_text(to_info.name);
await this.getLocalStream();
const configuration = {
iceServers: this.ice_servers,
};
console.log('configuration=', configuration);
this.pc = new RTCPeerConnection(configuration);
// 处理本地流添加到 PeerConnection
this.localVideo
.get_stream()
.getTracks()
.forEach(track => {
this.pc.addTrack(track, this.localVideo.stream);
});
if (! this.pc){
console.log('pc is null');
return;
}
// 处理 ICE 候选
this.pc.onicecandidate = this.send_candidate.bind(this, to_info);
this.pc.oniceconnectionstatechange = this.ice_statechange.bind(this);
this.pc.onicegatheringstatechange = this.ice_g_statechange.bind(this);
this.pc.onconnectionstatechange = this.connection_statechange.bind(this);
// 处理远程流添加
this.pc.ontrack = event => {
console.log('=========ontrack() called ......');
this.remoteVideo.set_stream(event.streams[0]);
};
if (this.role == 'requester'){
await this.send_offer();
}
console.log('createPeerConnection() finished');
}
async ice_statechange(event){
console.log(`oniceconnectionstatechange, pc.iceConnectionState is ${this.pc.iceConnectionState}.`);
}
async connection_statechange(event){
console.log(`onconnectionstatechange, pc.connectionState is ${this.pc.connectionState}.`);
if (this.pc.connectionState == 'disconnected'){
var w = new bricks.Error({
title:'Error',
message:'lose connection',
timeout:4
});
w.open();
this.call_close();
this.dispatch('disconnected', this.peer_info);
return;
}
if (this.pc.connectionState == 'connected'){
this.dispatch('connected', this.peer_info);
}
}
ice_g_statechange(event){
console.log(`onicegatheringstatechange, pc.iceGatheringState is ${this.pc.iceGatheringState}.`);
}
}
bricks.Factory.register('RTCClient', bricks.RTCClient);