[*] 本地录像

This commit is contained in:
acgist
2023-06-02 07:17:10 +08:00
parent a06f6a251f
commit becc68b05f
19 changed files with 404 additions and 179 deletions

View File

@@ -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() {

View File

@@ -1,7 +1,7 @@
/**
* 音频默认配置
* TODOMediaStreamTrack.applyConstraints().then().catch();
* let setting = {
* const setting = {
* autoGainControl: true,
* noiseSuppression: true
* }

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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,
};

View File

@@ -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 };