前端视频流播放

本文详细记录了在前端实现视频流播放、解决h.265编码支持问题、处理旧版EasyPlayer的定制以及实现截图和视频录制功能的过程。作者遇到并解决了Chrome对h.265编码不支持的问题,通过引入WasmPlayer实现播放,并对旧版EasyPlayer进行源码修改以适应需求。此外,还实现了针对不同编码格式的截图和录制方法。
摘要由CSDN通过智能技术生成

前端视频流播放

1. 开始

最近写了一个小功能,是在前端播放视频流(格式flv/mp4,编码h.264/h.265),一开始找到了LivePlayer。但很遗憾后续被告知需要支持h.265(chrome目前支持h.264编码但不支持h.265);于是又搜寻了一番找到了Easyplayer,照着上面Vue集成的部分搞了一下,WTF居然不行?出现了跟这个issue一样的问题,于是就换了个思路,直接把它用原生方式引入,成功了,具体操作如下:

1-1. 在npm包里找到相应文件放到public下(我多套了个文件夹,不套也可以)

image-20220516141126416

1-2. 在index.html中引入资源文件

image-20220516141151062

1-3. (可选)修改webpack配置

若不配置,vue-loader会报warning,提示此组件不是Vue的组件也不是标准组件库里的组件。

configureWebpack->module->rules下:

image-20220516141553400

1-4. 使用

按照文档上HTML集成示例使用即可。

2. 录播h.265流无法播放

这个时候已经能用了,但而后又发现了一个问题,直播推h.265的流没有任何问题,但是录播的流不知道为什么无法解析。后来领导从别人那顺了一份旧版本的EasyPlayer说是可以,我就又看了一下,写完发现果然可以。

旧版本集成方式和新版本一样,就是使用起来不太一样,使用方式可以看文档旧版EasyPlayer。由于项目上了ts,于是又自己写了一个声明文件,很简陋,凑活着用:

declare class WasmPlayer {
 constructor(
  url: string | undefined | null,
  id: string,
  cb?: (e: any) => void,
  opts?: {
   decodeType?: 'auto' | 'soft';
   openAudio?: boolean;
   BigPlay?: boolean;
   Height?: boolean;
   HideKbs?: boolean;
   cbUserPtr?: any;
   cfKbs?: (e: any) => void;
  }
 ) {
  return
 }
 play(url: string, autoplay = 0, currentTime = 0) {
  return
 }
 pause() {
  return
 }
 destroy() {
  return
 }
 openAudio() {
  return
 }
 closeAudio() {
  return
 }
 startLoading() {
  return
 }
 endLoading() {
  return
 }
 fullScreen() {
  return
 }
 setSnap(url: string) {
  return
 }
 endSnap() {
  return
 }
}

另外旧版本的EasyPlayer下面自带一条无法清除的toolbar,toolbar上还有一个无法清除的logo,点击会跳转到官网。这个就只能在源码里修改了,由于源代码已经做过混淆,让我找了半天。清除logo全局搜索this.logo.style=,并将其中的display改为none;toolbar全局搜索timeBox.style,这两者前者还有验证函数,会验证logo的样式,因此再搜索LogoTimer修改其中的验证条件;后者会定时修改样式为初始样式,因此需要再搜索setTimeout,找到其中有timeBox.style的一条代码,修改其样式。至此大功告成。

3. 增加截图和视频录制

本来我觉得这个问题大概不是什么问题,因为本身新版EasyPlayer是自带的,但找了找发现旧版并没有这个方法。没办法只能自己写了。

一开始我以为EasyPlayer它是默认将视频流渲染成canvas的。因此只做了对canvas的截图和录制。

截图方法:

