diff --git a/bricks/build.sh b/bricks/build.sh index b8f7da4..5acde74 100755 --- a/bricks/build.sh +++ b/bricks/build.sh @@ -6,7 +6,7 @@ SOURCES=" page_data_loader.js factory.js uitypesdef.js utils.js uitype.js \ tree.js multiple_state_image.js dynamiccolumn.js form.js message.js conform.js \ paging.js datagrid.js iframe.js cols.js echartsext.js \ floaticonbar.js miniform.js wterm.js dynamicaccordion.js \ - binstreaming.js streaming_audio.js vadtext.js \ + binstreaming.js streaming_audio.js vadtext.js rtc.js \ llm_dialog.js llm.js websocket.js datarow.js tabular.js \ line.js pie.js bar.js gobang.js " echo ${SOURCES} diff --git a/bricks/form.js b/bricks/form.js index 9eb97a9..d245c94 100644 --- a/bricks/form.js +++ b/bricks/form.js @@ -83,7 +83,7 @@ bricks.FormBody = class extends bricks.VScrollPanel { this.fg.build_fields(form, this, form.opts.fields) } create(){ - this.dom_element = this._create('form') + this.dom_element = this._create('form'); } } @@ -299,6 +299,7 @@ bricks.Form = class extends bricks.FormBase { i18n:true}); this.add_widget(d); } + this.set_css('vcontainer'); var filler = new bricks.Filler({}); this.add_widget(filler); this.body = new bricks.FormBody(this, opts); diff --git a/bricks/modal.js b/bricks/modal.js index 3bb733b..acedc10 100755 --- a/bricks/modal.js +++ b/bricks/modal.js @@ -192,6 +192,7 @@ bricks.ModalForm = class extends bricks.Modal { } build_form(){ var opts = { + height:'100%', title:this.opts.title, description:this.opts.description, fields:this.opts.fields diff --git a/bricks/rtc.js b/bricks/rtc.js new file mode 100644 index 0000000..3cbce6f --- /dev/null +++ b/bricks/rtc.js @@ -0,0 +1,187 @@ +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": + "rtcid": + "rtcname" + } + */ + constructor(opts){ + super(opts); + this.localVideo = new bricks.VideoBox({}); + this.remoteVideo = new bricks.VideoBox({}); + this.peerConnection; + this.socket = new WebSocket(this.signaling_url); + this.socket.onopen = this.register_myself.bind(this); + this.socket.onclose = null; + this.socket.onerror = null; + this.socket.onmessage = this.signaling_message_handle.bind(this); + } + this.signaling_register(){ + var d = { + type:'register', + data:{ + rtcid:this.rtcid, + rtcname:this.rtcname + } + } + this.socket.send(JSON.stringify(d)); + } + // 获取本地媒体流 + async function getLocalStream() { + try { + const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); + this.localVideo.set_stream(stream); + return stream; + } catch (error) { + console.error('获取本地媒体流失败:', error); + } + } + async call_request(peer_id){ + var d = { + type:'callrequest', + rtcid:peer_id + } + this.peer_id = peer_id; + 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, + to:this.peer_id + })); + } + send_candidate(event){ + if (event.candidate) { + this.socket.send(JSON.stringify({ + type: 'iceCandidate', + to:this.peer_id, + candidate: event.candidate + })); + } + } + async need_conform_call(peer_id){ + var w = new Conform({title:'请确认', + message:'来自' + peer_id + '的连接请求'); + w.bind('conformed', this.call_accepted.bind(this, peer_id)); + w.bind('cancelled', this.call_hungup.bind(this.peer_id)); + w.open(); + } + call_accepted(peer_id){ + if (this.peer_id){ + this.call_close(); + } + this.peer_id = peer_id + this.role = 'responser' + async this.createPeerConnection(); + this.socket.send(JSON.stringify({ + type: 'callAccepted', + to: peer_id + })); + } + async call_close(active){ + this.peerConnection.onicecandidate = null; + this.peerConnection.ontrack = null; + this.peerConnection.close(); + if (! active){ + this.socket.send(JSON.stringify({ + type:'disconnect', + to:this.peer_id + }); + } + this.peer_id = null; + this.localVideo.get_stream().getTracks().forEach(track => track.stop()); + } + call_hangup(peer_id){ + this.socket.send(JSON.stringify({ + type: 'callRejected', + to: peer_id + })); + } + async signaling_message_handle(event){ + var d = event.data; + switch (d.type){ + case 'callRequest': + this.need_conform_call(d.from); + 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, + to:this.peer_id + }); + case 'answer': + if (d.from == self.peer_id){ + var desc = new RTCSessionDescription(d.answer)); + this.peerConnection.setRemoteDescription(desc); + } + break; + case 'iceCandidate': + if (d.from == self.peer_id){ + var candidate = new RTCIceCandidate(d.candidate); + this.peerConnection.addIceCandidate(candidate); + } + break; + case 'callAccepted': + await this.createPeerConnection(this.peer_id); + break; + case 'callRejected': + this.peer_id = null; + break; + case 'disconnect': + this.call_close(true); + break; + } + } + + // 创建 PeerConnection + async createPeerConnection(to_id) { + if (this.role == 'requester'){ + await this.send_offer(); + } + const configuration = { + // iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] + iceServers: [ + { urls: this.ice_url }, + { urls: 'stun:stunserver2024.stunprotocol.org:3478' } + ] + }; + this.peerConnection = new RTCPeerConnection(configuration); + + // 处理本地流添加到 PeerConnection + this.localVideo.stream.getTracks().forEach(track => this.peerConnection.addTrack(track, this.localVideo.stream)); + + // 处理 ICE 候选 + this.peerConnection.onicecandidate = this.send_candidate.bind(this, to_id); + + // 处理远程流添加 + this.peerConnection.ontrack = event => { + this.remoteVideo.set_stream(event.streams[0]); + }; + } +}