2024-08-19 15:50:51 +08:00
|
|
|
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.RTCClient = class extends bricks.VBox {
|
|
|
|
/*
|
|
|
|
{
|
|
|
|
"signaling_url":
|
|
|
|
"ice_url":
|
2024-08-20 14:04:50 +08:00
|
|
|
"info":{
|
|
|
|
"id":
|
|
|
|
"name":
|
|
|
|
}
|
2024-08-19 15:50:51 +08:00
|
|
|
}
|
|
|
|
*/
|
|
|
|
constructor(opts){
|
|
|
|
super(opts);
|
2024-08-20 14:04:50 +08:00
|
|
|
this.build_phone();
|
|
|
|
this.videobox = new bricks.ResponsableBox({});
|
2024-08-19 15:50:51 +08:00
|
|
|
this.localVideo = new bricks.VideoBox({});
|
|
|
|
this.remoteVideo = new bricks.VideoBox({});
|
2024-08-20 14:04:50 +08:00
|
|
|
this.videobox.add_widget(this.localVideo);
|
|
|
|
this.videobox.add_widget(this.remoteVideo);
|
|
|
|
this.add_widget(this.videobox);
|
2024-08-19 15:50:51 +08:00
|
|
|
this.peerConnection;
|
2024-08-20 16:58:09 +08:00
|
|
|
this.onlineList= [];
|
2024-08-19 15:50:51 +08:00
|
|
|
this.socket = new WebSocket(this.signaling_url);
|
2024-08-20 17:41:55 +08:00
|
|
|
this.socket.onmessage = this.signaling_message_handle.bind(this);
|
2024-08-20 14:04:50 +08:00
|
|
|
this.socket.onopen = this.signaling_login.bind(this);
|
2024-08-19 15:50:51 +08:00
|
|
|
this.socket.onclose = null;
|
|
|
|
this.socket.onerror = null;
|
|
|
|
}
|
2024-08-20 14:04:50 +08:00
|
|
|
build_phone(){
|
2024-08-20 14:44:47 +08:00
|
|
|
var opts = {
|
2024-08-20 14:04:50 +08:00
|
|
|
states:[
|
|
|
|
{
|
|
|
|
state:'free',
|
|
|
|
url:bricks_resource('imgs/free_phone.png')
|
|
|
|
},{
|
|
|
|
state:'using',
|
|
|
|
url:bricks_resource('img/using_phone.png')
|
|
|
|
}
|
|
|
|
],
|
|
|
|
state:'free',
|
|
|
|
rate:2
|
|
|
|
}
|
|
|
|
this.phone = new bricks.StatedIcon(opts);
|
|
|
|
this.add_widget(this.phone);
|
|
|
|
this.phone.bind('click', this.phone_action.bind(this));
|
|
|
|
}
|
|
|
|
phone_action(){
|
|
|
|
if (this.phone.state == 'free'){
|
|
|
|
this.choose_peer();
|
|
|
|
} else {
|
|
|
|
this.call_close();
|
|
|
|
this.phone.set_state('free');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
choose_peer(){
|
|
|
|
var items = [];
|
2024-08-20 17:36:30 +08:00
|
|
|
if (this.onlineList.length < 1){
|
|
|
|
var w = new bricks.Error({title:'Error',
|
|
|
|
timeout:4,
|
|
|
|
message:'no peer logined'
|
|
|
|
});
|
|
|
|
w.open();
|
2024-08-20 17:41:55 +08:00
|
|
|
return;
|
2024-08-20 17:36:30 +08:00
|
|
|
}
|
2024-08-20 14:04:50 +08:00
|
|
|
this.onlineList.forEach( p => {
|
|
|
|
var m = {
|
|
|
|
name:p.id,
|
|
|
|
label:p.name
|
2024-08-19 15:50:51 +08:00
|
|
|
}
|
2024-08-20 14:04:50 +08:00
|
|
|
items.push(m);
|
|
|
|
});
|
|
|
|
var menu = new bricks.Menu({
|
|
|
|
width:'300px',
|
2024-08-20 14:23:36 +08:00
|
|
|
height:'400px',
|
2024-08-20 14:04:50 +08:00
|
|
|
items:items
|
|
|
|
});
|
|
|
|
menu.set_css('popup');
|
2024-08-20 18:02:17 +08:00
|
|
|
bricks.Body.add_widget(menu);
|
2024-08-20 14:04:50 +08:00
|
|
|
menu.bind('command', this.call_peer.bind(this));
|
|
|
|
this.menu = menu;
|
|
|
|
}
|
2024-08-20 14:24:58 +08:00
|
|
|
call_peer(event){
|
2024-08-20 18:41:39 +08:00
|
|
|
console.log('event params=', event.params);
|
2024-08-20 18:43:06 +08:00
|
|
|
bricks.Body.remove_widget(this.menu);
|
2024-08-20 14:04:50 +08:00
|
|
|
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
|
2024-08-19 15:50:51 +08:00
|
|
|
}
|
|
|
|
this.socket.send(JSON.stringify(d));
|
|
|
|
}
|
|
|
|
// 获取本地媒体流
|
2024-08-20 14:04:50 +08:00
|
|
|
async getLocalStream() {
|
2024-08-19 15:50:51 +08:00
|
|
|
try {
|
|
|
|
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
|
|
|
|
this.localVideo.set_stream(stream);
|
|
|
|
return stream;
|
|
|
|
} catch (error) {
|
|
|
|
console.error('获取本地媒体流失败:', error);
|
|
|
|
}
|
|
|
|
}
|
2024-08-20 14:04:50 +08:00
|
|
|
async call_request(peer_info){
|
2024-08-19 15:50:51 +08:00
|
|
|
var d = {
|
2024-08-20 14:04:50 +08:00
|
|
|
type:'callRequest',
|
|
|
|
to:peer_info
|
2024-08-19 15:50:51 +08:00
|
|
|
}
|
2024-08-20 14:04:50 +08:00
|
|
|
this.peer_info = peer_info;
|
2024-08-19 15:50:51 +08:00
|
|
|
this.role = 'requester';
|
|
|
|
this.socket.send(JSON.stringify(d));
|
|
|
|
}
|
|
|
|
async send_offer(){
|
|
|
|
var offer = await this.peerConnection.createOffer();
|
|
|
|
this.peerConnection.setLocalDescription(offer);
|
|
|
|
this.socket.send(JSON.stringify({
|
|
|
|
type:'offer',
|
|
|
|
offer:this.peerConnection.localDescription,
|
2024-08-20 14:04:50 +08:00
|
|
|
to:this.peer_info
|
2024-08-19 15:50:51 +08:00
|
|
|
}));
|
|
|
|
}
|
|
|
|
send_candidate(event){
|
|
|
|
if (event.candidate) {
|
|
|
|
this.socket.send(JSON.stringify({
|
|
|
|
type: 'iceCandidate',
|
2024-08-20 14:04:50 +08:00
|
|
|
to:this.peer_info,
|
2024-08-19 15:50:51 +08:00
|
|
|
candidate: event.candidate
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
}
|
2024-08-20 14:04:50 +08:00
|
|
|
async need_conform_call(peer_info){
|
2024-08-19 15:50:51 +08:00
|
|
|
var w = new Conform({title:'请确认',
|
2024-08-20 14:26:42 +08:00
|
|
|
message:'来自' + peer_info.name + '的连接请求'});
|
2024-08-20 14:04:50 +08:00
|
|
|
w.bind('conformed', this.call_accepted.bind(this, peer_info));
|
|
|
|
w.bind('cancelled', this.call_rejected.bind(this.peer_info));
|
2024-08-19 15:50:51 +08:00
|
|
|
w.open();
|
|
|
|
}
|
2024-08-20 14:32:15 +08:00
|
|
|
async call_accepted(peer_info){
|
2024-08-20 14:04:50 +08:00
|
|
|
if (this.peer_info){
|
2024-08-19 15:50:51 +08:00
|
|
|
this.call_close();
|
|
|
|
}
|
2024-08-20 14:04:50 +08:00
|
|
|
this.peer_info = peer_info
|
2024-08-19 15:50:51 +08:00
|
|
|
this.role = 'responser'
|
2024-08-20 14:31:11 +08:00
|
|
|
await this.createPeerConnection();
|
2024-08-19 15:50:51 +08:00
|
|
|
this.socket.send(JSON.stringify({
|
|
|
|
type: 'callAccepted',
|
2024-08-20 14:04:50 +08:00
|
|
|
to: peer_info
|
2024-08-19 15:50:51 +08:00
|
|
|
}));
|
|
|
|
}
|
|
|
|
async call_close(active){
|
|
|
|
this.peerConnection.onicecandidate = null;
|
|
|
|
this.peerConnection.ontrack = null;
|
|
|
|
this.peerConnection.close();
|
|
|
|
if (! active){
|
|
|
|
this.socket.send(JSON.stringify({
|
|
|
|
type:'disconnect',
|
2024-08-20 14:04:50 +08:00
|
|
|
to:this.peer_info
|
2024-08-20 14:33:44 +08:00
|
|
|
}));
|
2024-08-19 15:50:51 +08:00
|
|
|
}
|
2024-08-20 14:04:50 +08:00
|
|
|
this.peer_info = null;
|
2024-08-19 15:50:51 +08:00
|
|
|
this.localVideo.get_stream().getTracks().forEach(track => track.stop());
|
|
|
|
}
|
2024-08-20 14:04:50 +08:00
|
|
|
call_rejected(peer_info){
|
2024-08-19 15:50:51 +08:00
|
|
|
this.socket.send(JSON.stringify({
|
|
|
|
type: 'callRejected',
|
2024-08-20 14:04:50 +08:00
|
|
|
to: peer_info
|
2024-08-19 15:50:51 +08:00
|
|
|
}));
|
|
|
|
}
|
|
|
|
async signaling_message_handle(event){
|
2024-08-20 17:49:48 +08:00
|
|
|
var datapkg = JSON.parse(event.data);
|
2024-08-20 19:23:01 +08:00
|
|
|
var d = datapkg.data;
|
2024-08-20 17:59:41 +08:00
|
|
|
console.log('data received from server', datapkg, 'd=', d, event);
|
2024-08-19 15:50:51 +08:00
|
|
|
switch (d.type){
|
2024-08-20 14:04:50 +08:00
|
|
|
case 'onlineList':
|
|
|
|
this.onlineList = d.onlineList;
|
2024-08-20 17:56:50 +08:00
|
|
|
console.log('onlineList branch exe');
|
2024-08-20 14:04:50 +08:00
|
|
|
break;
|
2024-08-19 15:50:51 +08:00
|
|
|
case 'callRequest':
|
|
|
|
this.need_conform_call(d.from);
|
2024-08-20 17:56:50 +08:00
|
|
|
console.log('callRequest branch exe');
|
2024-08-19 15:50:51 +08:00
|
|
|
break;
|
|
|
|
case 'offer':
|
|
|
|
var offer = new RTCSessionDescription(d.offer);
|
|
|
|
var answer = await this.peerConnection.setRemoteDescription(offer);
|
|
|
|
this.peerConnection.setLocalDescription(answer);
|
|
|
|
this.socket.send(JSON.stringify({
|
|
|
|
type:'answer',
|
|
|
|
answer:answer,
|
2024-08-20 14:04:50 +08:00
|
|
|
to:this.peer_info
|
2024-08-20 14:35:19 +08:00
|
|
|
}));
|
2024-08-20 17:56:50 +08:00
|
|
|
console.log('offer branch exe');
|
|
|
|
break;
|
2024-08-19 15:50:51 +08:00
|
|
|
case 'answer':
|
2024-08-20 14:04:50 +08:00
|
|
|
if (d.from == self.peer_info){
|
2024-08-20 17:56:50 +08:00
|
|
|
var desc = new RTCSessionDescription(d.answer);
|
|
|
|
this.peerConnection.setRemoteDescription(desc);
|
2024-08-19 15:50:51 +08:00
|
|
|
}
|
2024-08-20 17:56:50 +08:00
|
|
|
console.log('answer branch exe');
|
2024-08-19 15:50:51 +08:00
|
|
|
break;
|
|
|
|
case 'iceCandidate':
|
2024-08-20 14:04:50 +08:00
|
|
|
if (d.from == self.peer_info){
|
2024-08-20 17:56:50 +08:00
|
|
|
var candidate = new RTCIceCandidate(d.candidate);
|
|
|
|
this.peerConnection.addIceCandidate(candidate);
|
2024-08-19 15:50:51 +08:00
|
|
|
}
|
2024-08-20 17:56:50 +08:00
|
|
|
console.log('iceCandidate branch exe');
|
2024-08-19 15:50:51 +08:00
|
|
|
break;
|
|
|
|
case 'callAccepted':
|
2024-08-20 14:04:50 +08:00
|
|
|
await this.createPeerConnection(this.peer_info);
|
2024-08-20 17:56:50 +08:00
|
|
|
console.log('callAccepted branch exe');
|
2024-08-19 15:50:51 +08:00
|
|
|
break;
|
|
|
|
case 'callRejected':
|
2024-08-20 14:04:50 +08:00
|
|
|
this.peer_info = null;
|
2024-08-20 17:56:50 +08:00
|
|
|
console.log('callRejected branch exe');
|
2024-08-19 15:50:51 +08:00
|
|
|
break;
|
|
|
|
case 'disconnect':
|
|
|
|
this.call_close(true);
|
2024-08-20 17:56:50 +08:00
|
|
|
console.log('disconnect branch exe');
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
console.log(d.type, 'no branch defined');
|
2024-08-19 15:50:51 +08:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 创建 PeerConnection
|
2024-08-20 14:04:50 +08:00
|
|
|
async createPeerConnection(to_info) {
|
|
|
|
this.phone.set_state('using');
|
2024-08-19 15:50:51 +08:00
|
|
|
if (this.role == 'requester'){
|
|
|
|
await this.send_offer();
|
|
|
|
}
|
2024-08-20 14:04:50 +08:00
|
|
|
const configuration = {
|
2024-08-19 15:50:51 +08:00
|
|
|
iceServers: [
|
|
|
|
{ urls: this.ice_url },
|
|
|
|
{ urls: 'stun:stunserver2024.stunprotocol.org:3478' }
|
|
|
|
]
|
2024-08-20 14:04:50 +08:00
|
|
|
};
|
|
|
|
this.peerConnection = new RTCPeerConnection(configuration);
|
|
|
|
// 处理本地流添加到 PeerConnection
|
|
|
|
this.localVideo
|
|
|
|
.get_stream()
|
|
|
|
.getTracks()
|
|
|
|
.forEach(track => {
|
2024-08-20 14:38:31 +08:00
|
|
|
this.peerConnection.addTrack(track, this.localVideo.stream);
|
|
|
|
});
|
2024-08-19 15:50:51 +08:00
|
|
|
|
2024-08-20 14:04:50 +08:00
|
|
|
// 处理 ICE 候选
|
|
|
|
this.peerConnection.onicecandidate = this.send_candidate.bind(this, to_info);
|
2024-08-19 15:50:51 +08:00
|
|
|
|
2024-08-20 14:04:50 +08:00
|
|
|
// 处理远程流添加
|
|
|
|
this.peerConnection.ontrack = event => {
|
|
|
|
this.remoteVideo.set_stream(event.streams[0]);
|
|
|
|
};
|
2024-08-19 15:50:51 +08:00
|
|
|
}
|
|
|
|
}
|
2024-08-20 14:43:28 +08:00
|
|
|
|
|
|
|
bricks.Factory.register('RTCClient', bricks.RTCClient);
|