[+] 视频添加
This commit is contained in:
@@ -573,7 +573,7 @@ class Signal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async mediaConsume(message, body) {
|
async mediaConsume(message, body) {
|
||||||
const { roomId, clientId, streamId, producerId, transportId, rtpCapabilities } = body;
|
const { roomId, clientId, sourceId, streamId, producerId, transportId, rtpCapabilities } = body;
|
||||||
const room = this.rooms.get(roomId);
|
const room = this.rooms.get(roomId);
|
||||||
const producer = room.producers.get(producerId);
|
const producer = room.producers.get(producerId);
|
||||||
const transport = room.transports.get(transportId);
|
const transport = room.transports.get(transportId);
|
||||||
@@ -682,6 +682,7 @@ class Signal {
|
|||||||
type: consumer.type,
|
type: consumer.type,
|
||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
clientId: clientId,
|
clientId: clientId,
|
||||||
|
sourceId: sourceId,
|
||||||
streamId: streamId,
|
streamId: streamId,
|
||||||
producerId: producerId,
|
producerId: producerId,
|
||||||
consumerId: consumer.id,
|
consumerId: consumer.id,
|
||||||
|
|||||||
@@ -13,7 +13,8 @@
|
|||||||
"vue": "^3.2.44",
|
"vue": "^3.2.44",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"element-plus": "^2.2.32",
|
"element-plus": "^2.2.32",
|
||||||
"mediasoup-client": "^3.6.82"
|
"mediasoup-client": "^3.6.82",
|
||||||
|
"@element-plus/icons": "^0.0.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^4.0.0",
|
"vite": "^4.0.0",
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
center
|
center
|
||||||
width="30%"
|
width="30%"
|
||||||
title="房间设置"
|
title="房间设置"
|
||||||
@open="init"
|
@open="loadList"
|
||||||
:show-close="false"
|
:show-close="false"
|
||||||
v-model="roomVisible"
|
v-model="roomVisible"
|
||||||
>
|
>
|
||||||
@@ -80,22 +80,27 @@
|
|||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 菜单 -->
|
<!-- 菜单 -->
|
||||||
<div class="menu">
|
<div class="menus">
|
||||||
<el-button type="primary" @click="signalVisible = true">连接信令</el-button>
|
<el-button type="primary" @click="signalVisible = true">连接信令</el-button>
|
||||||
<el-button type="primary" @click="roomActive = 'enter'; roomVisible = true;">选择房间</el-button>
|
<el-button type="primary" @click="roomActive = 'enter'; roomVisible = true;">选择房间</el-button>
|
||||||
<el-button type="primary" @click="roomActive = 'create';roomVisible = true;">创建房间</el-button>
|
<el-button type="primary" @click="roomActive = 'create';roomVisible = true;">创建房间</el-button>
|
||||||
|
<el-button>邀请终端</el-button>
|
||||||
<el-button>退出房间</el-button>
|
<el-button>退出房间</el-button>
|
||||||
<el-button type="danger">关闭房间</el-button>
|
<el-button type="danger">关闭房间</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 终端 -->
|
<!-- 终端 -->
|
||||||
<div class="client">
|
<div class="clients">
|
||||||
|
<LocalClient ref="local"></LocalClient>
|
||||||
|
<RemoteClient :ref="'remote-' + kv[0]" v-for="(kv, index) in remoteClients" :key="index"></RemoteClient>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { Taoyao } from "./components/Taoyao.js";
|
import { Taoyao } from "./components/Taoyao.js";
|
||||||
|
import LocalClient from './components/LocalClient.vue';
|
||||||
|
import RemoteClient from './components/RemoteClient.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Taoyao",
|
name: "Taoyao",
|
||||||
@@ -114,7 +119,8 @@ export default {
|
|||||||
taoyao: {},
|
taoyao: {},
|
||||||
roomActive: "enter",
|
roomActive: "enter",
|
||||||
roomVisible: false,
|
roomVisible: false,
|
||||||
signalVisible: true,
|
signalVisible: false,
|
||||||
|
remoteClients: new Map(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@@ -124,7 +130,14 @@ export default {
|
|||||||
`);
|
`);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async init() {
|
async connectSignal() {
|
||||||
|
let self = this;
|
||||||
|
self.taoyao = new Taoyao({ ...this.config });
|
||||||
|
self.remoteClients = self.taoyao.remoteClients;
|
||||||
|
await self.taoyao.connectSignal(self.callback, self.callbackMedia);
|
||||||
|
self.signalVisible = false;
|
||||||
|
},
|
||||||
|
async loadList() {
|
||||||
this.rooms = await this.taoyao.roomList();
|
this.rooms = await this.taoyao.roomList();
|
||||||
this.medias = await this.taoyao.mediaList();
|
this.medias = await this.taoyao.mediaList();
|
||||||
},
|
},
|
||||||
@@ -138,12 +151,6 @@ export default {
|
|||||||
this.room = room;
|
this.room = room;
|
||||||
await this.enterRoom(room.roomId);
|
await this.enterRoom(room.roomId);
|
||||||
},
|
},
|
||||||
async connectSignal() {
|
|
||||||
let self = this;
|
|
||||||
self.taoyao = new Taoyao({ ...this.config });
|
|
||||||
await self.taoyao.connectSignal(self.callback);
|
|
||||||
self.signalVisible = false;
|
|
||||||
},
|
|
||||||
/**
|
/**
|
||||||
* 信令回调
|
* 信令回调
|
||||||
*
|
*
|
||||||
@@ -172,10 +179,37 @@ export default {
|
|||||||
message: data.message,
|
message: data.message,
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
break;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* 媒体回调
|
||||||
|
*/
|
||||||
|
callbackMedia(type, track, consumer) {
|
||||||
|
const self = this;
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if(type === 'local') {
|
||||||
|
self.$refs.local.media(track);
|
||||||
|
} else {
|
||||||
|
this.$refs['remote-' + consumer.sourceId][0].media(track, consumer);
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
LocalClient,
|
||||||
|
RemoteClient
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.menus{width:100%;top:1rem;left:0;text-align:center;position:fixed;z-index:1;}
|
||||||
|
.clients{width:100%;height:100%;top:0;left:0;position:fixed;}
|
||||||
|
.client{float:left;width:50vw;height:50vh;border:1px solid #eee;}
|
||||||
|
.client .buttons{width:100%;bottom:1rem;left:0;text-align:center;position:absolute;padding:0.8rem 0;background: rgba(0,0,0,0.6);text-align:center;}
|
||||||
|
.client audio{display:none;}
|
||||||
|
.client video{width:100%;height:100%;}
|
||||||
|
</style>
|
||||||
@@ -1,24 +1,113 @@
|
|||||||
<!-- 本地终端 -->
|
<!-- 本地终端 -->
|
||||||
<template></template>
|
<template>
|
||||||
|
<div class="client">
|
||||||
|
<audio ref="audio"></audio>
|
||||||
|
<video ref="video"></video>
|
||||||
|
<div class="buttons">
|
||||||
|
<el-button type="danger" title="打开麦克风" :icon="Mute" circle />
|
||||||
|
<el-button type="primary" title="关闭麦克风" :icon="Microphone" circle />
|
||||||
|
<el-button type="danger" title="打开摄像头" :icon="VideoPause" circle />
|
||||||
|
<el-button type="primary" title="关闭摄像头" :icon="VideoPlay" circle />
|
||||||
|
<el-button title="交换媒体" :icon="Refresh" circle />
|
||||||
|
<el-button title="拍照" :icon="Camera" circle />
|
||||||
|
<el-button title="录像" :icon="VideoCamera" circle />
|
||||||
|
<el-button title="媒体信息" :icon="InfoFilled" circle />
|
||||||
|
<el-select placeholder="视频质量">
|
||||||
|
<el-option
|
||||||
|
v-for="option in options"
|
||||||
|
:key="option.value"
|
||||||
|
:label="option.label"
|
||||||
|
:value="option.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { defineComponent } from "@vue/composition-api";
|
import {
|
||||||
|
Mute,
|
||||||
export default defineComponent({
|
Camera,
|
||||||
|
Refresh,
|
||||||
|
VideoPlay,
|
||||||
|
VideoPause,
|
||||||
|
InfoFilled,
|
||||||
|
Microphone,
|
||||||
|
VideoCamera,
|
||||||
|
CircleClose,
|
||||||
|
} from "@element-plus/icons";
|
||||||
|
export default {
|
||||||
|
name: "LocalClient",
|
||||||
setup() {
|
setup() {
|
||||||
// 本地视频
|
return {
|
||||||
this._externalVideo = document.createElement("video");
|
Mute,
|
||||||
|
Camera,
|
||||||
this._externalVideo.controls = true;
|
Refresh,
|
||||||
this._externalVideo.muted = true;
|
VideoPlay,
|
||||||
this._externalVideo.loop = true;
|
VideoPause,
|
||||||
this._externalVideo.setAttribute("playsinline", "");
|
InfoFilled,
|
||||||
this._externalVideo.src = EXTERNAL_VIDEO_SRC;
|
Microphone,
|
||||||
|
VideoCamera,
|
||||||
// TODO:关闭摄像头、视频、音频
|
CircleClose,
|
||||||
this._externalVideo
|
};
|
||||||
.play()
|
|
||||||
.catch((error) => console.warn("externalVideo.play() failed:%o", error));
|
|
||||||
},
|
},
|
||||||
});
|
data() {
|
||||||
|
return {
|
||||||
|
taoyao: null,
|
||||||
|
audio: null,
|
||||||
|
video: null,
|
||||||
|
audioStream: null,
|
||||||
|
videoStream: null,
|
||||||
|
dataProducer: null,
|
||||||
|
audioProducer: null,
|
||||||
|
videoProducer: null,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "HD",
|
||||||
|
label: "高清",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "SD",
|
||||||
|
label: "标签",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "FD",
|
||||||
|
label: "超清",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "BD",
|
||||||
|
label: "蓝光",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "QD",
|
||||||
|
label: "2K",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "UD",
|
||||||
|
label: "4K",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.audio = this.$refs.audio;
|
||||||
|
this.video = this.$refs.video;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
media(track) {
|
||||||
|
if (track.kind === "video") {
|
||||||
|
if (this.videoStream) {
|
||||||
|
// TODO:资源释放
|
||||||
|
} else {
|
||||||
|
this.videoStream = new MediaStream();
|
||||||
|
this.videoStream.addTrack(track);
|
||||||
|
this.video.srcObject = this.videoStream;
|
||||||
|
}
|
||||||
|
this.video.play().catch((error) => console.warn("视频播放失败", error));
|
||||||
|
} else {
|
||||||
|
console.debug("本地不支持的媒体类型:", track);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,14 +1,88 @@
|
|||||||
<!-- 远程终端 -->
|
<!-- 远程终端 -->
|
||||||
<template>
|
<template>
|
||||||
<video></video>
|
<div class="client">
|
||||||
<audio></audio>
|
<audio ref="audio"></audio>
|
||||||
<div>
|
<video ref="video"></video>
|
||||||
<el-button type="primary" title="关闭麦克风" :icon="Edit" circle />
|
<div class="buttons">
|
||||||
|
<el-button type="danger" title="打开麦克风" :icon="Mute" circle />
|
||||||
|
<el-button type="primary" title="关闭麦克风" :icon="Microphone" circle />
|
||||||
|
<el-button type="danger" title="打开摄像头" :icon="VideoPause" circle />
|
||||||
|
<el-button type="primary" title="关闭摄像头" :icon="VideoPlay" circle />
|
||||||
|
<el-button title="拍照" :icon="Camera" circle />
|
||||||
|
<el-button title="录像" :icon="VideoCamera" circle />
|
||||||
|
<el-button title="媒体信息" :icon="InfoFilled" circle />
|
||||||
|
<el-button title="踢出" :icon="CircleClose" circle />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import {
|
||||||
|
Mute,
|
||||||
|
Camera,
|
||||||
|
Refresh,
|
||||||
|
VideoPlay,
|
||||||
|
VideoPause,
|
||||||
|
InfoFilled,
|
||||||
|
Microphone,
|
||||||
|
VideoCamera,
|
||||||
|
CircleClose,
|
||||||
|
} from "@element-plus/icons";
|
||||||
export default {
|
export default {
|
||||||
name: "RemoteClient",
|
name: "RemoteClient",
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
Mute,
|
||||||
|
Camera,
|
||||||
|
Refresh,
|
||||||
|
VideoPlay,
|
||||||
|
VideoPause,
|
||||||
|
InfoFilled,
|
||||||
|
Microphone,
|
||||||
|
VideoCamera,
|
||||||
|
CircleClose,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
taoyao: null,
|
||||||
|
audio: null,
|
||||||
|
video: null,
|
||||||
|
audioStream: null,
|
||||||
|
videoStream: null,
|
||||||
|
dataConsumer: null,
|
||||||
|
audioConsumer: null,
|
||||||
|
videoConsumer: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.audio = this.$refs.audio;
|
||||||
|
this.video = this.$refs.video;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
media(track, consumer) {
|
||||||
|
if(track.kind === 'audio') {
|
||||||
|
if (this.audioStream) {
|
||||||
|
// TODO:资源释放
|
||||||
|
} else {
|
||||||
|
this.audioStream = new MediaStream();
|
||||||
|
this.audioStream.addTrack(track);
|
||||||
|
this.audio.srcObject = this.audioStream;
|
||||||
}
|
}
|
||||||
|
this.audio.play().catch((error) => console.warn("视频播放失败", error));
|
||||||
|
} else if(track.kind === 'video') {
|
||||||
|
if (this.videoStream) {
|
||||||
|
// TODO:资源释放
|
||||||
|
} else {
|
||||||
|
this.videoStream = new MediaStream();
|
||||||
|
this.videoStream.addTrack(track);
|
||||||
|
this.video.srcObject = this.videoStream;
|
||||||
|
}
|
||||||
|
this.video.play().catch((error) => console.warn("视频播放失败", error));
|
||||||
|
} else {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -11,18 +11,14 @@ import {
|
|||||||
} from "./Config.js";
|
} from "./Config.js";
|
||||||
|
|
||||||
// Used for simulcast webcam video.
|
// Used for simulcast webcam video.
|
||||||
const WEBCAM_SIMULCAST_ENCODINGS =
|
const WEBCAM_SIMULCAST_ENCODINGS = [
|
||||||
[
|
{ scaleResolutionDownBy: 4, maxBitrate: 500000, scalabilityMode: "S1T2" },
|
||||||
{ scaleResolutionDownBy: 4, maxBitrate: 500000, scalabilityMode: 'S1T2' },
|
{ scaleResolutionDownBy: 2, maxBitrate: 1000000, scalabilityMode: "S1T2" },
|
||||||
{ scaleResolutionDownBy: 2, maxBitrate: 1000000, scalabilityMode: 'S1T2' },
|
{ scaleResolutionDownBy: 1, maxBitrate: 5000000, scalabilityMode: "S1T2" },
|
||||||
{ scaleResolutionDownBy: 1, maxBitrate: 5000000, scalabilityMode: 'S1T2' }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Used for VP9 webcam video.
|
// Used for VP9 webcam video.
|
||||||
const WEBCAM_KSVC_ENCODINGS =
|
const WEBCAM_KSVC_ENCODINGS = [{ scalabilityMode: "S3T3_KEY" }];
|
||||||
[
|
|
||||||
{ scalabilityMode: 'S3T3_KEY' }
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 信令通道
|
* 信令通道
|
||||||
@@ -294,6 +290,12 @@ const signalChannel = {
|
|||||||
case "client::shutdown":
|
case "client::shutdown":
|
||||||
self.defaultClientShutdown(message);
|
self.defaultClientShutdown(message);
|
||||||
break;
|
break;
|
||||||
|
case "room::enter":
|
||||||
|
self.defaultRoomEnter(message);
|
||||||
|
break;
|
||||||
|
case "room::client::list":
|
||||||
|
self.defaultRoomClientList(message);
|
||||||
|
break;
|
||||||
case "platform::error":
|
case "platform::error":
|
||||||
self.callbackError(message);
|
self.callbackError(message);
|
||||||
break;
|
break;
|
||||||
@@ -334,6 +336,24 @@ const signalChannel = {
|
|||||||
console.info("关闭终端");
|
console.info("关闭终端");
|
||||||
window.close();
|
window.close();
|
||||||
},
|
},
|
||||||
|
defaultRoomEnter(message) {
|
||||||
|
const { roomId, clientId } = message.body;
|
||||||
|
if(clientId === this.taoyao.clientId) {
|
||||||
|
// 忽略自己
|
||||||
|
} else {
|
||||||
|
this.taoyao.remoteClients.set(clientId, roomId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defaultRoomClientList(message) {
|
||||||
|
const self = this;
|
||||||
|
message.body.forEach(v => {
|
||||||
|
if(v.clientId === self.taoyao.clientId) {
|
||||||
|
// 忽略自己
|
||||||
|
} else {
|
||||||
|
self.taoyao.remoteClients.set(v.clientId, self.taoyao.roomId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -354,6 +374,8 @@ class Taoyao {
|
|||||||
password;
|
password;
|
||||||
// 回调事件
|
// 回调事件
|
||||||
callback;
|
callback;
|
||||||
|
// 媒体回调
|
||||||
|
callbackMedia;
|
||||||
// 音频媒体配置
|
// 音频媒体配置
|
||||||
audio;
|
audio;
|
||||||
// 视频媒体配置
|
// 视频媒体配置
|
||||||
@@ -364,10 +386,6 @@ class Taoyao {
|
|||||||
push;
|
push;
|
||||||
// 请求信令
|
// 请求信令
|
||||||
request;
|
request;
|
||||||
// 本地终端
|
|
||||||
localClient;
|
|
||||||
// 远程终端
|
|
||||||
remoteClients = new Map();
|
|
||||||
// 信令通道
|
// 信令通道
|
||||||
signalChannel;
|
signalChannel;
|
||||||
// 发送媒体通道
|
// 发送媒体通道
|
||||||
@@ -381,30 +399,31 @@ class Taoyao {
|
|||||||
// 是否生产
|
// 是否生产
|
||||||
produce;
|
produce;
|
||||||
// 视频来源:file | camera | screen
|
// 视频来源:file | camera | screen
|
||||||
videoSource = "screen";
|
videoSource = "camera";
|
||||||
|
// 强制使用TCP
|
||||||
|
forceTcp;
|
||||||
// 强制使用VP9
|
// 强制使用VP9
|
||||||
forceVP9;
|
forceVP9;
|
||||||
// 强制使用H264
|
// 强制使用H264
|
||||||
forceH264;
|
forceH264;
|
||||||
|
//
|
||||||
useSimulcast;
|
useSimulcast;
|
||||||
|
// 是否生产数据
|
||||||
|
dataProduce;
|
||||||
// 是否生产音频
|
// 是否生产音频
|
||||||
audioProduce;
|
audioProduce;
|
||||||
// 是否生成视频
|
// 是否生成视频
|
||||||
videoProduce;
|
videoProduce;
|
||||||
// 强制使用TCP
|
// 数据生产者
|
||||||
forceTcp;
|
dataProducer;
|
||||||
// 使用数据通道
|
|
||||||
useDataChannel;
|
|
||||||
// 音频生产者
|
// 音频生产者
|
||||||
audioProducer;
|
audioProducer;
|
||||||
// 视频生产者
|
// 视频生产者
|
||||||
videoProducer;
|
videoProducer;
|
||||||
// 数据生产者
|
// 消费者:音频、视频、数据
|
||||||
dataChannnelProducer;
|
|
||||||
// 媒体消费者
|
|
||||||
consumers = new Map();
|
consumers = new Map();
|
||||||
// 数据消费者
|
// 远程终端
|
||||||
dataConsumers = new Map();
|
remoteClients = new Map();
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
roomId,
|
roomId,
|
||||||
@@ -418,7 +437,7 @@ class Taoyao {
|
|||||||
audioProduce = true,
|
audioProduce = true,
|
||||||
videoProduce = true,
|
videoProduce = true,
|
||||||
forceTcp = false,
|
forceTcp = false,
|
||||||
useDataChannel = true,
|
dataProduce = true,
|
||||||
}) {
|
}) {
|
||||||
this.roomId = roomId;
|
this.roomId = roomId;
|
||||||
this.clientId = clientId;
|
this.clientId = clientId;
|
||||||
@@ -428,22 +447,24 @@ class Taoyao {
|
|||||||
this.password = password;
|
this.password = password;
|
||||||
this.consume = consume;
|
this.consume = consume;
|
||||||
this.produce = produce;
|
this.produce = produce;
|
||||||
|
this.dataProduce = produce && dataProduce;
|
||||||
this.audioProduce = produce && audioProduce;
|
this.audioProduce = produce && audioProduce;
|
||||||
this.videoProduce = produce && videoProduce;
|
this.videoProduce = produce && videoProduce;
|
||||||
this.forceTcp = forceTcp;
|
this.forceTcp = forceTcp;
|
||||||
this.useDataChannel = useDataChannel;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 连接信令
|
* 连接信令
|
||||||
*
|
*
|
||||||
* @param {*} callback
|
* @param {*} callback 信令回调
|
||||||
|
* @param {*} callbackMedia 媒体回调
|
||||||
*
|
*
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
async connectSignal(callback) {
|
async connectSignal(callback, callbackMedia) {
|
||||||
const self = this;
|
const self = this;
|
||||||
self.callback = callback;
|
self.callback = callback;
|
||||||
|
self.callbackMedia = callbackMedia;
|
||||||
self.signalChannel = signalChannel;
|
self.signalChannel = signalChannel;
|
||||||
signalChannel.taoyao = self;
|
signalChannel.taoyao = self;
|
||||||
// 不能直接this.push = this.signalChannel.push这样导致this对象错误
|
// 不能直接this.push = this.signalChannel.push这样导致this对象错误
|
||||||
@@ -469,6 +490,7 @@ class Taoyao {
|
|||||||
} else {
|
} else {
|
||||||
console.warn("没有注册回调:", message);
|
console.warn("没有注册回调:", message);
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
// 错误回调
|
// 错误回调
|
||||||
const errorMessage = protocol.buildMessage(
|
const errorMessage = protocol.buildMessage(
|
||||||
@@ -490,6 +512,12 @@ class Taoyao {
|
|||||||
);
|
);
|
||||||
return response.body;
|
return response.body;
|
||||||
}
|
}
|
||||||
|
async clientList() {
|
||||||
|
const response = await this.request(
|
||||||
|
protocol.buildMessage("client::list", { roomId: self.roomId })
|
||||||
|
);
|
||||||
|
return response.body;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 创建房间
|
* 创建房间
|
||||||
*/
|
*/
|
||||||
@@ -526,7 +554,7 @@ class Taoyao {
|
|||||||
? self.mediasoupDevice.rtpCapabilities
|
? self.mediasoupDevice.rtpCapabilities
|
||||||
: undefined,
|
: undefined,
|
||||||
sctpCapabilities:
|
sctpCapabilities:
|
||||||
self.consume && self.useDataChannel
|
self.consume && self.dataProduce
|
||||||
? self.mediasoupDevice.sctpCapabilities
|
? self.mediasoupDevice.sctpCapabilities
|
||||||
: undefined,
|
: undefined,
|
||||||
})
|
})
|
||||||
@@ -564,7 +592,7 @@ class Taoyao {
|
|||||||
forceTcp: self.forceTcp,
|
forceTcp: self.forceTcp,
|
||||||
producing: true,
|
producing: true,
|
||||||
consuming: false,
|
consuming: false,
|
||||||
sctpCapabilities: self.useDataChannel
|
sctpCapabilities: self.dataProduce
|
||||||
? self.mediasoupDevice.sctpCapabilities
|
? self.mediasoupDevice.sctpCapabilities
|
||||||
: undefined,
|
: undefined,
|
||||||
})
|
})
|
||||||
@@ -661,7 +689,7 @@ class Taoyao {
|
|||||||
forceTcp: self.forceTcp,
|
forceTcp: self.forceTcp,
|
||||||
producing: false,
|
producing: false,
|
||||||
consuming: true,
|
consuming: true,
|
||||||
sctpCapabilities: self.useDataChannel
|
sctpCapabilities: self.dataProduce
|
||||||
? self.mediasoupDevice.sctpCapabilities
|
? self.mediasoupDevice.sctpCapabilities
|
||||||
: undefined,
|
: undefined,
|
||||||
})
|
})
|
||||||
@@ -843,20 +871,24 @@ class Taoyao {
|
|||||||
track = stream.getVideoTracks()[0];
|
track = stream.getVideoTracks()[0];
|
||||||
} else if (self.videoSource === "screen") {
|
} else if (self.videoSource === "screen") {
|
||||||
const stream = await navigator.mediaDevices.getDisplayMedia({
|
const stream = await navigator.mediaDevices.getDisplayMedia({
|
||||||
|
// 如果需要共享声音
|
||||||
audio: false,
|
audio: false,
|
||||||
video: {
|
video: {
|
||||||
displaySurface: "monitor",
|
|
||||||
logicalSurface: true,
|
|
||||||
cursor: true,
|
cursor: true,
|
||||||
width: { max: 1920 },
|
width: { max: 1920 },
|
||||||
height: { max: 1080 },
|
height: { max: 1080 },
|
||||||
frameRate: { max: 30 },
|
frameRate: { max: 30 },
|
||||||
|
logicalSurface: true,
|
||||||
|
displaySurface: "monitor",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
track = stream.getVideoTracks()[0];
|
track = stream.getVideoTracks()[0];
|
||||||
} else {
|
} else {
|
||||||
// TODO:异常
|
// TODO:异常
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.callbackMedia("local", track);
|
||||||
|
|
||||||
let codec;
|
let codec;
|
||||||
let encodings;
|
let encodings;
|
||||||
const codecOptions = {
|
const codecOptions = {
|
||||||
@@ -924,6 +956,69 @@ class Taoyao {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async closeVideoProducer() {
|
||||||
|
console.debug("disableWebcam()");
|
||||||
|
if (!this.videoProducer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.videoProducer.close();
|
||||||
|
try {
|
||||||
|
await this.request(
|
||||||
|
protocol.buildMessage("media::producer::close", {
|
||||||
|
producerId: this.videoProducer.id,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._webcamProducer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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:异常调用回调
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateVideoConfig(config) {
|
||||||
|
console.debug("更新摄像头参数");
|
||||||
|
try {
|
||||||
|
this.videoProducer.track.stop();
|
||||||
|
// TODO:screen、参数配置
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
video: true,
|
||||||
|
});
|
||||||
|
const track = stream.getVideoTracks()[0];
|
||||||
|
await this.videoProducer.replaceTrack({ track });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("changeWebcam() | failed: %o", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 消费媒体
|
* 消费媒体
|
||||||
*
|
*
|
||||||
@@ -941,6 +1036,7 @@ class Taoyao {
|
|||||||
type,
|
type,
|
||||||
roomId,
|
roomId,
|
||||||
clientId,
|
clientId,
|
||||||
|
sourceId,
|
||||||
streamId,
|
streamId,
|
||||||
producerId,
|
producerId,
|
||||||
consumerId,
|
consumerId,
|
||||||
@@ -962,6 +1058,7 @@ class Taoyao {
|
|||||||
appData, // Trick.
|
appData, // Trick.
|
||||||
});
|
});
|
||||||
consumer.clientId = clientId;
|
consumer.clientId = clientId;
|
||||||
|
consumer.sourceId = sourceId;
|
||||||
consumer.streamId = streamId;
|
consumer.streamId = streamId;
|
||||||
self.consumers.set(consumer.id, consumer);
|
self.consumers.set(consumer.id, consumer);
|
||||||
consumer.on("transportclose", () => {
|
consumer.on("transportclose", () => {
|
||||||
@@ -991,17 +1088,9 @@ class Taoyao {
|
|||||||
// )
|
// )
|
||||||
// );
|
// );
|
||||||
self.push(message);
|
self.push(message);
|
||||||
console.log(consumer);
|
console.log("消费者", consumer);
|
||||||
|
|
||||||
const audioElem = document.createElement("video");
|
self.callbackMedia("remote", consumer.track, consumer);
|
||||||
document.getElementsByTagName("body")[0].appendChild(audioElem);
|
|
||||||
|
|
||||||
const stream = new MediaStream();
|
|
||||||
stream.addTrack(consumer.track);
|
|
||||||
audioElem.srcObject = stream;
|
|
||||||
audioElem
|
|
||||||
.play()
|
|
||||||
.catch((error) => console.warn("audioElem.play() failed:%o", error));
|
|
||||||
|
|
||||||
// If audio-only mode is enabled, pause it.
|
// If audio-only mode is enabled, pause it.
|
||||||
if (consumer.kind === "video" && !self.videoProduce) {
|
if (consumer.kind === "video" && !self.videoProduce) {
|
||||||
@@ -1013,6 +1102,26 @@ class Taoyao {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证设备
|
* 验证设备
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -163,6 +163,10 @@ public interface Constant {
|
|||||||
* 终端ID
|
* 终端ID
|
||||||
*/
|
*/
|
||||||
String CLIENT_ID = "clientId";
|
String CLIENT_ID = "clientId";
|
||||||
|
/**
|
||||||
|
* 来源终端ID
|
||||||
|
*/
|
||||||
|
String SOURCE_ID = "sourceId";
|
||||||
/**
|
/**
|
||||||
* 路由ID
|
* 路由ID
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ public class MediaConsumeProtocol extends ProtocolRoomAdapter implements Applica
|
|||||||
final String streamId = producer.getStreamId() + "->" + clientId;
|
final String streamId = producer.getStreamId() + "->" + clientId;
|
||||||
body.put(Constant.ROOM_ID, room.getRoomId());
|
body.put(Constant.ROOM_ID, room.getRoomId());
|
||||||
body.put(Constant.CLIENT_ID, clientId);
|
body.put(Constant.CLIENT_ID, clientId);
|
||||||
|
body.put(Constant.SOURCE_ID, producer.getProduceClient().getClientId());
|
||||||
body.put(Constant.STREAM_ID, streamId);
|
body.put(Constant.STREAM_ID, streamId);
|
||||||
body.put(Constant.PRODUCER_ID, producer.getProducerId());
|
body.put(Constant.PRODUCER_ID, producer.getProducerId());
|
||||||
body.put(Constant.TRANSPORT_ID, recvTransport.getTransportId());
|
body.put(Constant.TRANSPORT_ID, recvTransport.getTransportId());
|
||||||
|
|||||||
@@ -1,5 +1,36 @@
|
|||||||
package com.acgist.taoyao.signal.protocol.room;
|
package com.acgist.taoyao.signal.protocol.room;
|
||||||
|
|
||||||
public class RoomClientListProtocol {
|
import java.util.Map;
|
||||||
|
|
||||||
|
import com.acgist.taoyao.boot.annotation.Description;
|
||||||
|
import com.acgist.taoyao.boot.annotation.Protocol;
|
||||||
|
import com.acgist.taoyao.boot.model.Message;
|
||||||
|
import com.acgist.taoyao.signal.client.Client;
|
||||||
|
import com.acgist.taoyao.signal.client.ClientType;
|
||||||
|
import com.acgist.taoyao.signal.flute.media.Room;
|
||||||
|
import com.acgist.taoyao.signal.protocol.ProtocolRoomAdapter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 房间终端列表信令
|
||||||
|
*
|
||||||
|
* @author acgist
|
||||||
|
*/
|
||||||
|
@Protocol
|
||||||
|
@Description(
|
||||||
|
flow = "终端=>信令服务->终端"
|
||||||
|
)
|
||||||
|
public class RoomClientListProtocol extends ProtocolRoomAdapter {
|
||||||
|
|
||||||
|
public static final String SIGNAL = "room::client::list";
|
||||||
|
|
||||||
|
public RoomClientListProtocol() {
|
||||||
|
super("房间终端列表信令", SIGNAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(String clientId, ClientType clientType, Room room, Client client, Client mediaClient, Message message, Map<String, Object> body) {
|
||||||
|
message.setBody(room.clientStatus());
|
||||||
|
client.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,8 +45,11 @@ public class RoomEnterProtocol extends ProtocolRoomAdapter {
|
|||||||
|
|
||||||
public static final String SIGNAL = "room::enter";
|
public static final String SIGNAL = "room::enter";
|
||||||
|
|
||||||
public RoomEnterProtocol() {
|
private final RoomClientListProtocol roomClientListProtocol;
|
||||||
|
|
||||||
|
public RoomEnterProtocol(RoomClientListProtocol roomClientListProtocol) {
|
||||||
super("进入房间信令", SIGNAL);
|
super("进入房间信令", SIGNAL);
|
||||||
|
this.roomClientListProtocol = roomClientListProtocol;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -71,6 +74,10 @@ public class RoomEnterProtocol extends ProtocolRoomAdapter {
|
|||||||
));
|
));
|
||||||
room.broadcast(message);
|
room.broadcast(message);
|
||||||
log.info("进入房间:{} - {}", clientId, room.getRoomId());
|
log.info("进入房间:{} - {}", clientId, room.getRoomId());
|
||||||
|
// 推送房间用户信息
|
||||||
|
final Message roomClientListMessage = this.roomClientListProtocol.build();
|
||||||
|
roomClientListMessage.setBody(room.clientStatus());
|
||||||
|
client.push(roomClientListMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user