前端获取设备视频流踩坑实战记录

 更新时间:2026年02月12日 11:09:14   作者:悠哉摸鱼大王  
在前端开发中,使用JavaScript读取视频流并将其保存为视频文件是一个常见的需求,尤其是在涉及用户上传、本地处理和下载的场景中,这篇文章主要介绍了前端获取设备视频流踩坑实战的相关资料,需要的朋友可以参考下

前言

众所周知,当使用http访问设备摄像头、麦克风等,需要在浏览器中配置安全策略。但是也会有其他问题。

最近,博主遇到了这样一个问题,在本地开发时,可以正常访问摄像头和麦克风,在局域网内,其他用户通过IP访问我的开发环境,在配置了浏览器安全策略后,也可以访问麦克风摄像头。但是当项目上线时,在同样未配置证书的情况下,使用http访问前端,则无法访问摄像头,调用截图方法时却能正确调用,可以拿到当前视频帧,浏览器也没有任何报错。

本来是通过https访问的,但是由于后端未通过加密通道传输,项目也没有正式上线,只是处于demo状态,所以还是改为http访问。如果一定要通过https访问,那么访问接口的地址也必须是https/wss,否则属于混合内容,会被浏览器拦截。 使用https的情况下,有两种解决方案:

  1. 后端需要支持TLS握手并提供“可信证书 + 主体匹配”(证书的 SAN 需匹配域名/IP)。否则会变成 TLS/证书错误(如NET::ERR_CERT_AUTHORITY_INVALID)
  2. 在前端同源的网关/Ingress/Nginx 暴露一个 WSS 路径(同源同证书),再反向代理到后端。

排除问题

一、检查元素大小是否被影响?

通过devtools发现元素布局正常,大小无变化,未被遮挡。

二、没有正确设置autoplay/playsinline属性

在一些浏览器中,尤其是移动端,如果没有 autoplay(页面加载后“尝试”自动播放视频。)muted(将音轨静音)playsinline(允许在页面内联播放) 属性,视频流会卡在“有流但不播放”的状态,导致黑屏但能截图。 但是我全加了,排除

三、检查 CSP、安全头或者 iframe sandbox 限制

如果视频是在 iframe 或某个带有 CSP 头的环境中(例如 Content-Security-Policy: default-src 'self'),可能媒体流被允许但渲染被限制。

查看头部信息,未发现任何限制

四、video 还没进入播放状态

在以上答案都排除后,发现有流但是未被播放(因为截图能拿到视频帧),考虑是video标签为正确绑定流。 在控制台中打印video的状态:

const video = document.querySelector('video');
console.log(video.readyState, video.paused);

获得的结果:

0 true

补充说明 readyState 的取值含义:

含义说明
0HAVE_NOTHING还没有任何关于视频的信息
1HAVE_METADATA读取到了元数据(宽高、时长等)
2HAVE_CURRENT_DATA有当前帧数据,但可能不足以播放
3HAVE_FUTURE_DATA有未来帧,但可能不够流畅
4HAVE_ENOUGH_DATA可以正常播放

理想情况下,应该看到 readyState === 4,并且 video.paused === false。 如果 readyState 长期卡在 2 或 3,而 pausedtrue,那就是 video 播放没有真正触发。这种情况下就算截图有帧,页面也会一直黑屏。

也就是说,srcObject 并没有被正确赋值或者 video 元素还没在文档流中,或者被浏览器静默拦截。

我之前的代码为:

useEffect(() => {
  let isMounted = true
  setIsLoading(true)
  const startCamera = async () => {
    try {
      const stream = await navigator.mediaDevices.getUserMedia({
        video: { facingMode: 'user' },
        audio: false
      })
      if (!isMounted) return
      streamRef.current = stream
      if (videoRef.current) {
        videoRef.current.srcObject = stream
        await videoRef.current.play().catch(() => {})
      }
    } catch {
      setError('无法访问摄像头,请检查权限')
    } finally {
      setIsLoading(false)
    }
  }
  startCamera()
  return () => {
    isMounted = false
    if (streamRef.current) {
      streamRef.current.getTracks().forEach(track => track.stop())
      streamRef.current = null
    }
  }
}, [])

首先考虑是否自动播放被拦截了,由于await videoRef.current.play().catch(() => {})catch块里没有任何处理,如果是被拦截了不会报错,我们加上错误处理后,发现并没有走到catch块了,证明并非是播放被拦截。

查看整个代码逻辑,如果没有任何报错,srcObject也赋值了视频流。那就只有一个可能:video标签挂载时间比获取媒体流的时间要晚,尤其是在dom结构上,设置了loading状态结束后才渲染video标签,更能证明这一点。

