From ab8646a21fb9880600a12bec5fec0eab8da9253b Mon Sep 17 00:00:00 2001 From: acgist <289547414@qq.com> Date: Sun, 12 Mar 2023 12:29:53 +0800 Subject: [PATCH] =?UTF-8?q?[*]=20=E8=B8=A2=E4=BA=BA=20=E6=8B=89=E4=BA=BA?= =?UTF-8?q?=20=E5=88=87=E6=8D=A2=E8=A7=86=E9=A2=91=E5=AA=92=E4=BD=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/Learning.md | 1 + taoyao-client-media/src/Server.js | 2 + taoyao-client-media/src/Taoyao.js | 343 +++++- taoyao-client-web/src/App.vue | 51 +- .../src/components/LocalClient.vue | 35 +- .../src/components/RemoteClient.vue | 15 +- taoyao-client-web/src/components/Taoyao.js | 1005 +++++++++++++---- .../taoyao/boot/annotation/Description.java | 4 - .../taoyao/boot/annotation/Prototype.java | 25 - .../acgist/taoyao/boot/config/Constant.java | 16 + .../src/main/resources/application.yml | 2 + .../signal/party/media/DataConsumer.java | 2 +- .../taoyao/signal/party/media/Room.java | 23 + .../signal/party/media/RoomManager.java | 14 + .../protocol/ProtocolClientAdapter.java | 8 +- .../protocol/ProtocolControlAdapter.java | 14 +- .../signal/protocol/ProtocolRoomAdapter.java | 4 +- .../protocol/media/MediaConsumeProtocol.java | 8 +- .../media/MediaConsumerCloseProtocol.java | 1 + .../media/MediaConsumerPauseProtocol.java | 1 + .../MediaConsumerRequestKeyFrameProtocol.java | 49 +- .../media/MediaConsumerResumeProtocol.java | 5 +- ...diaConsumerSetPreferredLayersProtocol.java | 54 +- .../MediaConsumerSetPriorityProtocol.java | 42 +- .../media/MediaDataConsumeProtocol.java | 93 +- .../media/MediaDataConsumerCloseProtocol.java | 1 + .../MediaDataConsumerStatusProtocol.java | 42 +- .../media/MediaDataProduceProtocol.java | 72 +- .../media/MediaDataProducerCloseProtocol.java | 1 + .../MediaDataProducerStatusProtocol.java | 42 +- .../media/MediaIceRestartProtocol.java | 26 +- .../protocol/media/MediaProduceProtocol.java | 4 +- .../media/MediaProducerCloseProtocol.java | 1 + .../media/MediaProducerPauseProtocol.java | 1 + .../media/MediaProducerResumeProtocol.java | 1 + .../MediaTransportWebRtcCreateProtocol.java | 1 + .../MediaVideoOrientationChangeProtocol.java | 38 +- .../protocol/room/RoomExpelProtocol.java | 40 +- .../protocol/room/RoomInviteProtocol.java | 44 +- 39 files changed, 1779 insertions(+), 352 deletions(-) delete mode 100644 taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/annotation/Prototype.java diff --git a/docs/Learning.md b/docs/Learning.md index cb9df3c..fde19af 100644 --- a/docs/Learning.md +++ b/docs/Learning.md @@ -28,6 +28,7 @@ https://www.cnblogs.com/ssyfj/p/14843082.html https://zhuanlan.zhihu.com/p/466172240 http://koca.szkingdom.com/forum/t/topic/218 +https://blog.csdn.net/qw225967/article/details/121251305 http://www.manoner.com/post/音视频基础/WebRTC核心组件和协议栈/ https://blog.csdn.net/ababab12345/article/details/115585378 https://blog.csdn.net/jisuanji111111/article/details/121634199 diff --git a/taoyao-client-media/src/Server.js b/taoyao-client-media/src/Server.js index db96572..a7ffc0d 100644 --- a/taoyao-client-media/src/Server.js +++ b/taoyao-client-media/src/Server.js @@ -82,6 +82,8 @@ async function main() { console.log(` 桃之夭夭,灼灼其华。 之子于归,宜其室家。 + + :: https://gitee.com/acgist/taoyao `); console.info("开始启动:", config.name); await buildMediasoupWorkers(); diff --git a/taoyao-client-media/src/Taoyao.js b/taoyao-client-media/src/Taoyao.js index 81de5b2..0a49fc7 100644 --- a/taoyao-client-media/src/Taoyao.js +++ b/taoyao-client-media/src/Taoyao.js @@ -1,5 +1,5 @@ -const config = require("./Config"); -const process = require("child_process"); +const config = require("./Config.js"); +const process = require("child_process"); const WebSocket = require("ws"); /** @@ -21,34 +21,40 @@ const protocol = { } const date = new Date(); return ( - 100000000000000 * date.getDate() + - 1000000000000 * date.getHours() + - 10000000000 * date.getMinutes() + - 100000000 * date.getSeconds() + - 1000 * this.clientIndex + + 100000000000000 * date.getDate() + + 1000000000000 * date.getHours() + + 10000000000 * date.getMinutes() + + 100000000 * date.getSeconds() + + 1000 * this.clientIndex + this.index ); }, /** * @param {*} signal 信令标识 - * @param {*} body 消息主体 - * @param {*} id 消息ID + * @param {*} body 消息主体 + * @param {*} id 消息ID + * @param {*} v 消息版本 * * @returns 信令消息 */ - buildMessage(signal, body = {}, id) { + buildMessage(signal, body = {}, id, v) { const message = { header: { - v: config.signal.version, - id: id || this.buildId(), + v: v || config.signal.version, + id: id || this.buildId(), signal: signal, }, - body: body, + body: body, }; return message; }, }; +/** + * 名称冲突 + */ + const taoyaoProtocol = protocol; + /** * 信令通道 */ @@ -404,9 +410,30 @@ class Taoyao { case "media::consumer::pause": me.mediaConsumerPause(message, body); break; + case "media::consumer::request::key::frame": + me.mediaConsumerRequestKeyFrame(message, body); + break; case "media::consumer::resume": me.mediaConsumerResume(message, body); break; + case "media::consumer::set::preferred::layers": + me.mediaConsumerSetPreferredLayers(message, body); + break; + case "media::data::consume": + me.mediaDataConsume(message, body); + break; + case "media::data::consumer::close": + me.mediaDataConsumerClose(message, body); + break; + case "media::data::produce": + me.mediaDataProduce(message, body); + break; + case "media::data::producer::close": + me.mediaDataProducerClose(message, body); + break; + case "media::ice::restart": + me.mediaIceRestart(message, body); + break; case "media::produce": me.mediaProduce(message, body); break; @@ -576,6 +603,7 @@ class Taoyao { kind, appData, rtpParameters, + // 关键帧延迟时间 // keyFrameRequestDelay: 5000 }); producer.clientId = clientId; @@ -597,10 +625,15 @@ class Taoyao { }); producer.on("videoorientationchange", (videoOrientation) => { console.info("producer videoorientationchange:", producer.id, videoOrientation); + self.push(protocol.buildMessage("media::video::orientation::change", { + roomId: roomId, + ...videoOrientation + })); }); - producer.on("trace", (trace) => { - console.info("producer trace:", producer.id, trace); - }); + // await producer.enableTraceEvent([ 'rtp', 'keyframe', 'nack', 'pli', 'fir' ]); + // producer.on("trace", (trace) => { + // console.info("producer trace:", producer.id, trace); + // }); producer.observer.on("close", () => { if(room.producers.delete(producer.id)) { console.info("producer close:", producer.id); @@ -635,7 +668,7 @@ class Taoyao { // producer.observer.on("score", fn(score)); // producer.observer.on("videoorientationchange", fn(videoOrientation)); // producer.observer.on("trace", fn(trace)); - message.body = { kind: kind, producerId: producer.id }; + message.body = { kind: kind, roomId: roomId, producerId: producer.id }; this.push(message); if (producer.kind === "audio") { room.audioLevelObserver @@ -725,8 +758,8 @@ class Taoyao { rtpCapabilities, } = body; const room = this.rooms.get(roomId); - const producer = room.producers.get(producerId); - const transport = room.transports.get(transportId); + const producer = room?.producers.get(producerId); + const transport = room?.transports.get(transportId); if ( !room || !producer || @@ -822,9 +855,10 @@ class Taoyao { }) ); }); - consumer.on("trace", (trace) => { - console.info("consumer trace:", consumer.id, trace); - }); + // await consumer.enableTraceEvent([ 'rtp', 'keyframe', 'nack', 'pli', 'fir' ]); + // consumer.on("trace", (trace) => { + // console.info("consumer trace:", consumer.id, trace); + // }); // consumer.on("rtp", (rtpPacket) => { // console.info("consumer rtp:", consumer.id, rtpPacket); // }); @@ -863,18 +897,19 @@ class Taoyao { // consumer.observer.on("layerschange", fn(layers)); // consumer.observer.on("trace", fn(trace)); // 等待终端准备就绪 - this.request( + await this.request( + // this.push( protocol.buildMessage("media::consume", { kind: consumer.kind, type: consumer.type, roomId: roomId, + appData: producer.appData, clientId: clientId, sourceId: sourceId, streamId: streamId, producerId: producerId, consumerId: consumer.id, rtpParameters: consumer.rtpParameters, - appData: producer.appData, producerPaused: consumer.producerPaused, }) ); @@ -924,7 +959,7 @@ class Taoyao { async mediaConsumerPause(message, body) { const { roomId, consumerId } = body; const room = this.rooms.get(roomId); - const consumer = room.consumers.get(consumerId); + const consumer = room?.consumers.get(consumerId); if(consumer) { console.info("暂停消费者:", consumerId); await consumer.pause(); @@ -933,6 +968,27 @@ class Taoyao { } } + /** + * 请求关键帧信令 + * + * @param {*} message 消息 + * @param {*} body 消息主体 + */ + async mediaConsumerRequestKeyFrame(message, body) { + const me = this; + const { roomId, consumerId } = body; + const room = this.rooms.get(roomId); + const consumer = room?.consumers.get(consumerId); + if(consumer) { + console.info("mediaConsumerRequestKeyFrame:", consumerId); + // 处理trace监听读取关键帧 + await consumer.requestKeyFrame(); + me.push(message); + } else { + console.info("mediaConsumerRequestKeyFrame non:", consumerId); + } + } + /** * 恢复消费者信令 * @@ -951,6 +1007,220 @@ class Taoyao { } } + /** + * 修改最佳空间层和时间层信令 + * + * @param {*} message 消息 + * @param {*} body 消息主体 + */ + async mediaConsumerSetPreferredLayers(message, body) { + const me = this; + const { roomId, consumerId, spatialLayer, temporalLayer } = body; + const room = this.rooms.get(roomId); + const consumer = room?.consumers.get(consumerId); + if(consumer) { + console.info("mediaConsumerSetPreferredLayers:", consumerId); + await consumer.setPreferredLayers({ spatialLayer, temporalLayer }); + me.push(message); + } else { + console.info("mediaConsumerSetPreferredLayers non:", consumerId); + } + } + + /** + * 消费数据信令 + * + * @param {*} message 消息 + * @param {*} body 消息主体 + */ + async mediaDataConsume(message, body) { + const me = this; + const { + roomId, + clientId, + sourceId, + streamId, + producerId, + transportId, + rtpCapabilities, + } = body; + const room = this.rooms.get(roomId); + const transport = room?.transports.get(transportId); + const dataProducer = room?.dataProducers.get(producerId); + if ( + !room || + !transport || + !dataProducer + ) { + console.warn( + "不能消费数据:", + roomId, + clientId, + producerId, + transportId + ); + return; + } + let dataConsumer; + try { + dataConsumer = await transport.consumeData({ + dataProducerId : dataProducer.id + }); + } catch (error) { + console.error("消费数据异常:", producerId, error); + return; + } + dataConsumer.clientId = clientId; + dataConsumer.streamId = streamId; + room.dataConsumers.set(dataConsumer.id, dataConsumer); + console.info("创建数据消费者:", dataProducer.id); + dataConsumer.on('transportclose', () => { + console.info("dataConsumer transportclose:", dataConsumer.id); + dataConsumer.close(); + }); + dataConsumer.on('dataproducerclose', () => { + console.info("dataConsumer dataproducerclose:", dataConsumer.id); + dataConsumer.close(); + }); + dataConsumer.observer.on("close", () => { + if(room.dataConsumers.delete(dataConsumer.id)) { + console.info("dataConsumer close:", dataConsumer.id); + me.push( + protocol.buildMessage("media::data::consumer::close", { + roomId: roomId, + consumerId: dataConsumer.id, + }) + ); + } else { + console.info("dataConsumer close non:", dataConsumer.id); + } + }); + // dataConsumer.on("message", fn(message, ppid)); + // dataConsumer.on("bufferedamountlow", fn(bufferedAmount)); + // dataConsumer.on("sctpsendbufferfull", fn()); + this.push( + protocol.buildMessage("media::data::consume", { + label: dataConsumer.label, + roomId: roomId, + appData: dataProducer.appData, + protocol: dataConsumer.protocol, + clientId: clientId, + sourceId: sourceId, + streamId: streamId, + producerId: producerId, + consumerId: dataConsumer.id, + sctpStreamParameters: dataConsumer.sctpStreamParameters, + }) + ); + } + + /** + * 关闭数据消费者信令 + * + * @param {*} message 消息 + * @param {*} body 消息主体 + */ + async mediaDataConsumerClose(message, body) { + const { roomId, consumerId } = body; + const room = this.rooms.get(roomId); + const dataConsumer = room?.dataConsumers.get(consumerId); + if(dataConsumer) { + console.info("关闭数据消费者:", consumerId); + await dataConsumer.close(); + } else { + console.info("关闭数据消费者无效:", consumerId); + } + } + + /** + * 生产数据信令 + * + * @param {*} message 消息 + * @param {*} body 消息主体 + */ + async mediaDataProduce(message, body) { + const me = this; + const { + label, + roomId, + appData, + clientId, + streamId, + protocol, + transportId, + sctpStreamParameters, + } = body; + const room = me.rooms.get(roomId); + const transport = room?.transports.get(transportId); + if(!transport) { + console.warn("生产数据生产者通道无效:", transportId); + return; + } + const dataProducer = await transport.produceData({ + label, + appData, + protocol, + sctpStreamParameters, + }); + dataProducer.clientId = clientId; + dataProducer.streamId = streamId; + room.dataProducers.set(dataProducer.id, dataProducer); + console.info("创建数据生产者:", dataProducer.id); + dataProducer.on("transportclose", () => { + console.info("dataProducer transportclose:", dataProducer.id); + dataProducer.close(); + }); + dataProducer.observer.on("close", () => { + if(room.dataProducers.delete(dataProducer.id)) { + console.info("dataProducer close:", dataProducer.id); + me.push( + taoyaoProtocol.buildMessage("media::data::producer::close", { + roomId: roomId, + producerId: dataProducer.id, + }) + ); + } else { + console.info("dataProducer close non:", dataProducer.id); + } + }) + message.body = { roomId: roomId, producerId: dataProducer.id }; + this.push(message); + } + + /** + * 关闭数据生产者信令 + * + * @param {*} message 消息 + * @param {*} body 消息主体 + */ + async mediaDataProducerClose(message, body) { + const { roomId, producerId } = body; + const room = this.rooms.get(roomId); + const dataProducer = room?.dataProducers.get(producerId); + if(dataProducer) { + console.info("关闭数据生产者:", producerId); + await dataProducer.close(); + } else { + console.info("关闭数据生产者无效:", producerId); + } + } + + /** + * 重启ICE信令 + * + * @param {*} message 消息 + * @param {*} body 消息主体 + */ + async mediaIceRestart(message, body) { + const me = this; + const { roomId, transportId } = body; + const room = this.rooms.get(roomId); + const transport = room?.transports.get(transportId); + const iceParameters = await transport.restartIce(); + message.body.iceParameters = iceParameters; + me.push(message); + } + /** * 路由RTP协商信令 * @@ -973,7 +1243,7 @@ class Taoyao { async mediaTransportClose(message, body) { const { roomId, transportId } = body; const room = this.rooms.get(roomId); - const transport = room.transports.get(transportId); + const transport = room?.transports.get(transportId); if(transport) { console.info("关闭传输通道:", transportId); transport.close(); @@ -991,10 +1261,15 @@ class Taoyao { async mediaTransportWebrtcConnect(message, body) { const { roomId, transportId, dtlsParameters } = body; const room = this.rooms.get(roomId); - const transport = room.transports.get(transportId); - await transport.connect({ dtlsParameters }); - message.body = { roomId: roomId, transportId: transport.id }; - this.push(message); + const transport = room?.transports.get(transportId); + if(transport) { + console.info("连接WebRTC通道:", transportId); + await transport.connect({ dtlsParameters }); + message.body = { roomId: roomId, transportId: transport.id }; + this.push(message); + } else { + console.info("连接WebRTC通道无效:", transportId); + } } /** @@ -1038,11 +1313,10 @@ class Taoyao { console.info("transport listenserverclose:", transport.id); transport.close(); }); - await transport.enableTraceEvent(["bwe"]); // await transport.enableTraceEvent([ 'probation', 'bwe' ]); - transport.on("trace", (trace) => { - console.debug("transport trace:", transport.id, trace); - }); + // transport.on("trace", (trace) => { + // console.debug("transport trace:", transport.id, trace); + // }); transport.observer.on("close", () => { if(room.transports.delete(transport.id)) { console.info("transport close:", transport.id); @@ -1100,6 +1374,7 @@ class Taoyao { // transport.on("rtcp", fn(rtcpPacket)); room.transports.set(transport.id, transport); message.body = { + roomId: roomId, transportId: transport.id, iceCandidates: transport.iceCandidates, iceParameters: transport.iceParameters, diff --git a/taoyao-client-web/src/App.vue b/taoyao-client-web/src/App.vue index 7807e53..b82ede7 100644 --- a/taoyao-client-web/src/App.vue +++ b/taoyao-client-web/src/App.vue @@ -3,7 +3,7 @@
{{ client?.name || "" }}
@@ -69,8 +69,12 @@ export default { } }, methods: { + roomExpel() { + this.taoyao.roomExpel(this.client.clientId); + }, media(track, consumer) { if(track.kind === 'audio') { + this.audioConsumer = consumer; if (this.audioStream) { // TODO:资源释放 } else { @@ -80,6 +84,7 @@ export default { } this.audio.play().catch((error) => console.warn("视频播放失败", error)); } else if(track.kind === 'video') { + this.videoConsumer = consumer; if (this.videoStream) { // TODO:资源释放 } else { diff --git a/taoyao-client-web/src/components/Taoyao.js b/taoyao-client-web/src/components/Taoyao.js index 8af96e1..b52bad0 100644 --- a/taoyao-client-web/src/components/Taoyao.js +++ b/taoyao-client-web/src/components/Taoyao.js @@ -26,38 +26,40 @@ const protocol = { } const date = new Date(); return ( - 100000000000000 * date.getDate() + - 1000000000000 * date.getHours() + - 10000000000 * date.getMinutes() + - 100000000 * date.getSeconds() + - 1000 * this.clientIndex + + 100000000000000 * date.getDate() + + 1000000000000 * date.getHours() + + 10000000000 * date.getMinutes() + + 100000000 * date.getSeconds() + + 1000 * this.clientIndex + this.index ); }, /** * @param {*} signal 信令标识 - * @param {*} body 消息主体 - * @param {*} id 消息ID - * @param {*} v 消息版本 + * @param {*} body 消息主体 + * @param {*} id 消息ID + * @param {*} v 消息版本 * * @returns 信令消息 */ buildMessage(signal, body = {}, id, v) { - if (!signal) { - throw new Error("信令标识缺失"); - } const message = { header: { - v: v || "1.0.0", - id: id || this.buildId(), + v: v || "1.0.0", + id: id || this.buildId(), signal: signal, }, - body: body, + body: body, }; return message; }, }; +/** + * 名称冲突 + */ +const taoyaoProtocol = protocol; + /** * 信令通道 */ @@ -236,12 +238,6 @@ class RemoteClient { volume = 0; // 代理对象 proxy; - // 数据可用 - dataActive = false; - // 音频可用 - audioActive = false; - // 视频可用 - videoActive = false; // 数据消费者 dataConsumer; // 音频消费者 @@ -328,7 +324,7 @@ class Taoyao extends RemoteClient { dataProduce; // 是否生产音频 audioProduce; - // 是否生成视频 + // 是否生产视频 videoProduce; // 数据生产者 dataProducer; @@ -336,8 +332,10 @@ class Taoyao extends RemoteClient { audioProducer; // 视频生产者 videoProducer; - // 消费者:音频、视频、数据 + // 消费者:音频、视频 consumers = new Map(); + // 消费者:数据 + dataConsumers = new Map(); // 远程终端 remoteClients = new Map(); @@ -450,7 +448,7 @@ class Taoyao extends RemoteClient { * 2. 执行前置回调 * 3. 如果注册全局回调,同时执行结果返回true不再执行后面所有回调。 * 4. 执行后置回调 - * + * * @param {*} message 消息 */ async on(message) { @@ -494,6 +492,9 @@ class Taoyao extends RemoteClient { case "media::consume": await me.defaultMediaConsume(message); break; + case "media::data::consume": + me.defaultMediaDataConsume(message); + break; case "platform::error": me.defaultPlatformError(message); break; @@ -519,6 +520,45 @@ class Taoyao extends RemoteClient { case "media::consumer::close": me.defaultMediaConsumerClose(message); break; + case "media::consumer::pause": + this.defaultMediaConsumerPause(message); + break; + case "media::consumer::request::key::frame": + me.defaultMediaConsumerRequestKeyFrame(message); + break; + case "media::consumer::resume": + this.defaultMediaConsumerResume(message); + break; + case "media::consumer::set::preferred::layers": + me.defaultMediaConsumerSetPreferredLayers(message); + break; + case "media::consumer::status": + this.defaultMediaConsumerStatus(message); + break; + case "media::data::consumer::close": + me.defaultMediaDataConsumerClose(message); + break; + case "media::data::consumer::status": + me.defaultMediaDataConsumerStatus(message); + break; + case "media::data::producer::close": + me.defaultMediaDataProducerClose(message); + break; + case "media::data::producer::status": + me.defaultMediaDataProducerStatus(message); + break; + case "media::producer::close": + me.defaultMediaProducerClose(message); + break; + case "media::producer::pause": + me.defaultMediaProducerPause(message); + break; + case "media::producer::resume": + me.defaultMediaProducerResume(message); + break; + case "media::video::orientation::change": + me.defaultMediaVideoOrientationChange(message); + break; case "room::client::list": me.defaultRoomClientList(message); break; @@ -528,6 +568,12 @@ class Taoyao extends RemoteClient { case "room::enter": me.defaultRoomEnter(message); break; + case "room::expel": + me.defaultRoomExpel(message); + break; + case "room::invite": + me.defaultRoomInvite(message); + break; case "room::leave": me.defaultRoomLeave(message); break; @@ -536,6 +582,58 @@ class Taoyao extends RemoteClient { break; } } + /************************ 管理 ************************/ + getProducer(producerId) { + const me = this; + if(me?.audioProducer.id === producerId) { + return me.audioProducer; + } else if(me?.videoProducer.id === producerId) { + return me.videoProducer; + } else if(me?.dataProducer.id === producerId) { + return me.dataProducer; + } else { + return null; + } + } + async getVideoTrack() { + let track; + const self = this; + if (self.videoSource === "file") { + // TODO:实现文件分享 + // const stream = await this._getExternalVideoStream(); + // track = stream.getVideoTracks()[0].clone(); + } else if (self.videoSource === "camera") { + console.debug("enableWebcam() | calling getUserMedia()"); + // TODO:参数 + const stream = await navigator.mediaDevices.getUserMedia({ + video: self.videoConfig, + }); + track = stream.getVideoTracks()[0]; + // TODO:验证修改API videoTrack.applyCapabilities + console.debug( + "视频信息:", + track.getSettings(), + track.getCapabilities() + ); + } else if (self.videoSource === "screen") { + const stream = await navigator.mediaDevices.getDisplayMedia({ + // 如果需要共享声音 + audio: false, + video: { + cursor: true, + width: { max: 1920 }, + height: { max: 1080 }, + frameRate: { max: 30 }, + logicalSurface: true, + displaySurface: "monitor", + }, + }); + track = stream.getVideoTracks()[0]; + } else { + // TODO:异常 + } + return track; + } /************************ 信令 ************************/ /** * 终端配置信令 @@ -546,14 +644,40 @@ class Taoyao extends RemoteClient { const me = this; const { media, webrtc } = message.body; const { audio, video } = media; - me.audioConfig.sampleSize = { min: media.minSampleSize, ideal: audio.sampleSize, max: media.maxSampleSize }; - me.audioConfig.sampleRate = { min: media.minSampleRate, ideal: audio.sampleRate, max: media.maxSampleRate }; - me.videoConfig.width = { min: media.minWidth, ideal: video.width, max: media.maxWidth }; - me.videoConfig.height = { min: media.minHeight, ideal: video.height, max: media.maxHeight }; - me.videoConfig.frameRate = { min: media.minFrameRate, ideal: video.frameRate, max: media.maxFrameRate }; + me.audioConfig.sampleSize = { + min: media.minSampleSize, + ideal: audio.sampleSize, + max: media.maxSampleSize, + }; + me.audioConfig.sampleRate = { + min: media.minSampleRate, + ideal: audio.sampleRate, + max: media.maxSampleRate, + }; + me.videoConfig.width = { + min: media.minWidth, + ideal: video.width, + max: media.maxWidth, + }; + me.videoConfig.height = { + min: media.minHeight, + ideal: video.height, + max: media.maxHeight, + }; + me.videoConfig.frameRate = { + min: media.minFrameRate, + ideal: video.frameRate, + max: media.maxFrameRate, + }; me.mediaConfig = media; me.webrtcConfig = webrtc; - console.debug("终端配置:", me.audioConfig, me.videoConfig, me.mediaConfig, me.webrtcConfig); + console.debug( + "终端配置:", + me.audioConfig, + me.videoConfig, + me.mediaConfig, + me.webrtcConfig + ); } /** * 重启终端信令 @@ -566,10 +690,10 @@ class Taoyao extends RemoteClient { } /** * 终端注册信令 - * + * * @param {*} message 消息 */ - defaultClientRegister(message) { + defaultClientRegister(message) { const { index } = message.body; protocol.clientIndex = index; } @@ -584,26 +708,26 @@ class Taoyao extends RemoteClient { } /** * 终端音量信令 - * + * * @param {*} message 消息 */ defaultMediaAudioVolume(message) { const me = this; const { roomId, volumes } = message.body; // 静音 - if(!volumes || !volumes.length) { + if (!volumes || !volumes.length) { me.volume = 0; - me.remoteClients.forEach(v => v.volume = 0); + me.remoteClients.forEach((v) => (v.volume = 0)); return; } // 声音 - volumes.forEach(v => { + volumes.forEach((v) => { const { volume, clientId } = v; - if(me.clientId === clientId) { + if (me.clientId === clientId) { me.setVolume(volume); } else { const remoteClient = me.remoteClients.get(clientId); - if(remoteClient) { + if (remoteClient) { remoteClient.setVolume(volume); } } @@ -611,26 +735,28 @@ class Taoyao extends RemoteClient { } /** * 关闭消费者信令 - * + * * @param {*} consumerId 消费者ID */ mediaConsumerClose(consumerId) { const me = this; - me.push(protocol.buildMessage("media::consumer::close", { - roomId: me.roomId, - consumerId: consumerId - })); + me.push( + protocol.buildMessage("media::consumer::close", { + roomId: me.roomId, + consumerId: consumerId, + }) + ); } /** * 关闭消费者信令 - * + * * @param {*} message 消息 */ defaultMediaConsumerClose(message) { const me = this; const { roomId, consumerId } = message.body; const consumer = me.consumers.get(consumerId); - if(consumer) { + if (consumer) { console.info("关闭消费者:", consumerId); consumer.close(); me.consumers.delete(consumerId); @@ -638,12 +764,329 @@ class Taoyao extends RemoteClient { console.debug("关闭消费者无效:", consumerId); } } + /** + * 暂停消费者 + * + * @param {*} consumerId 消费者ID + */ + mediaConsumerPause(consumerId) { + const me = this; + const consumer = me.consumers.get(consumerId); + if(consumer) { + if(consumer.paused) { + return; + } + console.debug("mediaConsumerPause:", consumerId); + me.push(protocol.buildMessage("media::consumer::pause", { + roomId: me.roomId, + consumerId: consumerId, + })); + } else { + console.debug("mediaConsumerPause non:", consumerId); + } + } + /** + * 暂停消费者信令 + * + * @param {*} message 消息 + */ + defaultMediaConsumerPause(message) { + const me = this; + const { roomId, consumerId } = message.body; + const consumer = me.consumers.get(consumerId); + if (consumer) { + console.debug("暂停消费者:", consumerId); + consumer.pause(); + } else { + console.debug("暂停消费者无效:", consumerId); + } + } + /** + * 请求关键帧 + * + * @param {*} consumerId 消费者ID + */ + mediaConsumerRequestKeyFrame(consumerId) { + const me = this; + const consumer = me.consumers.get(consumerId); + if(!consumer) { + me.callbackError("消费者无效:" + consumerId); + return; + } + if(consumer.kind !== "video") { + me.callbackError("消费者不是视频媒体:" + consumerId); + return; + } + me.push(protocol.buildMessage("media::consumer::request::key::frame", { + roomId: me.roomId, + consumerId: consumerId, + })); + } + /** + * 请求关键帧信令 + * + * @param {*} message 消息 + */ + defaultMediaConsumerRequestKeyFrame(message) { + console.debug("defaultMediaConsumerRequestKeyFrame:", message); + } + /** + * 恢复消费者 + * + * @param {*} consumerId 消费者ID + */ + mediaConsumerResume(consumerId) { + const me = this; + const consumer = me.consumers.get(consumerId); + if(consumer) { + if(!consumer.paused) { + return; + } + console.debug("mediaConsumerResume:", consumerId); + me.push(protocol.buildMessage("media::consumer::resume", { + roomId: me.roomId, + consumerId: consumerId, + })); + } else { + console.debug("mediaConsumerResume non:", consumerId); + } + } + /** + * 恢复消费者信令 + * + * @param {*} message 消息 + */ + defaultMediaConsumerResume(message) { + const me = this; + const { roomId, consumerId } = message.body; + const consumer = me.consumers.get(consumerId); + if (consumer) { + console.info("恢复消费者:", consumerId); + consumer.resume(); + } else { + console.debug("恢复消费者无效:", consumerId); + } + } + /** + * 修改最佳空间层和时间层 + * + * @param {*} consumerId 消费者ID + * @param {*} spatialLayer 空间层 + * @param {*} temporalLayer 时间层 + */ + mediaConsumerSetPreferredLayers(consumerId, spatialLayer, temporalLayer) { + const me = this; + const consumer = me.consumers.get(consumerId); + if(!consumer) { + me.callbackError("消费者无效:" + consumerId); + return; + } + if(consumer.kind !== "video") { + me.callbackError("消费者不是视频媒体:" + consumerId); + return; + } + me.push(protocol.buildMessage("media::consumer::set::preferred::layers", { + roomId: me.roomId, + consumerId, + spatialLayer, + temporalLayer, + })); + } + /** + * 修改最佳空间层和时间层信令 + * + * @param {*} message 消息 + */ + defaultMediaConsumerSetPreferredLayers(message) { + console.debug("defaultMediaConsumerSetPreferredLayers:", message); + } + /** + * 查询消费者状态信令 + * + * @param {*} message 消息 + */ + defaultMediaConsumerStatus(message) { + console.info("defaultMediaConsumerStatus:", message); + } + /** + * 关闭数据消费者信令 + * + * @param {*} message 消息 + */ + defaultMediaDataConsumerClose(message) { + const me = this; + const { roomId, consumerId } = message.body; + const dataConsumer = me.dataConsumers.get(consumerId); + if (dataConsumer) { + console.info("关闭数据消费者:", consumerId); + dataConsumer.close(); + me.dataConsumers.delete(consumerId); + } else { + console.debug("关闭数据消费者无效:", consumerId); + } + } + /** + * 查询数据消费者状态信令 + * + * @param {*} message 消息 + */ + defaultMediaDataConsumerStatus(message) { + console.info("defaultMediaDataConsumerStatus:", message); + } + /** + * 关闭数据生产者信令 + * + * @param {*} message 消息 + */ + defaultMediaDataProducerClose(message) { + const me = this; + const { roomId, producerId } = message.body; + const producer = me.dataProducer; + if (producer) { + console.info("关闭数据生产者:", producerId); + producer.close(); + // TODO:类型判断设置为空 + } else { + console.debug("关闭数据生产者无效:", producerId); + } + } + /** + * 关闭数据消费者信令 + * + * @param {*} message 消息 + */ + defaultMediaDataProducerStatus(message) { + console.info("defaultMediaDataProducerStatus:", message); + } + /** + * 关闭生产者信令 + * + * @param {*} message 消息 + */ + async defaultMediaProducerClose(message) { + const me = this; + const { roomId, producerId } = message.body; + const producer = me.getProducer(producerId); + if (producer) { + console.info("关闭生产者:", producerId); + producer.close(); + // TODO:类型判断设置为空 + } else { + console.debug("关闭生产者无效:", producerId); + } + } + /** + * 暂停生产者 + * + * @param {*} producerId 生产者ID + */ + mediaProducerPause(producerId) { + const me = this; + const producer = me.getProducer(producerId); + if(producer) { + if(producer.paused) { + return; + } + console.debug("mediaProducerPause:", producerId); + me.push(protocol.buildMessage("media::producer::pause", { + roomId: me.roomId, + producerId: producerId, + })); + } else { + console.debug("mediaProducerPause non:", producerId); + } + } + /** + * 暂停生产者信令 + * + * @param {*} message 消息 + */ + async defaultMediaProducerPause(message) { + const me = this; + const { roomId, producerId } = message.body; + const producer = me.getProducer(producerId); + if (producer) { + console.debug("暂停生产者:", producerId); + producer.pause(); + } else { + console.debug("暂停生产者无效:", producerId); + } + } + /** + * 恢复生产者 + * + * @param {*} producerId 生产者ID + */ + mediaProducerResume(producerId) { + const me = this; + const producer = me.getProducer(producerId); + if(producer) { + if(!producer.paused) { + return; + } + console.debug("mediaProducerResume:", producerId); + me.push(protocol.buildMessage("media::producer::resume", { + roomId: me.roomId, + producerId: producerId, + })); + } else { + console.debug("mediaProducerResume non:", producerId); + } + } + /** + * 恢复生产者信令 + * + * @param {*} message 消息 + */ + async defaultMediaProducerResume(message) { + const me = this; + const { roomId, producerId } = message.body; + const producer = me.getProducer(producerId); + if (producer) { + console.debug("恢复生产者:", producerId); + producer.resume(); + } else { + console.debug("恢复生产者无效:", producerId); + } + } + /** + * 重启ICE + */ + async mediaIceRestart() { + const me = this; + if (me.sendTransport) { + const response = await me.request(protocol.buildMessage( + 'media::ice::restart', + { transportId: me.sendTransport.id } + )); + const { iceParameters } = response.body; + await me.sendTransport.restartIce({ iceParameters }); + } + if (me.recvTransport) + { + const response = await me.request(protocol.buildMessage( + 'media::ice::restart', + { transportId: me.recvTransport.id } + )); + const { iceParameters } = response; + await me.recvTransport.restartIce({ iceParameters }); + } + + } + /** + * 视频方向变化信令 + * + * @param {*} message 消息 + */ + defaultMediaVideoOrientationChange(message) { + console.debug("视频方向变化信令:", message); + } /** * 消费媒体信令 * * @param {*} message 消息 */ - async defaultMediaConsume(message) { + async defaultMediaConsume(message) { const self = this; if (!self.audioConsume && !self.videoConsume) { console.debug("没有消费媒体"); @@ -708,15 +1151,13 @@ class Taoyao extends RemoteClient { self.push(message); console.debug("远程媒体:", consumer); const remoteClient = self.remoteClients.get(consumer.sourceId); - if(remoteClient && remoteClient.proxy && remoteClient.proxy.media) { + if (remoteClient && remoteClient.proxy && remoteClient.proxy.media) { const track = consumer.track; // TODO:旧的媒体? - if(track.kind === 'audio') { - remoteClient.audioActive = true; + if (track.kind === "audio") { remoteClient.audioTrack = track; remoteClient.audioConsumer = consumer; - } else if(track.kind === 'video') { - remoteClient.videoActive = true; + } else if (track.kind === "video") { remoteClient.videoTrack = track; remoteClient.videoconsumer = consumer; } else { @@ -736,24 +1177,98 @@ class Taoyao extends RemoteClient { } } /** - * 平台异常信令 * + * @param {*} producerId + */ + mediaDataConsume(producerId) { + const me = this; + if(!me.recvTransport) { + me.callbackError("没有连接接收通道"); + return; + } + me.push( + protocol.buildMessage("media::data::consume", { + roomId: me.roomId, + producerId: producerId, + }) + ); + } + /** + * 消费数据信令 + * + * @param {*} message 消息 + */ + async defaultMediaDataConsume(message) { + const me = this; + const { + label, + appData, + protocol, + consumerId, + producerId, + sctpStreamParameters, + } = message.body; + try { + const dataConsumer = await me.recvTransport.consumeData({ + id : consumerId, + dataProducerId : producerId, + label, + appData, + protocol, + sctpStreamParameters, + }); + me.dataConsumers.set(dataConsumer.id, dataConsumer); + dataConsumer.on('transportclose', () => { + console.info("dataConsumer transportclose:", dataConsumer.id); + dataConsumer.close(); + }); + dataConsumer.on('open', () => { + console.info("dataConsumer open:", dataConsumer.id); + window.dataConsumer = dataConsumer; + }); + dataConsumer.on('close', () => { + if(me.dataConsumers.delete(dataConsumer.id)) { + console.info("dataConsumer close:", dataConsumer.id); + me.push( + taoyaoProtocol.buildMessage("media::data::consumer::close", { + roomId: roomId, + consumerId: dataConsumer.id, + }) + ); + } else { + console.info("dataConsumer close non:", dataConsumer.id); + } + }); + dataConsumer.on('error', (error) => { + console.error("dataConsumer error:", dataConsumer.id, error); + dataConsumer.close(); + }); + dataConsumer.on('message', (message) => { + console.info("dataConsume message:", dataConsumer.id, message); + }); + } catch (error) { + console.error("打开数据消费者异常", error); + } + } + /** + * 平台异常信令 + * * @param {*} message 消息 */ defaultPlatformError(message) { const { code } = message; - if(code === "3401") { + if (code === "3401") { signalChannel.close(); } } /** * 房间终端列表信令 - * + * * @param {*} message 消息 */ defaultRoomClientList(message) { const me = this; - message.body.forEach(v => { + message.body.forEach((v) => { if (v.clientId === me.clientId) { // 忽略自己 } else { @@ -764,35 +1279,39 @@ class Taoyao extends RemoteClient { /** * 关闭房间信令 */ - async roomClose() { + async roomClose() { const me = this; - if(!me.roomId) { + if (!me.roomId) { console.warn("房间无效:", me.roomId); return; } - me.push(protocol.buildMessage("room::close", { - roomId: me.roomId - })); + me.push( + protocol.buildMessage("room::close", { + roomId: me.roomId, + }) + ); } /** * 关闭房间信令 - * + * * @param {*} message 消息 */ defaultRoomClose(message) { const me = this; const { roomId } = message.body; - if(me.roomId !== roomId) { + if (me.roomId !== roomId) { return; } console.info("关闭房间:", roomId); me.closeMedia(); + me.roomId = null; + me.remoteClients.clear(); } /** * 创建房间信令 - * + * * @param {*} room 房间 - * + * * @returns 房间 */ async roomCreate(room) { @@ -808,7 +1327,7 @@ class Taoyao extends RemoteClient { } /** * 进入房间信令 - * + * * @param {*} roomId 房间ID * @param {*} password 房间密码 */ @@ -822,7 +1341,7 @@ class Taoyao extends RemoteClient { me.mediasoupDevice = new mediasoupClient.Device(); const response = await me.request( protocol.buildMessage("media::router::rtp::capabilities", { - roomId: me.roomId + roomId: me.roomId, }) ); const routerRtpCapabilities = response.body.rtpCapabilities; @@ -831,14 +1350,14 @@ class Taoyao extends RemoteClient { protocol.buildMessage("room::enter", { roomId: roomId, password: password, - rtpCapabilities: (me.audioConsume || me.videoConsume || me.audioProduce || me.videoProduce) ? me.mediasoupDevice.rtpCapabilities : undefined, - sctpCapabilities: (me.dataConsume || me.dataProduce) ? me.mediasoupDevice.sctpCapabilities : undefined, + rtpCapabilities: me.audioConsume || me.videoConsume || me.audioProduce || me.videoProduce ? me.mediasoupDevice.rtpCapabilities : undefined, + sctpCapabilities: me.dataConsume || me.dataProduce ? me.mediasoupDevice.sctpCapabilities : undefined, }) ); } /** * 进入房间信令 - * + * * @param {*} message 消息 */ defaultRoomEnter(message) { @@ -851,16 +1370,84 @@ class Taoyao extends RemoteClient { } } /** - * 离开房间信令 + * 踢出终端 * - * @param {*} message + * @param {*} clientId 终端ID + */ + roomExpel(clientId) { + const me = this; + if(!me.roomId) { + this.callbackError("没有进入房间"); + return; + } + me.push(protocol.buildMessage("room::expel", { + roomId: this.roomId, + clientId, + })); + } + /** + * 踢出终端信令 + * + * @param {*} message 消息 + */ + async defaultRoomExpel(message) { + const me = this; + me.roomLeave(); + } + /** + * 邀请终端 + * + * @param {*} clientId 终端ID + */ + roomInvite(clientId) { + const me = this; + if(!me.roomId) { + this.callbackError("没有进入房间"); + return; + } + me.push(protocol.buildMessage("room::invite", { + roomId: this.roomId, + clientId, + })); + } + /** + * 邀请终端信令 + * + * @param {*} message 消息 + */ + async defaultRoomInvite(message) { + const me = this; + // 默认进入,如果需要确认使用回调函数重写。 + const { roomId, password } = message.body; + // if(me.roomId) { + // this.callbackError(); + // return; + // } + await me.roomEnter(roomId, password); + await me.produceMedia(); + } + /** + * 离开房间 + */ + roomLeave() { + const me = this; + me.push(protocol.buildMessage("room::leave", { + roomId: me.roomId + })); + me.closeMedia(); + me.roomId = null; + me.remoteClients.clear(); + } + /** + * 离开房间信令 + * + * @param {*} message */ defaultRoomLeave(message) { const me = this; const { clientId } = message.body; me.remoteClients.delete(clientId); console.info("终端离开:", clientId); - // TODO:资源释放 } /** * 错误回调 @@ -895,6 +1482,12 @@ class Taoyao extends RemoteClient { ); return response.body; } + async clientList() { + const response = await this.request( + protocol.buildMessage("client::list", {}) + ); + return response.body; + } async clientList() { const response = await this.request( protocol.buildMessage("client::list", { roomId: self.roomId }) @@ -920,21 +1513,19 @@ class Taoyao extends RemoteClient { */ { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); - stream.getAudioTracks().forEach(audioTrack => { + stream.getAudioTracks().forEach((audioTrack) => { audioTrack.enabled = false; setTimeout(() => audioTrack.stop(), 30000); }); } - if (self.audioProduce || self.videoProduce) { + if (self.dataProduce || self.audioProduce || self.videoProduce) { const response = await self.request( protocol.buildMessage("media::transport::webrtc::create", { roomId: self.roomId, forceTcp: self.forceTcp, producing: true, consuming: false, - sctpCapabilities: self.dataProduce - ? self.mediasoupDevice.sctpCapabilities - : undefined, + sctpCapabilities: self.dataProduce ? self.mediasoupDevice.sctpCapabilities : undefined, }) ); const { @@ -982,7 +1573,7 @@ class Taoyao extends RemoteClient { "produce", async ({ kind, appData, rtpParameters }, callback, errback) => { try { - const { producerId } = await self.request( + const response = await self.request( protocol.buildMessage("media::produce", { kind, roomId: self.roomId, @@ -991,37 +1582,37 @@ class Taoyao extends RemoteClient { rtpParameters, }) ); + const { streamId, producerId } = response.body; callback({ id: producerId }); } catch (error) { errback(error); } } ); + // 生产数据 self.sendTransport.on( "producedata", - async ( - { label, protocol, appData, sctpStreamParameters }, - callback, - errback - ) => { + async ({ label, appData, protocol, sctpStreamParameters }, callback, errback) => { try { - const { id } = await self.request( - protocol.buildMessage("media::produceData", { + const response = await self.request( + taoyaoProtocol.buildMessage("media::data::produce", { label, + roomId: self.roomId, appData, protocol, transportId: self.sendTransport.id, sctpStreamParameters, }) ); - callback({ id }); + const { treamId, producerId } = response.body; + callback({ id: producerId }); } catch (error) { errback(error); } } ); } - if (self.audioConsume || self.videoConsume) { + if (self.dataConsume || self.audioConsume || self.videoConsume) { const self = this; const response = await self.request( protocol.buildMessage("media::transport::webrtc::create", { @@ -1029,9 +1620,7 @@ class Taoyao extends RemoteClient { forceTcp: self.forceTcp, producing: false, consuming: true, - sctpCapabilities: self.dataProduce - ? self.mediasoupDevice.sctpCapabilities - : undefined, + sctpCapabilities: self.dataProduce ? self.mediasoupDevice.sctpCapabilities : undefined, }) ); const { @@ -1079,8 +1668,14 @@ class Taoyao extends RemoteClient { } ); } + // 快速响应 this.produceAudio(); this.produceVideo(); + this.produceData(); + // 等待响应 + // await this.produceAudio(); + // await this.produceVideo(); + // await this.produceData(); } /** * 生产音频 @@ -1104,7 +1699,11 @@ class Taoyao extends RemoteClient { } track = tracks[0]; // TODO:验证修改API audioTrack.applyCapabilities - console.debug("音频信息:", track.getSettings(), track.getCapabilities()); + console.debug( + "音频信息:", + track.getSettings(), + track.getCapabilities() + ); this.audioProducer = await this.sendTransport.produce({ track, codecOptions: { @@ -1115,8 +1714,12 @@ class Taoyao extends RemoteClient { // codec : this._mediasoupDevice.rtpCapabilities.codecs // .find((codec) => codec.mimeType.toLowerCase() === 'audio/pcma') }); - self.audioActive = true; - self.track = track; + if (self.proxy && self.proxy.media) { + self.audioTrack = track; + self.proxy.media(track, this.audioProducer); + } else { + console.warn("终端没有实现服务代理:", self); + } // TODO:加密解密 // if (this._e2eKey && e2e.isSupported()) { // e2e.setupSenderTransform(this._micProducer.rtpSender); @@ -1145,46 +1748,26 @@ class Taoyao extends RemoteClient { if (!this.audioProducer) { return; } - this.audioProducer.close(); try { await this.request( protocol.buildMessage("media::producer::close", { + roomId: this.roomId, producerId: this.audioProducer.id, }) ); } catch (error) { console.error("关闭麦克风异常", error); } - this.audioProducer = null; } async pauseAudioProducer() { console.debug("静音麦克风"); - this.audioProducer.pause(); - try { - await this.request( - protocol.buildMessage("media::producer::pause", { - producerId: this.audioProducer.id, - }) - ); - } catch (error) { - console.error("静音麦克风异常", error); - // TODO:异常调用回调 - } + this.mediaProducerPause(this.audioProducer.id); } async resumeAudioProducer() { console.debug("恢复麦克风"); - this.audioProducer.resume(); - try { - await this.request( - protocol.buildMessage("media::producer::resume", { - producerId: this.audioProducer.id, - }) - ); - } catch (error) { - console.error("恢复麦克风异常", error); - } + this.mediaProducerResume(this.audioProducer.id); } /** @@ -1198,46 +1781,8 @@ class Taoyao extends RemoteClient { if (self.videoProducer) { return; } - let track; try { - if (self.videoSource === "file") { - // TODO:实现文件分享 - // const stream = await this._getExternalVideoStream(); - // track = stream.getVideoTracks()[0].clone(); - } else if (self.videoSource === "camera") { - console.debug("enableWebcam() | calling getUserMedia()"); - // TODO:参数 - const stream = await navigator.mediaDevices.getUserMedia({ - video: self.videoConfig, - }); - track = stream.getVideoTracks()[0]; - // TODO:验证修改API videoTrack.applyCapabilities - console.debug("视频信息:", track.getSettings(), track.getCapabilities()); - } else if (self.videoSource === "screen") { - const stream = await navigator.mediaDevices.getDisplayMedia({ - // 如果需要共享声音 - audio: false, - video: { - cursor: true, - width: { max: 1920 }, - height: { max: 1080 }, - frameRate: { max: 30 }, - logicalSurface: true, - displaySurface: "monitor", - }, - }); - track = stream.getVideoTracks()[0]; - } else { - // TODO:异常 - } - if(self.proxy && self.proxy.media) { - self.videoTrack = track; - self.proxy.media(track); - } else { - console.warn("终端没有实现服务代理:", self); - } - self.videoActive = true; - self.track = track; + let track = await self.getVideoTrack(); let codec; let encodings; const codecOptions = { @@ -1281,15 +1826,18 @@ class Taoyao extends RemoteClient { encodings, codecOptions, }); - + if (self.proxy && self.proxy.media) { + self.videoTrack = track; + self.proxy.media(track, this.videoProducer); + } else { + console.warn("终端没有实现服务代理:", self); + } // if (this._e2eKey && e2e.isSupported()) { // e2e.setupSenderTransform(this.videoProducer.rtpSender); // } - this.videoProducer.on("transportclose", () => { this.videoProducer = null; }); - this.videoProducer.on("trackended", () => { console.warn("video producer trackended", this.audioProducer); this.closeVideoProducer().catch(() => {}); @@ -1305,89 +1853,97 @@ class Taoyao extends RemoteClient { } } + /** + * 生产数据 + */ + async produceData() { + const me = this; + try { + const dataProducer = await me.sendTransport.produceData({ + ordered: false, + maxPacketLifeTime: 2000, + }); + me.dataProducer = dataProducer; + me.dataProducer.on("open", () => { + console.debug("dataProducer open:", me.dataProducer.id); + window.dataProducer = me.dataProducer; + }); + me.dataProducer.on("close", () => { + console.debug("dataProducer close:", me.dataProducer.id); + me.dataProducer = null; + }); + me.dataProducer.on("error", (error) => { + console.debug("dataProducer error:", me.dataProducer.id, error); + me.dataProducer.close(); + }); + me.dataProducer.on("transportclose", () => { + console.debug("dataProducer transportclose:", me.dataProducer.id); + me.dataProducer.close(); + }); + me.dataProducer.on("bufferedamountlow", () => { + console.debug("dataProducer bufferedamountlow:", me.dataProducer.id); + }); + } catch (error) { + me.callbackError("生产数据异常", error); + } + } + + /** + * 通过数据生产者发送数据 + * + * @param {*} data 数据 + */ + async sendDataProducer(data) { + const me = this; + if(!me.dataProducer) { + me.callbackError("数据生产者无效"); + return; + } + me.dataProducer.send(data); + } + async closeVideoProducer() { console.debug("disableWebcam()"); if (!this.videoProducer) { return; } - this.videoProducer.close(); try { await this.request( protocol.buildMessage("media::producer::close", { + roomId: this.roomId, producerId: this.videoProducer.id, }) - ); - } catch (error) { - console.error(error); - } - - this._webcamProducer = null; + ); + } catch (error) { + console.error(error); + } } async pauseVideoProducer() { console.debug("关闭摄像头"); - this.videoProducer.pause(); - try { - await this.request( - protocol.buildMessage("media::producer::pause", { - producerId: this.videoProducer.id, - }) - ); - } catch (error) { - console.error("关闭摄像头异常", error); - // TODO:异常调用回调 - } + this.mediaProducerPause(this.videoProducer.id); } async resumeVideoProducer() { console.debug("恢复摄像头"); - this.videoProducer.resume(); - try { - await this.request( - protocol.buildMessage("media::producer::resume", { - producerId: this.videoProducer.id, - }) - ); - } catch (error) { - console.error("恢复摄像头异常", error); - } + this.mediaProducerResume(this.videoProducer.id); } - async updateVideoConfig(config) { + /** + * 更新视频生产者 + */ + async updateVideoProducer() { + const me = this; console.debug("更新摄像头参数"); try { - this.videoProducer.track.stop(); - // TODO:screen、参数配置 - const stream = await navigator.mediaDevices.getUserMedia({ - video: true, - }); - const track = stream.getVideoTracks()[0]; + const track = await me.getVideoTrack(); await this.videoProducer.replaceTrack({ track }); + me.proxy.media(track, this.videoProducer); } catch (error) { console.error("changeWebcam() | failed: %o", error); } } - async pauseConsumer(consumer) { - if (consumer.paused) return; - try { - await this._protoo.request("pauseConsumer", { consumerId: consumer.id }); - consumer.pause(); - } catch (error) { - logger.error("_pauseConsumer() | failed:%o", error); - } - } - - async resumeConsumer(consumer) { - if (!consumer.paused) return; - try { - await this._protoo.request("resumeConsumer", { consumerId: consumer.id }); - consumer.resume(); - } catch (error) { - logger.error("_resumeConsumer() | failed:%o", error); - } - } - /** * 验证设备 */ @@ -1453,14 +2009,25 @@ class Taoyao extends RemoteClient { * 关闭媒体 */ closeMedia() { - let self = this; - if (self.sendTransport) { - self.sendTransport.close(); + let me = this; + if (me.sendTransport) { + me.sendTransport.close(); } - if (self.recvTransport) { - self.recvTransport.close(); + if (me.recvTransport) { + me.recvTransport.close(); } - }; + if(me.audioTrack) { + me.audioTrack.stop(); + } + if(me.videoTrack) { + me.videoTrack.stop(); + } + me.sendTransport = null; + me.recvTransport = null; + me.audioProducer = null; + me.videoProducer = null; + me.dataProducer = null; + } /** * 关闭资源 */ @@ -1470,7 +2037,7 @@ class Taoyao extends RemoteClient { if (me.signalChannel) { me.signalChannel.close(); } - }; + } } export { Taoyao }; diff --git a/taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/annotation/Description.java b/taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/annotation/Description.java index 55d7645..8f7ccf8 100644 --- a/taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/annotation/Description.java +++ b/taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/annotation/Description.java @@ -14,7 +14,6 @@ import java.lang.annotation.Target; * =[消息类型]> 同步请求 * -[消息类型]) 全员广播:对所有的终端广播信令(排除自己) * +[消息类型]) 全员广播:对所有的终端广播信令(包含自己) - * ...:其他自定义的透传内容 * * @author acgist */ @@ -30,9 +29,6 @@ public @interface Description { String[] body() default { "{}" }; /** - * 同步:需要等待服务端数据时使用 - * 异步:不用等待服务端数据时使用(服务端能主动通知类型消息都能使用异步) - * * @return 数据流向 */ String[] flow() default { "终端->信令服务->终端" }; diff --git a/taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/annotation/Prototype.java b/taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/annotation/Prototype.java deleted file mode 100644 index 2e0a563..0000000 --- a/taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/annotation/Prototype.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.acgist.taoyao.boot.annotation; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import org.springframework.beans.factory.config.ConfigurableBeanFactory; -import org.springframework.context.annotation.Scope; -import org.springframework.stereotype.Component; - -/** - * 模板:多例对象 - * - * @author acgist - */ -@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) -@Target(ElementType.TYPE) -@Component -@Retention(RetentionPolicy.RUNTIME) -@Documented -public @interface Prototype { - -} diff --git a/taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/config/Constant.java b/taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/config/Constant.java index 0279e86..d298eaf 100644 --- a/taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/config/Constant.java +++ b/taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/config/Constant.java @@ -1,5 +1,7 @@ package com.acgist.taoyao.boot.config; +import java.util.function.BiFunction; + /** * 常量 * @@ -107,6 +109,10 @@ public interface Constant { * 密码 */ String PASSWORD = "password"; + /** + * 数据 + */ + String DATA = "data"; /** * 名称 */ @@ -228,4 +234,14 @@ public interface Constant { */ String SUBSCRIBE_TYPE = "subscribeType"; + /** + * 生产者ID生成器 + */ + public static final BiFunction