[*] 本地录像
This commit is contained in:
@@ -161,7 +161,6 @@ export default {
|
||||
},
|
||||
async sessionCall() {
|
||||
this.taoyao.sessionCall(this.room.callClientId);
|
||||
// this.taoyao.sessionCall(this.room.callClientId, false, false);
|
||||
this.roomVisible = false;
|
||||
},
|
||||
async roomCreate() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* 音频默认配置
|
||||
* TODO:MediaStreamTrack.applyConstraints().then().catch();
|
||||
* let setting = {
|
||||
* const setting = {
|
||||
* autoGainControl: true,
|
||||
* noiseSuppression: true
|
||||
* }
|
||||
|
||||
@@ -9,10 +9,11 @@
|
||||
<el-button @click="taoyao.mediaProducerPause(audioProducer.id)" v-show="audioProducer && !audioProducer.paused" type="primary" title="关闭麦克风" :icon="Microphone" circle class="mic" :style="{'--volume': client.volume}" />
|
||||
<el-button @click="taoyao.mediaProducerResume(videoProducer.id)" v-show="videoProducer && videoProducer.paused" type="danger" title="打开摄像头" :icon="VideoPlay" circle />
|
||||
<el-button @click="taoyao.mediaProducerPause(videoProducer.id)" v-show="videoProducer && !videoProducer.paused" type="primary" title="关闭摄像头" :icon="VideoPause" circle />
|
||||
<el-button @click="exchangeVideoSource" :icon="Refresh" circle title="交换媒体" />
|
||||
<el-button :icon="Camera" circle title="拍照" />
|
||||
<el-button :icon="VideoCamera" circle title="录像" />
|
||||
<el-button @click="taoyao.mediaProducerStatus()" :icon="InfoFilled" circle title="媒体信息" />
|
||||
<el-button @click="exchangeVideoSource" :icon="Refresh" circle title="交换媒体" />
|
||||
<el-button @onclick="localPhotograph" :icon="Camera" circle title="拍照" />
|
||||
<el-button @onclick="localClientRecord" :icon="VideoCamera" circle title="录像" :type="clientRecord ? 'danger' : ''" />
|
||||
<el-button @click="taoyao.controlServerRecord(client.clientId, (serverRecord = !serverRecord))" :icon="MostlyCloudy" circle title="录像" :type="serverRecord ? 'danger' : ''" />
|
||||
<el-button @click="taoyao.mediaProducerStatus()" :icon="InfoFilled" circle title="媒体信息" />
|
||||
<el-popover placement="top" :width="240" trigger="hover">
|
||||
<template #reference>
|
||||
<el-button>视频质量</el-button>
|
||||
@@ -37,6 +38,7 @@ import {
|
||||
Microphone,
|
||||
VideoCamera,
|
||||
CircleClose,
|
||||
MostlyCloudy,
|
||||
} from "@element-plus/icons-vue";
|
||||
export default {
|
||||
name: "LocalClient",
|
||||
@@ -51,12 +53,15 @@ export default {
|
||||
Microphone,
|
||||
VideoCamera,
|
||||
CircleClose,
|
||||
MostlyCloudy,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
audio: null,
|
||||
video: null,
|
||||
clientRecord: false,
|
||||
serverRecord: false,
|
||||
audioStream: null,
|
||||
videoStream: null,
|
||||
dataProducer: null,
|
||||
@@ -78,6 +83,13 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
localPhotograph() {
|
||||
this.taoyao.localPhotograph(this.video);
|
||||
},
|
||||
localClientRecord() {
|
||||
this.clientRecord = !this.clientRecord;
|
||||
this.taoyao.localClientRecord(this.audioStream, this.videoStream, this.clientRecord);
|
||||
},
|
||||
exchangeVideoSource() {
|
||||
// TODO:文件支持
|
||||
this.taoyao.videoSource = this.taoyao.videoSource === "camera" ? "screen" : "camera";
|
||||
@@ -85,8 +97,16 @@ export default {
|
||||
},
|
||||
media(track, producer) {
|
||||
if(track.kind === "audio") {
|
||||
// 不用加载音频
|
||||
this.audioProducer = producer;
|
||||
if (this.audioStream) {
|
||||
this.audioStream.getAudioTracks().forEach(oldTrack => {
|
||||
console.debug("关闭旧的媒体:", oldTrack);
|
||||
oldTrack.stop();
|
||||
});
|
||||
}
|
||||
this.audioStream = new MediaStream();
|
||||
this.audioStream.addTrack(track);
|
||||
// 不用加载音频
|
||||
} else if (track.kind === "video") {
|
||||
this.videoProducer = producer;
|
||||
if (this.videoStream) {
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
<el-button @click="taoyao.mediaConsumerPause(audioConsumer.id)" v-show="audioConsumer && !audioConsumer.paused" type="primary" title="关闭麦克风" :icon="Microphone" circle class="mic" :style="{'--volume': client.volume}" />
|
||||
<el-button @click="taoyao.mediaConsumerResume(videoConsumer.id)" v-show="videoConsumer && videoConsumer.paused" type="danger" title="打开摄像头" :icon="VideoPlay" circle />
|
||||
<el-button @click="taoyao.mediaConsumerPause(videoConsumer.id)" v-show="videoConsumer && !videoConsumer.paused" type="primary" title="关闭摄像头" :icon="VideoPause" circle />
|
||||
<el-button @click="taoyao.controlPhotograph(client.clientId)" :icon="Camera" circle title="拍照" />
|
||||
<el-button @click="taoyao.controlRecord(client.clientId, (record = !record))" :icon="VideoCamera" circle title="录像" :type="record ? 'danger' : ''" />
|
||||
<el-button @click="taoyao.mediaConsumerStatus()" :icon="InfoFilled" circle title="媒体信息" />
|
||||
<el-button @click="taoyao.controlPhotograph(client.clientId)" :icon="Camera" circle title="拍照" />
|
||||
<el-button @click="taoyao.controlClientRecord(client.clientId, (clientRecord = !clientRecord))" :icon="VideoCamera" circle title="终端录像" :type="clientRecord ? 'danger' : ''" />
|
||||
<el-button @click="taoyao.controlServerRecord(client.clientId, (serverRecord = !serverRecord))" :icon="MostlyCloudy" circle title="服务端录像" :type="serverRecord ? 'danger' : ''" />
|
||||
<el-button @click="taoyao.mediaConsumerStatus()" :icon="InfoFilled" circle title="媒体信息" />
|
||||
<el-popover placement="top" :width="240" trigger="hover">
|
||||
<template #reference>
|
||||
<el-button>视频质量</el-button>
|
||||
@@ -40,6 +41,7 @@ import {
|
||||
Microphone,
|
||||
VideoCamera,
|
||||
CircleClose,
|
||||
MostlyCloudy,
|
||||
} from "@element-plus/icons-vue";
|
||||
export default {
|
||||
name: "RemoteClient",
|
||||
@@ -54,13 +56,15 @@ export default {
|
||||
Microphone,
|
||||
VideoCamera,
|
||||
CircleClose,
|
||||
MostlyCloudy,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
audio: null,
|
||||
video: null,
|
||||
record: false,
|
||||
clientRecord: false,
|
||||
serverRecord: false,
|
||||
audioStream: null,
|
||||
videoStream: null,
|
||||
dataConsumer: null,
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
<el-button @click="taoyao.sessionPause(client.id, 'audio')" v-show="audioStream && client.remoteAudioEnabled" type="primary" title="关闭远程麦克风" :icon="Microphone" circle class="mic" :style="{'--volume': client.volume}" />
|
||||
<el-button @click="taoyao.sessionResume(client.id, 'video')" v-show="videoStream && !client.remoteVideoEnabled" type="danger" title="打开远程摄像头" :icon="VideoPlay" circle />
|
||||
<el-button @click="taoyao.sessionPause(client.id, 'video')" v-show="videoStream && client.remoteVideoEnabled" type="primary" title="关闭远程摄像头" :icon="VideoPause" circle />
|
||||
<el-button @click="taoyao.controlPhotograph(client.clientId)" :icon="Camera" circle title="拍照" />
|
||||
<el-button @click="taoyao.controlRecord(client.clientId, (record = !record))" :icon="VideoCamera" circle title="录像" :type="record ? 'danger' : ''" />
|
||||
<el-button @click="taoyao.controlPhotograph(client.clientId)" :icon="Camera" circle title="拍照" />
|
||||
<el-button @click="taoyao.controlClientRecord(client.clientId, (clientRecord = !clientRecord))" :icon="VideoCamera" circle title="录像" :type="clientRecord ? 'danger' : ''" />
|
||||
<el-popover placement="top" :width="240" trigger="hover">
|
||||
<template #reference>
|
||||
<el-button>视频质量</el-button>
|
||||
@@ -64,7 +64,8 @@ export default {
|
||||
return {
|
||||
audio: null,
|
||||
video: null,
|
||||
record: false,
|
||||
clientRecord: false,
|
||||
serverRecord: false,
|
||||
audioStream: null,
|
||||
videoStream: null,
|
||||
};
|
||||
|
||||
@@ -509,6 +509,10 @@ class Taoyao extends RemoteClient {
|
||||
remoteClients = new Map();
|
||||
// 会话终端
|
||||
sessionClients = new Map();
|
||||
// 本地录像机
|
||||
mediaRecorder;
|
||||
// 本地录像数据
|
||||
mediaRecorderChunks = [];
|
||||
|
||||
constructor({
|
||||
name,
|
||||
@@ -811,6 +815,21 @@ class Taoyao extends RemoteClient {
|
||||
}
|
||||
return stream;
|
||||
}
|
||||
async getAudioTrack() {
|
||||
const self = this;
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: self.audioConfig,
|
||||
});
|
||||
// TODO:首个?
|
||||
const track = stream.getAudioTracks()[0];
|
||||
// TODO:验证修改API audioTrack.applyCapabilities
|
||||
console.debug(
|
||||
"音频信息:",
|
||||
track.getSettings(),
|
||||
track.getCapabilities()
|
||||
);
|
||||
return track;
|
||||
}
|
||||
async getVideoTrack() {
|
||||
let track;
|
||||
const self = this;
|
||||
@@ -819,11 +838,11 @@ class Taoyao extends RemoteClient {
|
||||
// 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,
|
||||
});
|
||||
// TODO:首个?
|
||||
track = stream.getVideoTracks()[0];
|
||||
// TODO:验证修改API videoTrack.applyCapabilities
|
||||
console.debug(
|
||||
@@ -939,20 +958,36 @@ class Taoyao extends RemoteClient {
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 录像
|
||||
* 终端录像信令
|
||||
*
|
||||
* @param {*} clientId
|
||||
* @param {*} enabled
|
||||
*/
|
||||
controlRecord(clientId, enabled) {
|
||||
controlClientRecord(clientId, enabled) {
|
||||
const me = this;
|
||||
me.push(
|
||||
protocol.buildMessage("control::record", {
|
||||
protocol.buildMessage("control::client::record", {
|
||||
to: clientId,
|
||||
enabled: enabled
|
||||
})
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 服务端录像信令
|
||||
*
|
||||
* @param {*} clientId
|
||||
* @param {*} enabled
|
||||
*/
|
||||
controlServerRecord(clientId, enabled) {
|
||||
const me = this;
|
||||
me.push(
|
||||
protocol.buildMessage("control::server::record", {
|
||||
to: clientId,
|
||||
roomId: me.roomId,
|
||||
enabled: enabled
|
||||
})
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 终端音量信令
|
||||
*
|
||||
@@ -1923,20 +1958,7 @@ class Taoyao extends RemoteClient {
|
||||
let track;
|
||||
try {
|
||||
console.debug("打开麦克风");
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: self.audioConfig,
|
||||
});
|
||||
const tracks = stream.getAudioTracks();
|
||||
if (tracks.length > 1) {
|
||||
console.warn("多个音频轨道");
|
||||
}
|
||||
track = tracks[0];
|
||||
// TODO:验证修改API audioTrack.applyCapabilities
|
||||
console.debug(
|
||||
"音频信息:",
|
||||
track.getSettings(),
|
||||
track.getCapabilities()
|
||||
);
|
||||
let track = self.getAudioTrack();
|
||||
this.audioProducer = await this.sendTransport.produce({
|
||||
track,
|
||||
codecOptions: {
|
||||
@@ -2009,13 +2031,13 @@ class Taoyao extends RemoteClient {
|
||||
* TODO:重复点击
|
||||
*/
|
||||
async produceVideo() {
|
||||
console.debug("打开摄像头");
|
||||
const self = this;
|
||||
if (self.videoProduce && self.mediasoupDevice.canProduce("video")) {
|
||||
if (self.videoProducer) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
console.debug("打开摄像头");
|
||||
let track = await self.getVideoTrack();
|
||||
let codec;
|
||||
let encodings;
|
||||
@@ -2215,9 +2237,13 @@ class Taoyao extends RemoteClient {
|
||||
});
|
||||
if (!audioEnabled && self.audioProduce) {
|
||||
self.callbackError("没有音频媒体设备");
|
||||
// 强制修改
|
||||
self.audioProduce = false;
|
||||
}
|
||||
if (!videoEnabled && self.videoProduce) {
|
||||
self.callbackError("没有视频媒体设备");
|
||||
// 强制修改
|
||||
self.videoProduce = false;
|
||||
}
|
||||
} else {
|
||||
self.callbackError("没有媒体设备");
|
||||
@@ -2251,23 +2277,22 @@ class Taoyao extends RemoteClient {
|
||||
/**
|
||||
* 发起会话
|
||||
*
|
||||
* @param {*} clientId 接收者ID
|
||||
* @param {*} audioEnabled 是否打开音频
|
||||
* @param {*} videoEnabled 是否打开视频
|
||||
* @param {*} clientId 接收者ID
|
||||
*/
|
||||
async sessionCall(clientId, audioEnabled = true, videoEnabled = true) {
|
||||
async sessionCall(clientId) {
|
||||
const me = this;
|
||||
if (!clientId) {
|
||||
this.callbackError("无效终端");
|
||||
return;
|
||||
}
|
||||
me.checkDevice();
|
||||
const response = await me.request(
|
||||
protocol.buildMessage("session::call", {
|
||||
clientId
|
||||
})
|
||||
);
|
||||
const { name, sessionId } = response.body;
|
||||
const session = new Session({name, clientId, sessionId, audioEnabled, videoEnabled});
|
||||
const session = new Session({name, clientId, sessionId, audioEnabled: me.audioProduce, videoEnabled: me.videoProduce});
|
||||
this.sessionClients.set(sessionId, session);
|
||||
}
|
||||
|
||||
@@ -2434,6 +2459,101 @@ class Taoyao extends RemoteClient {
|
||||
return peerConnection;
|
||||
}
|
||||
|
||||
/**
|
||||
* 本地截图
|
||||
*
|
||||
* @param {*} video 视频
|
||||
*/
|
||||
localPhotograph(video) {
|
||||
const me = this;
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
const context = canvas.getContext('2d');
|
||||
context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
|
||||
const dataURL = canvas.toDataURL('images/png');
|
||||
const download = document.createElement('a');
|
||||
download.href = dataURL;
|
||||
download.download = 'taoyao.png';
|
||||
download.style.display = 'none';
|
||||
document.body.appendChild(download);
|
||||
download.click();
|
||||
download.remove();
|
||||
}
|
||||
|
||||
// 'video/webm;codecs=aac,vp8',
|
||||
// 'video/webm;codecs=aac,vp9',
|
||||
// 'video/webm;codecs=aac,h264',
|
||||
// 'video/webm;codecs=pcm,vp8',
|
||||
// 'video/webm;codecs=pcm,vp9',
|
||||
// 'video/webm;codecs=pcm,h264',
|
||||
// 'video/webm;codecs=opus,vp8',
|
||||
// 'video/webm;codecs=opus,vp9',
|
||||
// 'video/webm;codecs=opus,h264',
|
||||
// 'video/mp4;codecs=aac,vp8',
|
||||
// 'video/mp4;codecs=aac,vp9',
|
||||
// 'video/mp4;codecs=aac,h264',
|
||||
// 'video/mp4;codecs=pcm,vp8',
|
||||
// 'video/mp4;codecs=pcm,vp9',
|
||||
// 'video/mp4;codecs=pcm,h264',
|
||||
// 'video/mp4;codecs=opus,vp8',
|
||||
// 'video/mp4;codecs=opus,vp9',
|
||||
// 'video/mp4;codecs=opus,h264',
|
||||
// MediaRecorder.isTypeSupported(mimeType)
|
||||
|
||||
/**
|
||||
* 本地录制
|
||||
*
|
||||
* video.captureStream().getTracks().forEach((v) => stream.addTrack(v));
|
||||
*
|
||||
* @param {*} audio 音频
|
||||
* @param {*} video 视频
|
||||
* @param {*} enabled 是否录制
|
||||
*/
|
||||
localClientRecord(audio, video, enabled) {
|
||||
const me = this;
|
||||
if (enabled) {
|
||||
if (me.mediaRecorder) {
|
||||
return;
|
||||
}
|
||||
const stream = new MediaStream();
|
||||
if(audio) {
|
||||
audio.getAudioTracks().forEach(track => stream.addTrack(track));
|
||||
}
|
||||
if(video) {
|
||||
video.getVideoTracks().forEach(track => stream.addTrack(track));
|
||||
}
|
||||
me.mediaRecorder = new MediaRecorder(stream, {
|
||||
audioBitsPerSecond: 128 * 1000,
|
||||
videoBitsPerSecond: 2400 * 1000,
|
||||
mimeType: 'video/webm;codecs=opus,h264',
|
||||
});
|
||||
mediaRecorder.onstop = function (e) {
|
||||
const blob = new Blob(me.mediaRecorderChunks);
|
||||
const objectURL = URL.createObjectURL(blob);
|
||||
const download = document.createElement('a');
|
||||
download.href = objectURL;
|
||||
download.download = 'taoyao.mp4';
|
||||
download.style.display = 'none';
|
||||
document.body.appendChild(download);
|
||||
download.click();
|
||||
download.remove();
|
||||
URL.revokeObjectURL(objectURL);
|
||||
me.mediaRecorderChunks = [];
|
||||
};
|
||||
mediaRecorder.ondataavailable = (e) => {
|
||||
me.mediaRecorderChunks.push(e.data);
|
||||
};
|
||||
me.mediaRecorder.start();
|
||||
} else {
|
||||
if (!me.mediaRecorder) {
|
||||
return;
|
||||
}
|
||||
me.mediaRecorder.stop();
|
||||
me.mediaRecorder = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭视频房间媒体
|
||||
*/
|
||||
@@ -2504,3 +2624,4 @@ class Taoyao extends RemoteClient {
|
||||
}
|
||||
|
||||
export { Taoyao };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user