{isLoading ? (
    <div className='text-white'>加载中...</div>
  ) : error ? (
    <div className='text-white gap-4 flex flex-col items-center'>
      <SvgIcon iconName='Error' />
      {error}
    </div>
  ) : (
    <>
      <video
        ref={videoRef}
        autoPlay
        playsInline
        muted
        className='w-full h-full object-contain rounded-2xl'
      />
    </>
  )}

但是为什么开发环境下可以正常访问呢?就是“时序竞态”问题: 在生产环境里 getUserMedia 返回得比<video>挂载更早,首次调用发生在 videoRef 还为 null 时,绑定被丢失;开发环境因为 StrictMode/HMR 导致 effect 被再次执行或整体更慢,恰好补上了这次遗漏,所以看不出问题。 按照我的理解,通过设置断点:

useEffect(() => {
  let isMounted = true
  setIsLoading(true)
  debugger // 1
  const startCamera = async () => {
    debugger // 2
    try {
      const stream = await navigator.mediaDevices.getUserMedia({
        video: { facingMode: 'user' },
        audio: false
      })
      console.log(isLoading)
      debugger // 3
      setVideoId(videoId + 1)
      if (!isMounted) return
      streamRef.current = stream
      if (videoRef.current) {
        videoRef.current.srcObject = stream
        await videoRef.current.play().catch(() => {})
      }
    } catch {
      setError('无法访问摄像头,请检查权限')
    } finally {
      debugger //4
      setIsLoading(false)
    }
  }
  startCamera()
  return () => {
    debugger // 5
    isMounted = false
    if (streamRef.current) {
      streamRef.current.getTracks().forEach(track => track.stop())
      streamRef.current = null
    }
  }
}, [])

开发环境执行顺序为:

  1. 执行第一次useEffct,执行到debugger2处,遇到await挂起,effect返回。
  2. 执行第二次useEffct,依旧执行到debugger2处,遇到await挂起
  3. 等待第一次await恢复,执行到setLoading(false)处,video挂载。
  4. 等待第二次await恢复,执行到debugger3处,videoRef.current此时已有值,成功挂载视频流。

总结

到此这篇关于前端获取设备视频流踩坑实战的文章就介绍到这了,更多相关前端获取设备视频流内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • JavaScript数组方法-系统性总结详解

    JavaScript数组方法-系统性总结详解

    本文是小编给大家特意整理的关于js数组方法的知识,非常实用,在面试笔试题中经常用得到,有需要的朋友可以参考下
    2021-09-09
  • Typescript协变与逆变简单理解

    Typescript协变与逆变简单理解

    深入学习TypeScript类型系统的话,逆变、协变、双向协变、不变是绕不过去的概念。这些概念看起来挺高大上的,其实并不复杂,这篇文章我们就来学习下协变和逆变吧
    2022-10-10
  • JS什么场景不适合箭头函数

    JS什么场景不适合箭头函数

    这篇文章主要介绍了JS什么场景不适合箭头函数,对箭头函数感兴趣的同学,可以参考下
    2021-04-04
  • JavaScript实现一个简易的计算器实例代码

    JavaScript实现一个简易的计算器实例代码

    这篇文章主要介绍了JavaScript实现一个简易的计算器实例代码,具有很好的参考价值,希望对大家有所帮助,一起跟随小编过来看看吧
    2018-05-05
  • js实现随机点名小功能

    js实现随机点名小功能

    这篇文章主要为大家详细介绍了js实现随机点名小功能,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-08-08
  • javascript利用canvas实现鼠标拖拽功能

    javascript利用canvas实现鼠标拖拽功能

    这篇文章主要为大家详细介绍了javascript利用canvas实现鼠标拖拽功能,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-07-07
  • js仿iphone秒表功能 计算平均数

    js仿iphone秒表功能 计算平均数

    这篇文章主要为大家详细介绍了js仿iphone秒表功能,可以计算平均数,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-01-01
  • 在js中判断checkboxlist(.net控件客户端id)是否有选中

    在js中判断checkboxlist(.net控件客户端id)是否有选中

    添加或修改内容时,需要对关键数据进行判空处理,checkboxlist是否有选择项如何使用js判断实现,接下来为大家详细介绍下实现方法,感兴趣的朋友可以参考下哈
    2013-04-04
  • js console.log打印对象时属性缺失的解决方法

    js console.log打印对象时属性缺失的解决方法

    在编写代码时,我们常常用 console.log() 的方式将信息在控制台中打印出来以帮助我们进行前端调试,那么console.log打印对象时属性缺失怎么办?下面我们就一起来了解一下解决方法
    2019-05-05
  • 『JavaScript』限制Input只能输入数字实现思路及代码

    『JavaScript』限制Input只能输入数字实现思路及代码

    一个文字方块必须限制只能输入数字(或是小数点)并且要支援 IE 和 Firefox,接下来为大家介绍下如何解决这个需求
    2013-04-04

最新评论