获取本地设备的媒体流
获取摄像头、屏幕共享的媒体流
function startRecording(){
let constraints = {
video: {
cursor: "never"
}, // 视频信息的设置
audio: false, // 是否包含音频信息
logicalSurface: false, // 设置是否包含所选屏幕外区域的一些信息
}
// navigator.mediaDevices.getDisplayMedia(constraints) // 录制屏幕共享
// 录制摄像头
navigator.mediaDevices.getUserMedia({
audio: true,
video: true,
}).then(stream => {
this.mediaStream = stream;
this.startRecord(stream)
})
}
同时录制屏幕共享及麦克风
屏幕共享是一个mediaStream
, 麦克风和摄像头是另一个mediaStream
, 需要将两个媒体流进行合并,方法就是将麦克风的音频轨道添加到是屏幕共享的mediaStream
中
function screenAudioRecording(){
navigator.mediaDevices.getDisplayMedia({ video: true, audio: false }).then(stream => {
this.screenStream = stream;
navigator.mediaDevices.getUserMedia({ video: false, audio: true }).then(audioStream => {
// 屏幕共享和麦克风进行混流
audioStream.getTracks().forEach(track => {
this.screenStream.addTrack(track);
})
// 此时的screenStream已包含音频轨道
this.startRecord(this.screenStream)
})
})
}
合并屏幕共享和摄像头
屏幕共享和摄像头是两个带视频的mediaStream
, 直接使用添加轨道的方法,一个流回完全覆盖另一个流。可以借助video/canvas
实现:
补充几个知识:
- video可直接播放媒体流
videoElement.srcObject = mediaStream
- canvas可直接绘制video内容, 并且可以指定位置和宽高
const ctx = canvas.getContext('2d');
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
- canvas可导出媒体流
const stream = canvas.captureStream();
- 将音频轨道添加到该媒体流内
mediaStream.getAudioTracks().forEach(track => stream.addTrack(track))
这样就有了思路~,
- 首先将屏幕共享的流(下面称
screenStream
)和摄像头的流(下面称cameraStream
)播放到两个<video>
上 - 然后将
screenStream
完全绘制到canvas
上(撑满整个canvas
),然后将cameraStream
也绘制到canvas
的指定位置上(例如右下角, 利用drawImage(x, y, width, height)
设置位置和尺寸). 这样就绘制好了一帧的影像 - 再用
requestAnimationFrame
不停的更新canvas内容, 即可实现再canvas上播放混合流 - 再使用
canvas.captureStream()
获取合并后的媒体流 - 最后把音频添加进去. 使用
mediaStream.getAudioTracks()
获取音频轨道, 插入的行的媒体流中
async function mergeStream(screenStream, cameraStream) {
const v1 = document.createElement('video')
v1.srcObject = screenStream
v1.addEventListener('play', () => {
render = () => {
if (screenStream) {
ctx.drawImage(v1, 0, 0, canvas.width, canvas.height)
window.requestAnimationFrame(render)
}
}
render();
})
v1.play()
// 摄像头
video2.srcObject = cameraStream
video2.addEventListener('play', () => {
render = () => {
if (cameraStream) {
ctx.drawImage(video2, 100, 100, 100, 100)
window.requestAnimationFrame(render)
}
}
render();
})
video2.play()
// 新的媒体流
const newStream = canvas.captureStream()
cameraStream.getAudioTracks().forEach(track => newStream.addTrack(track))
screenStream.getAudioTracks().forEach(track => newStream.addTrack(track))
return newStream
}
借助video-stream-merger
可以更方便的实现
import { VideoStreamMerger } from 'video-stream-merger'
const begin = async () => {``
merger = new VideoStreamMerger({
width: 680, // Width of the output video
height: 380, // Height of the output video
fps: 25, // Video capture frames per second
clearRect: true
})
// 获取屏幕共享
try {
screen = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true })
merger.addStream(screen, {
x: 0,
y: 0,
width: merger.width,
height: merger.height
})
} catch (e) {
console.error('出错了', e);
screen = null
}
// 获取摄像头
try {
camera = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
merger.addStream(camera, {
x: merger.width - 100,
y: merger.height - 100,
width: 100,
height: 100
})
} catch (e) {
console.error('出错了', e);
camera = null
}
merger.start()
// 在video中播放
const videoDom = document.querySelector('video')
videoDom.srcObject = merger.result
videoDom.play()
}
下载录制的视频
function download(chunk) {
let url = URL.createObjectURL(chunk);
// 创建隐藏的可下载链接
var eleLink = document.createElement('a');
eleLink.download = 'fileName.webm';
// eleLink.style.display = 'none';
// 字符内容转变成blob地址
eleLink.href = url
// 触发点击
console.log('url', url)
document.body.appendChild(eleLink);
eleLink.click();
// 然后移除
document.body.removeChild(eleLink);
}
媒体流录制
直接录制
借助MediaRecorder
, 可录制webm
的视频
let recorder = null
// 开始录制
const startRecord = (stream) => {
recorder = new MediaRecorder(stream)
recorder.ondataavailable = e => {
download(e.data)
}
recorder.start()
}
// 结束录制
const stopRecord = () => {
recorder.stop()
}
大视频录制上传
如果录制时间很长,生成的录制文件就会很大,大文件再录制结束后再上传会耗用大量时间,并且上传过程中如果网络问题造成上传失败,就会前功尽弃,重新上传。
为了解决这个问题,可以再录制的时候就进行上传,每过n
秒就生成视频文件进行上传,大量降低上传时间,即时上传失败也能进行断点续传。
let recorder = null
// 开始录制
const startRecord = (stream) => {
let index = 0
recorder = new MediaRecorder(stream)
recorder.ondataavailable = e => {
// 每次上传10s视频, 录制结束后通知后台 合并视频片段
upload(e.data, index++) // index 为当前片段的索引
}
recorder.start(10000) // 每隔10s生成一个视频片段(触发一次ondataavailable)
}
// 结束录制
const stopRecord = () => {
recorder.stop()
}
合并录制的视频片段
将每次录制的视频片段按顺序合并成一个Blob
即可完成视频的拼接。前端合并演示如下,后端实现方式也是blob合并
const startRecord = (stream) => {
let blob = null
recorder = new MediaRecorder(stream)
recorder.ondataavailable = e => {
// 将当前生成的chunk与之前的chunk合并成一个blob
blob = new Blob([blob ? blob : null, e.data], {
type: "video/x-matroska;codecs=avc1,opus"
})
if (!recording.value) { // 当停止录制时,下载合并的blob
download(blob, 'blob.webm')
}
}
// recorder.start(time > 0 ? time : undefined)
recorder.start(3000) // 每3s生成一个视频片段
recording.value = true
}
录制问题
使用以上方法录制的视频格式为wbem
, 该格式视频无法快进,且ios
端无法播放,需要转码.
MediaRecorder()
构造函数会创建一个对指定的 MediaStream 进行录制
function recoder (stream) {
var options = {
audioBitsPerSecond : 128000,
videoBitsPerSecond : 2500000,
mimeType : 'video/mp4'
}
var mediaRecorder = new MediaRecorder(stream,options);
m = mediaRecorder;
}
仓库demo地址
mediaStream-merge-record (github.com)
注
由于浏览器安全问题,navigator
只有在本地localhost
或者https
下才能获取到用户媒体信息
webm视频可在chrome内直接播放