async function snapshot() {
  const playerEle = document.getElementById(id.value) as HTMLElement
  const children = playerEle.children
  const canvas = Object.values(children).find(
    child => child.tagName.toLocaleLowerCase() === 'canvas'
  ) as HTMLCanvasElement
  let url = ''
  if (canvas) {
    // 由于视频流有空白帧,渲染到canvas上后导致canvas截图空白,故如果截图时机不好会截出来空白的图,因此判断图片大小以避免空白图(空白图大小<100kb)
    const interval = setInterval(() => {
      url = canvas.toDataURL('image/jpg')
      if (url.length > 204800) {
        clearInterval(interval)
        const aEle = document.createElement('a')
        aEle.setAttribute('href', url.replace('image/jpg', 'image/octet-stream'))
        aEle.setAttribute(
          'download',
          `${props.videoOpts.channel_no}-${props.videoOpts.guid}.jpg`
        )
        aEle.click()
        aEle.remove()
      }
    }, 50)
  }
}

甚至本来截图我是没想到它会能截出来空白的,后来发现之后给它加了个循环,直到截出正常图片为止。

录制方法:

let recorder: MediaRecorder
let blobData: Blob[] = []

function record() {
 const playerEle = document.getElementById(id.value) as HTMLElement
 const children = playerEle.children
 const canvas = Object.values(children).find((child) => child.tagName.toLocaleLowerCase() === 'canvas') as HTMLCanvasElement
 if (canvas) {
  const stream = canvas.captureStream(30)
  recorder = new MediaRecorder(stream, { mimeType: 'video/webm;codecs=vp9'})
  recorder.ondataavailable = (e) => {
   blobData.push(e.data)
  }
  recorder.start(10)
 }
}

function stopRecord() {
	recorder.stop()
	const blob = new Blob(blobData)
	const url = window.URL.createObjectURL(blob)
	const aEle = document.createElement('a')
	aEle.setAttribute('href', url)
	aEle.setAttribute('download', `${props.videoOpts.channel_no}-${props.videoOpts.guid}.webm`)
	aEle.click()
	aEle.remove()
	blobData = []
}

本来到这也就告一段落了,结果某一天,又发现h.264视频无法截图也无法录制,查找了一番发现是EasyPlayer会分析视频编码格式,如果是h.264格式就会利用<video>标签简化操作,只有h.265编码才会转成canvas。于是我又改了下截图方法,至于录制方法,我暂时不知道怎么录制<video>播放的视频,唯一能想到的就是每秒截图30次拼接起来,但考虑到性能开销太大,于是让后端处理了,如果有大佬会的话麻烦告知一下。

改进后的截图:

async function snapshot() {
  const playerEle = document.getElementById(id.value) as HTMLElement
  const children = playerEle.children
  const canvas = Object.values(children).find(
    child => child.tagName.toLocaleLowerCase() === 'canvas'
  ) as HTMLCanvasElement
  const video = Object.values(children).find(
    child => child.tagName.toLocaleLowerCase() === 'video'
  ) as HTMLVideoElement
  let url = ''
  if (canvas) {
    // 由于视频流有空白帧,渲染到canvas上后导致canvas截图空白,故如果截图时机不好会截出来空白的图,因此判断图片大小以避免空白图(空白图大小<100kb)
    const interval = setInterval(() => {
      url = canvas.toDataURL('image/jpg')
      if (url.length > 204800) {
        clearInterval(interval)
        const aEle = document.createElement('a')
        aEle.setAttribute('href', url.replace('image/jpg', 'image/octet-stream'))
        aEle.setAttribute(
          'download',
          `${props.videoOpts.channel_no}-${props.videoOpts.guid}.jpg`
        )
        aEle.click()
        aEle.remove()
      }
    }, 50)
  } else if (video) {
    const target = await html2canvas(video)
    url = target.toDataURL('image/jpg')
    const aEle = document.createElement('a')
    aEle.setAttribute('href', url.replace('image/jpg', 'image/octet-stream'))
    aEle.setAttribute('download', `${props.videoOpts.channel_no}-${props.videoOpts.guid}.jpg`)
    aEle.click()
    aEle.remove()
  } else {
    message.error('不支持的dom类型')
    return
  }
}

其中video的截图使用了html2canvas。

至此整个视频流播放问题都解决了,以后说不定还会再用ffmpeg自己写一个视频流解析的组件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值