记一次电视机播放白屏的优化

avatar
@古茗科技

作者:蔡钧

背景

门店的电视机上需要随着饮品上新播放一些不同的画面,包含图片(含当前门店商品价格),视频,需要支持所有画面都加载完成之后再播放并且支持离线播放。

设备:电视机

系统:安卓5/安卓9

浏览器内核:X5

缓存设计:利用serviceworker做请求拦截器

通信架构

说说这个故事

某天早上

产品:“XXX,有两家店反馈视频播放白屏了,昨天有发版本吗?”

我:“没有,我去看看”

打开后台看到运营同学昨天上传了一个200M的视频

我:“是这个视频放不出来吗?”

产品:“是的,但也只有两家店反馈”

打开设备后台查看两台电视的设备型号,发现两台设备都是机顶盒设备

我:“他们这是普通电视插了个安卓盒子吗?”

产品:“打电话问!”

。。。。。。。。

产品:“那这种设备咋搞,内存这么小”

我:“先研究一下”

解决问题

测试环境上传了一个1G的视频就复现了问题,由于需要做先下载再播放的能力,浏览器直接就爆了。

于是选择了新的播放方式:流视频(m3u8)

画板

m3u8的内容信息特别简单,可以理解成视频设置和视频内容链接两部分组成,而每一段分片都可以理解成是一个mp4,每一段分片都能独立播放。

前端采用了hls.js库进行播放

// 判断videoElement是不是已经有了Hls实例,如果有了就不用再创建了
if (videoElement.src.indexOf('blob') !== -1) {
  videoElement.currentTime = 0;
  if (videoElement.paused) {
    videoElement.play();
  }
  return;
}
const hls = new Hls();
hls.loadSource(key);
hls.attachMedia(videoElement);
// hls 播放异常 上报
hls.on(Hls.Events.ERROR, function (event, data) {
  console.log(event, data);
});

很好,开始启动!

什么?离线不能播放??那我多缓存一点!

const hls = new Hls({
  maxBufferLength: 9999999,
});

很好继续启动!

什么?又播放白屏了??

内存又爆了??

坏了,想缓存住这一整个视频,似乎流视频的方案也并不能解决这个问题,那该怎么办呢。。

目光看向了开头的serviceworker,对了请求拦截器!

分析白屏原因,还是因为整个视频太大了,分片绝对不能一次性下载下来。

画板

而如果我们利用拦截器+缓存就可以变成这样,每次经过内存的只有一个小分片,当播放的时候直接从indexDB中读取出来

画板

在这个基础上,我们需要对所有的视频分片主动请求一次进行前置缓存,当所有的分片都被请求过之后这个视频才会被标记成缓存完成。

// 预加载逻辑
const fetchFinish = async (response) => {
  if (!response.ok) {
    return false;
  }
  const reader = response.body?.getReader();
  const contentLength = Number(response.headers.get('Content-Length'));

  const getFinished = async (reader, contentLength, receivedLength) => {
    const { done, value } = await reader.read();
    if (done) {
      return true;
    }
    receivedLength += value.length;
    if (contentLength && receivedLength === contentLength) {
      return true;
    }
    return getFinished(reader, contentLength, receivedLength);
  };

  const res = await getFinished(reader, contentLength, 0);
  return res;
};

const hlsLoad = () => {
  return new Promise((resolve) => {
    const hls = new Hls({
      maxBufferLength: 0,
    });
    hls.loadSource(url);

    // hls播放器的初始化
    const video = document.createElement('video');
    hls.attachMedia(video);

    let totalFragments = 0;

    hls.on(Hls.Events.LEVEL_LOADED, async (_, data) => {
      totalFragments = data.details.fragments.length;
      // 按顺序每个切片都fetch一次, 并且每个fetch都得等到上一个fetch成功后才能进行
      for (let i = 0; i < totalFragments; i++) {
        const fragment = data.details.fragments[i];
        const url = fragment.url;
        const response = await fetch(url);
        const res = await fetchFinish(response);
        // 判断是否加载成功,失败则返回
        if (!res) {
          resolve(false);
          return;
        }
      }
      video.remove();
      resolve(true);
    });

    hls.on(Hls.Events.ERROR, () => {
      video.remove();
      resolve(false);
    });
  });
};

在servicesworker层需要做一次请求拦截

self.addEventListener('fetch', function (event) {
  event.respondWith(
    caches.match(event.request).then(function (response) {
      if (response) {
        return response;
      }
      return (
        fetch(event.request)
          .then(function (response) {
            const responseToCache = response.clone();
            if (!response) {
              return response;
            }
            caches.open('main').then(function (cache) {
              console.log(event.request.url);
              cache.put(event.request, responseToCache);
            });
            return response;
          })
      );
    })
  );
});

在播放端只需要简单的几行代码就能让这个流程在所有的电视上跑起来

const hls = new Hls();
hls.loadSource(key);
hls.attachMedia(videoElement);

总结

引入HLS协议,将视频拆解为轻量级m3u8索引文件与多个TS/MP4分片(通常2-10秒/段),通过Hls.js库实现分片按需加载。将单次内存消耗从百兆级降至分片级(十兆级),避免内存峰值。

  • 预加载与离线保障:通过Service Worker拦截所有分片请求,结合IndexedDB实现分片级缓存,确保所有分片下载完成后标记为“可离线播放”。

  • 内存动态管控:强制播放器仅保留当前播放分片,播放后立即释放内存,避免分片堆积。

开发独立预加载逻辑,按顺序主动请求所有分片并验证完整性,确保缓存成功率;结合分片级错误监控,能够精准定位故障点(如网络中断、分片损坏)。