日常开发中遇到过需要本地录制的需求,现在将相关代码记录下
其中fix-webm-duration用来进行webm格式视频的进度条可拖拽
开启设备hooks部分
//获取媒体报错类型
export const mediaStatusMap = {
'0': '正在获取设备权限以及兼容的设备列表',
'1': '浏览器权限开启成功,请选择设备后开启',
'2': '设备开启中,请稍候',
'3': '设备开启成功',
'6': '视频输入被中断,若正在录制则当前部分已保存,请重新插拔后重试!',
'7': '查询设备列表失败,请重试',
'8': '未查询到符合条件的设备,请连接文档内提供的设备后重试',
'9': '当前浏览器版本过低,请下载谷歌70版本以上的浏览器',
'AbortError': '设备异常无法访问,请更换设备后重试!',
'NotAllowedError': '摄像头开启失败,请查看权限是否开启后重试!',
'NotFoundError': '找不到满足请求参数的媒体类型,或未连接使用文档内限定的设备,请连接后重试!',
'OverconstrainedError': '配置参数当前设备无法满足,请检查设备是否为文档内限定的设备',
'SecurityError': '设备开启权限被用户禁止,请开启后重试!',
'TypeError': '开启摄像头配置分辨率参数异常!',//constraints参数类型错误,该情况不会出现
'NotReadableError': '无法启动视频,请重试!', //两台设备拔插切换可能会导致该问题
};
//设备开启的hooks
export const useOpenUserMedia = (ifOpenAudio = false) => {
const [mediaStatus, setMediaStatus,getMediaStatus] = useGetStateHook('0'); //设备状态
const [mediaList, setMediaList] = useState<any[]>([]); //设备列表
const streamRef = useRef(null) //开启的摄像头的数据
useEffect(()=>{
return ()=>{
clearMedia() //页面销毁时需要清除
}
},[])
//清除摄像头开启的track
const clearMedia = () =>{
if(streamRef.current){
streamRef.current.getTracks().forEach(item=>{
item.stop()
})
streamRef.current = null
}
}
//获取设备列表(需要预先申请下权限)
const getMediaList = () => {
setMediaStatus('0');//设置设备状态为获取权限
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { //判断当前 浏览器是否支持开启设备
clearMedia()
//预开启设备若之前未开放权限则会弹出权限获取弹框
navigator.mediaDevices.getUserMedia({ audio: ifOpenAudio, video: true }).then((a) => {
setMediaStatus('1');//设置设备状态为权限开启成功
const tracks = a?.getTracks();
if (tracks && tracks.length > 0) { //关闭当前开启的流,保证第一次请求设备仅坐于权限开启
tracks.forEach((item) => {
item.stop();
});
}
navigator.mediaDevices.enumerateDevices().then((res) => {
const _mediaList = res.filter(item => item.kind === 'videoinput');
const compatibleMedia = arrayUnion(_mediaList.map((item) => ({ value: item.deviceId, label: item.label })));
if (compatibleMedia.length <= 0) setMediaStatus('8');
setMediaList(compatibleMedia);
}).catch(() => {
setMediaList([]);
setMediaStatus('7');
});
}).catch(({ name }) => {
setMediaList([]);
setMediaStatus(name);
});
} else {
setMediaStatus('9');//设置设备状态为开启异常
}
};
//开启指定的设备
const openMedia = (success, mediaConfig) => {
setMediaStatus('2');
clearMedia()
const ratio = mediaConfig.media_ratio?.split('×')
navigator.mediaDevices.getUserMedia({
audio: ifOpenAudio, //是否开启音频,***开启音频会导致浏览器手动关闭权限无法拦截问题
video: {
deviceId: mediaConfig.media_id,
width: Number(ratio[0]),
height: Number(ratio[1]),
},
}).then((stream) => {
streamRef.current = stream
setMediaStatus('3'); //设置设备状态为获取成功
success(stream);
}).catch(({ name }) => setMediaStatus(name));
};
return { getMediaList, mediaList, mediaStatus, setMediaStatus, openMedia,getMediaStatus };
};
其余用到的部分hooks
//抄写aHook useGetState 可以通过getState 获取实时改变后的state值
export const useGetStateHook = (initVal) => {
const [state, setState] = useState(initVal)
const ref = useRef(initVal)
const setStateCopy = (newVal) => {
ref.current = newVal
setState(newVal)
}
const getState = () => ref.current
return [state, setStateCopy, getState]
}
具体调用部分ts代码
import React, { useEffect, useRef, useState } from 'react';
import { mediaStatusMap, useOpenUserMedia } from '@/components/utils';
import { v4 as uuidv4 } from 'uuid';
import fixWebmDuration from '../fixWebm/fix-webm-duration';
import { useGetStateHook } from '@/common/vpcUtils/a-hooks';
const srcObject = useRef<any>(); //视频流页面反显存储
const timer = useRef<any>(); //倒计时以及录制时长控制计时器
const mediaRecorder = useRef<any>(); //视频录制模组创建,仅支持webm
const recordedBlobs = useRef<any>([]); //视频录制数据存储
const startTime = useRef<any>(null); //开始录制时间
const fileBlob = useRef<any>(null); //视频录制完成后的blob文件
const fileName = useRef<any>(null); //视频名称
const {mediaStatus, setMediaStatus,getMediaList,openMedia,mediaList,getMediaStatus} = useOpenUserMedia(true); //设备状态
const [recordingStatus, setRecordingStatus,getRecordingStatus] = useGetStateHook('-1'); //录制状态
const [countdownNum, setCountdownNum] = useState(-1); //倒计时
const [recordingTime, setRecordingTime] = useState(180); //录制时间
useEffect(() => {
getMediaList(); //获取设备列表
return () => { //页面销毁时执行
mediaRecorder.current && mediaRecorder.current.state === 'recording' && mediaRecorder.current?.stop(); //关闭视频录制(仅录制中触发)
const synth = window.speechSynthesis;
synth.cancel(); // 关闭语音播报
if (timer.current) { //清除计时器
clearInterval(timer.current);
timer.current = '';
}
};
}, []);
useEffect(() => {
if (countdownNum === 4) { //倒计时结束触发
clearInterval(timer.current); //清除计时器
timer.current = '';
setRecordingStatus('0'); //设置录制状态为录制中
toggleRecording(); //开始录制前置
}
}, [countdownNum]);
useEffect(() => {
if (recordingTime <= 0) { //录制计时器结束触发
stopRecording(); //停止录制
}
}, [recordingTime]);
//开始录制倒计时
const startCountdown = () => {
setCountdownNum(0); //重置倒计时时间
setRecordingTime(180); //重置录制时间
setRecordingStatus('-1'); //重置录制状态
fileBlob.current = null;
startTime.current = null;
fileName.current = null;
fileShardingId.current = null;
fileShardingNum.current = 0;
fileShardingData.current = {};
timer.current = setInterval(() => {
setCountdownNum(a => a + 1);
}, 1000); //倒计时计时器
};
//开启设备
const useMedia = () => {
openMedia((stream) => {
//开启音频时该方法插拔有效,改变权限无效
if (!stream.oninactive) { //设备被外力断开或设备管理器禁用 捕捉事件
stream.oninactive = () => {
const synth = window.speechSynthesis;
synth.cancel(); //关闭语音播报
setCountdownNum(-1); //重置倒计时时间
getMediaStatus()!=='0' && setMediaStatus('6'); //设置设备状态为断开链接
stopRecording();
if (timer.current) { //清除计时器
clearInterval(timer.current);
timer.current = '';
}
};
}
srcObject.current.srcObject = stream; //设置反显页面video的实时视频流
const synth = window.speechSynthesis; //创建语音播报内容
const voices = new window.SpeechSynthesisUtterance();
voices.lang = 'zh-CN';
voices.text = '请调整摄像头位置后点击开始录制';
synth.speak(voices); //播放语音播报
},{ratio: '3840×1080'});
};
//视频录制结束
const stopRecording = () => {
if(getRecordingStatus() !== '0') return;
if(timer.current){
clearInterval(timer.current); //清除计时器
timer.current = '';
}
setRecordingStatus('1'); //设置录制状态为已录制
mediaRecorder.current && mediaRecorder.current.state === 'recording' && mediaRecorder.current?.stop();//触发停止方法
fileName.current = `双录视频${uuidv4()}.webm`;
};
//开始录制前置
const toggleRecording = () => {
startRecording(); //开始录制
timer.current = setInterval(() => {
setRecordingTime(a => a - 1);
}, 1000); //录制视频计时器
};
//开始录制
const startRecording = () => {
recordedBlobs.current = []; // 重置录制数据
try {
mediaRecorder.current = new MediaRecorder(srcObject.current.srcObject, {
audioBitsPerSecond: 128000,
videoBitsPerSecond: 1500000,
mimeType: 'video/webm',
}); //创建录制模组
} catch (e) {
message.error('MediaRecorder创建失败');
return;
}
// 录制中
mediaRecorder.current.ondataavailable = event => {
if (event.data && event.data.size > 0) {
recordedBlobs.current.push(event.data);
}
};
// 录制结束回调
mediaRecorder.current.onstop = event => {
console.log('Recorder stopped: ', event);
const duration = new Date().getTime() - startTime.current;
const blob = new Blob(recordedBlobs.current, {
type: 'video/webm',
}); //录制视频转blob对象
fixWebmDuration(blob, duration, (fixedBlob) => {
const _recordingVideoUrl = URL.createObjectURL(fixedBlob); //创建页面显示本地url
fileBlob.current = fixedBlob;
console.log(_recordingVideoUrl) //设置视频url
});
};
// 开始录制
mediaRecorder.current.start(10);
startTime.current = new Date().getTime();
};
对应内容的dom内容
<video style={{ width: '100%', height: '100%' }} {...{ ref: srcObject }} autoPlay muted />
{
mediaStatus === '3' && <div>
● 请调整摄像头角度,并点击开始录制
{
countdownNum === -1 &&
<Button type={'link'} onClick={() => startCountdown()}>开始录制</Button>
}
</div>
}
<div>当前设备状态: {mediaStatusMap[mediaStatus]}</div>