html调用mediaDevices进行视频录制

90 阅读5分钟

日常开发中遇到过需要本地录制的需求,现在将相关代码记录下

其中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>