Video播放器开发实战:从零实现自定义播放器核心功能

闲人钰岩 组件 阅读 2,965
赞 14 收藏
二维码
手机扫码查看
反馈

项目初期的技术选型

最近在做一个视频课程平台的前端重构,核心需求是支持自定义样式的视频播放器,能嵌入到各种页面结构里,还要兼容移动端。一开始我直接用了原生 <video> 标签加点 CSS,想着“能跑就行”。但很快发现事情没那么简单——产品经理要进度条拖拽、倍速播放、画中画、自定义 loading 动画,甚至还要埋点统计播放时长。原生控件根本不够用,而且 iOS 和 Android 上的行为还不一致。

Video播放器开发实战:从零实现自定义播放器核心功能

折腾了两天后,我决定上现成的播放器库。对比了 video.js、plyr、hls.js(只处理流)之后,最终选了 video.js。原因很简单:文档全、插件多、社区活跃,而且我们不需要做直播,点播场景完全够用。虽然它体积有点大(gzip 后 50KB+),但项目对首屏性能要求不高,先保证功能再说。

最大的坑:移动端自动播放和静音策略

集成 video.js 后,本地测试一切正常。但一上真机就翻车了——iOS Safari 和 Android Chrome 都不自动播放,哪怕加了 autoplay 属性。查了资料才知道,现代浏览器为了省流量和防骚扰,强制要求视频必须用户手势触发才能播放,除非是静音状态。

我们的产品逻辑是:用户点击课程卡片后,新页面直接播放视频。理想情况是“开箱即播”,但现实是白屏几秒后才出画面。我试过在页面加载时调用 play(),结果控制台报错:NotAllowedError: play() failed because the user didn't interact with the document first

解决方案分两步走:

  • 首次加载时设置 muted: true,这样能自动播放(静音)
  • 监听用户第一次点击(比如点击播放按钮或视频区域),再取消静音并重新播放

但这里有个细节:video.js 的 API 和原生 video 不太一样,得用它的实例方法。折腾了半天,最后代码长这样:

const player = videojs('my-video', {
  controls: true,
  autoplay: 'muted', // 关键!允许静音自动播放
  preload: 'auto',
  sources: [{
    src: 'https://example.com/video.mp4',
    type: 'video/mp4'
  }]
});

// 监听用户交互,解除静音
player.on('play', function() {
  if (player.muted()) {
    // 等待用户点击后再取消静音
    const handleUserInteraction = () => {
      player.muted(false);
      document.removeEventListener('touchstart', handleUserInteraction);
      document.removeEventListener('click', handleUserInteraction);
    };
    document.addEventListener('touchstart', handleUserInteraction, { once: true });
    document.addEventListener('click', handleUserInteraction, { once: true });
  }
});

这个方案在大多数机型上 work,但部分低端 Android 机还是偶发失败。后来发现是因为有些浏览器在 play() 成功后立即检查是否静音,如果没及时设为 true 就会中断。所以保险起见,我在初始化时就显式设置 muted: true,而不是依赖 autoplay: 'muted' 的隐式行为。

性能问题:大量视频实例的内存泄漏

项目里有个“课程列表”页,每个卡片都预加载了一个 mini 播放器(用于展示封面+播放按钮)。一开始我直接在每个卡片里 new 一个 video.js 实例,结果滑动几十个卡片后,页面明显卡顿,Chrome DevTools 显示内存占用飙到 800MB+。

排查发现,video.js 实例不会自动销毁,即使 DOM 被移除,事件监听器和定时器还在跑。解决方法是在组件卸载时手动调用 player.dispose()。但在 React 里,得配合 useEffect 的 cleanup 函数:

useEffect(() => {
  const player = videojs(videoRef.current, options);

  return () => {
    if (player) {
      player.dispose(); // 关键!释放资源
    }
  };
}, []);

不过 dispose 也有坑:如果视频正在播放,dispose 会触发异常。所以加了个状态判断:

return () => {
  if (player && !player.isDisposed()) {
    player.dispose();
  }
};

优化后,内存稳定在 200MB 以内,滑动流畅多了。但说实话,这种“每个卡片一个播放器”的设计本身就不合理,后续应该改成点击卡片才初始化播放器,只是时间紧先这么搞了。

自定义 UI 的兼容性问题

产品经理要求播放器 UI 跟设计稿 100% 一致,包括进度条颜色、按钮图标、loading 动画。video.js 支持通过 CSS 覆盖样式,但不同版本 class 名可能变,而且移动端原生控件有时会覆盖自定义样式。

最头疼的是 iOS 的全屏按钮——video.js 默认用的是自己的全屏逻辑,但 iOS Safari 强制用自己的全屏控件,导致我们的自定义按钮在 iOS 上无效。查了官方 issue,发现得加一个配置项:

{
  fullscreen: {
    options: {
      fullscreen: {
        ios: true // 允许 iOS 使用原生全屏
      }
    }
  }
}

但这样又导致全屏时 UI 不一致。最后妥协方案是:iOS 上隐藏自定义全屏按钮,引导用户用系统自带的。虽然不完美,但至少不崩。

最终的解决方案

综合下来,我们的播放器封装成了一个 React 组件,核心逻辑如下:

import videojs from 'video.js';
import 'video.js/dist/video-js.css';

const VideoPlayer = ({ src }) => {
  const videoRef = useRef(null);
  const playerRef = useRef(null);

  useEffect(() => {
    const options = {
      controls: true,
      responsive: true,
      fluid: true,
      autoplay: 'muted',
      muted: true,
      sources: [{ src, type: 'video/mp4' }],
      // 关闭原生控件,避免冲突
      nativeControlsForTouch: false,
    };

    const player = videojs(videoRef.current, options, () => {
      console.log('player ready');
    });

    playerRef.current = player;

    // 用户交互后取消静音
    const handleUserInteraction = () => {
      if (player.muted()) {
        player.muted(false);
      }
    };

    document.addEventListener('touchstart', handleUserInteraction, { once: true });
    document.addEventListener('click', handleUserInteraction, { once: true });

    return () => {
      document.removeEventListener('touchstart', handleUserInteraction);
      document.removeEventListener('click', handleUserInteraction);
      
      if (player && !player.isDisposed()) {
        player.dispose();
      }
    };
  }, [src]);

  return (
    <div data-vjs-player>
      <video ref={videoRef} className="video-js vjs-big-play-centered" />
    </div>
  );
};

加上一些全局 CSS 覆盖,基本满足需求了。虽然还有小问题(比如某些安卓机进度条拖拽不灵敏),但不影响主流程,先上线再说。

回顾与反思

这次 Video 播放器的集成,踩了几个典型坑:自动播放策略、内存泄漏、UI 兼容性。最大的教训是——别低估移动端浏览器的限制,尤其是 iOS。下次再做类似需求,我会优先考虑懒加载:只有进入视口且用户点击后才初始化播放器,这样能省很多事。

另外,video.js 虽然功能强,但体积确实大。如果项目只需要基础功能,或许用原生 video + 自己封装控件更轻量。不过考虑到工期和维护成本,用成熟库还是更稳妥。

目前这套方案在线上跑了几周,崩溃率很低,用户反馈也还行。就是那个安卓进度条拖拽的问题还没彻底解决,怀疑是 touch 事件被父容器拦截了,但暂时没精力深挖——毕竟影响面不大,优先级低。

以上是我个人对 Video 播放器集成的完整踩坑总结,有更优的实现方式欢迎评论区交流。这个技巧的拓展用法还有很多(比如 HLS 流、DRM 加密),后续会继续分享这类博客。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论