加载.m3u8格式的音视频并定位到具体帧

1,143 阅读2分钟

使用场景:

  1. 审核管理平台,需要人工审核音频、视频,需要页面先加载出链接返回的音频、视频。资源对应的后缀都是.m3u8。
  2. 苹果TestFlight送审后,将iOS审核后台的资源拉取到内部系统展示。

处理方法hls

使用hls(# HTTP Live Streaming)技术处理。

Send live and on‐demand audio and video to iPhone, iPad, Mac, Apple Watch, Apple TV, and PC with HTTP Live Streaming (HLS) technology from Apple. Using the same protocol that powers the web, HLS lets you deploy content using ordinary web servers and content delivery networks. HLS is designed for reliability and dynamically adapts to network conditions by optimizing playback for the available speed of wired and wireless connections.

第三方库hls.js

github地址github.com/video-dev/h…

使用文档地址github.com/video-dev/h…

实际应用

音视频加载

<script src="https://cdn.jsdelivr.net/npm/hls.js@1"></script>
<script>
    useEffect(()=>{
        if (Hls.isSupported()) {
            var video = document.querySelector("#video_m3u8");
            var hls = new Hls();
            hls.on(Hls.Events.MEDIA_ATTACHED, function () {
              console.log('video and hls.js are now bound together !');
            });
            hls.on(Hls.Events.MANIFEST_PARSED, function (event, data) {
              console.log(
                'manifest loaded, found ' + data.levels.length + ' quality level',
              );
            });
            hls.loadSource(url);
            // bind them together
            hls.attachMedia(video);
          }
 }, [url]); 
</script>

<div>
    <video src={url} className="video" controls id={`video_m3u8`}></video>
</div>

定位到具体帧数

使用video元素的属性 currentTime。该属性属于video的原生属性。

// 通过输入视频具体的秒进行定位
                <Input
                    className="inputValue"
                    placeholder={'0.000 <=时间<= 30.000'}
                    value={time}
                    type="number"
                    change={value => {
                            document.querySelector("#video_m3u8").currentTime = value;
                        }
                    }}
                />

通过修改毫秒数可以定位到具体的帧。

毫秒和帧数的换算

首先根据视频的属性确定视频的fps。即1s内当前视频的帧数--每秒传输帧数(Frames Per Second)

1s = 1000ms。如果当前视频属性为nfps。 则一帧对应的ms数为 1000/n。 则根据对应的换算关系,可以定位到具体的帧数。

计算视频的fps

  1. 第一种方法使用 requestVideoFrameCallback()方法。(已验证)

原理: 使用requestVideoFrameCallback方法必须视频播放的时候才行。但是视频自动播放又不友好。所以默认视频设置autoPlay: true. 默认播放一帧获取到视频的帧率。然后暂停,然后将视频定位到服务器端记录的时间或者回到视频的起点。

        var fps;
        var last_frame = 0; // time of last frame 
        var framelength = 1; // length of one frame 
        function check(_, m) {
          var diff = Math.abs(m.mediaTime - last_frame); // difference between this frame and the last
          if (diff && diff < framelength) {
            framelength = diff;
            fps = Math.round( 1 / framelength);
          }
          if (fps) {
            video.pause();
            // 设置为服务器记录的时间或者回到起点
            video.currentTime = serverTime || 0;
            setFps(fps);
            return fps;
          }
          last_frame = m.mediaTime;
          video.requestVideoFrameCallback(check);
        }
        video.requestVideoFrameCallback(check);   
  1. 第二种方法使用 mediainfo.js(待验证)

codepen.io/buzzone/pen…

// 引入CDN文件
<script type="text/javascript" src="https://unpkg.com/mediainfo.js" ></script>

const fileinput = document.getElementById('fileinput')
const output = document.getElementById('output')

const onChangeFile = (mediainfo) => {
  const file = fileinput.files[0]
  if (file) {
    output.value = 'Working…'

    const getSize = () => file.size

    const readChunk = (chunkSize, offset) =>
      new Promise((resolve, reject) => {
        const reader = new FileReader()
        reader.onload = (event) => {
          if (event.target.error) {
            reject(event.target.error)
          }
          resolve(new Uint8Array(event.target.result))
        }
        reader.readAsArrayBuffer(file.slice(offset, offset + chunkSize))
      })

    mediainfo
      .analyzeData(getSize, readChunk)
      .then((result) => {
        output.value = result
      })
      .catch((error) => {
        output.value = `An error occured:\n${error.stack}`
      })
  }
}

MediaInfo.default({ format: 'text' }, (mediainfo) => {
  fileinput.removeAttribute('disabled')
  fileinput.addEventListener('change', () => onChangeFile(mediainfo))
}, (err) => {
  console.error(err)
})