大前端播放器 VideoX 如何回应业务诉求

前端开发

undefined

VideoX 是内容前端团队基于电商业务(以下简称大淘宝)背景打造的面向大终端场景的前端播放器。这篇文章谈谈我对播放器领域问题的认识,以及当下解决这些问题的思路。

大淘宝视频播放的场景有哪些?

undefined

大淘宝视频播放的第一业务场景,是在消费侧。回想起来我最早在淘宝上看到视频内容,应该是在商品的详情页。几十秒的视频内容更形象地传递了商品的信息。商品有了视频内容,自然地在一些前置入口上也就可以透出它们了。比方说首页上的「猜你喜欢」,搜索的结果页等这些淘宝的基础交易链路;16 年淘宝开始提出内容化与社区的方向,引入了创作者的角色以及图文类型的内容,并推出了爱逛街、有好货、淘宝头条等内容电商产品,短视频在其中扮演着至关重要的角色。淘宝直播也在这期间横空出世,实时类视频登上淘宝历史的舞台。21 年淘宝持续推进内容化,提出“生活在淘宝”的愿景,逐渐形成了以逛逛、点淘和淘宝直播的社交内容产品矩阵;商家上传的商品视频和创作者发布的图文视频,又可以在前台的导购场景中透出,不断丰富淘内的商品导购形式和信息消费形态。

应该说,在内容和交易日渐融合的趋势下,在淘宝从交易走向消费的进程中,视频已经无处不在了。

视频内容的流转涉及从生产到消费的完整链路,在生产侧和平台端的业务场景同样存在视频播放的诉求。在生产侧,创作时在亲拍中需要播放模板视频,发布时在逛逛需要预览视频上传后的效果;在平台端,在纵横的体验中心演示中台能力时需要对多种分辨率和编码格式的视频进行播放,在黄雀每天有上千名审核人员完成百万级的审核任务时需要播放视频。

undefined

业务对于视频播放的诉求有哪些?

由上可见,大淘宝视频播放的业务场景是非常复杂的。尽管场景是复杂的,但业务对于视频播放的诉求是可抽象的。我把它们抽象为以下几点。

多端的播放能力

undefined

业务上对于视频播放的首要诉求,还是视频能不能播的问题。在大淘宝上,主要播放短视频、视频动画、直播和回放、全景视频等。依据实时性和交互方式的不同,通常将这几类视频分为点播(Vod)、直播(Live)和全景视频(Panorama)。

为使得这些类型的视频能够在网络上更好地进行传输和播放,则需要用到不同的视频格式:

undefined

不同的视频类型使用哪种视频格式是由跨平台兼容性、延时、可拓展性、使用成本等因素综合决定的。

由于不同业务场景面向的客户群体的差异,以及业务根据其性质对用户体验和研发效率的权衡,业务上的终端场景也是千差万别:

undefined

比方说行业小二给商家进行直播开课,商家是在千牛 PC 客户端上进行观看;店铺为了做三方的开放技术,很长的一段时间用的是小程序的方案;基础链路要优先保障稳定性,用的是 Native 的方案,外投又需要适配 WebView……

播放器需要对这些的视频类型、视频格式和终端场景提供完备的视频播放支持。

undefined

视频交互能力

除了播放画面和声音,在观看视频时,业务还需要为用户提供视频交互能力。这些实现交互功能的控件是盖在视频之上的:

undefined

交互的目的通常是为了控制视频的播放进度和效果,以及设置视频的可见性。这些交互能力我归类总结了一下,有以下几种:

undefined

交互定制能力

undefined

上面列举了全量的交互能力。但不同的业务场景需要的交互能力是各不相同的。这就需要播放器提供交互的定制能力。对相关的定制能力进行归纳总结,有以下几种:

undefined

还有一种是控件语言的定制,允许业务设置控件内使用的文本语言。例如业务可根据用户所在地区设置播放器使用的语言。目前在淘内没有这类诉求。

多视频管理能力

undefined

在移动端,当页面上有多个视频时,为了避免一次性加载所有视频资源浪费用户流量或多个视频一起播放出现「双音轨」的情况影响用户体验,业务普遍有多视频管理能力的诉求。

管理能力可以概况为以下几种:

  1. 控制每个视频的加载时机:例如在幻灯片(Slider)场景下,只有当前显示的那个幻灯片才需要加载视频资源。切换幻灯片后播放当前幻灯片下的视频,暂停或销毁之前的视频;
  2. 选择最佳位置的视频进行播放:例如在滚动(Scroll)场景下,要选择离屏幕视觉中心点最近的视频进行播放,该视频播放时,暂停或销毁上一个正在播放的视频。

淘内的交互场景还有很多。比方说:

undefined

播控服务能力

通常来说,业务播放视频,只需要传递一个视频资源 URL 就足够了。但部分业务为了权衡视频投入的支出和前台用户的体验,就需要使用到播控服务能力。播放器会请求一个播控的服务,来实现:

undefined

数据化能力

undefined undefined

业务需要了解视频的投放效果和用户的观看体验,就需要中台提供数据化能力。视频播放评估指标可以分为两类:

  1. 一个是视频体验质量(QoE: Quality of Experience) ,该指标用来衡量最终的业务效果,反映在业务中用户使用视频产品的情况(例如对视频是否喜爱)。具体的指标值包括播放次数、播放时长、有效播放率等等;
  2. 一个是视频服务质量(QoS: Quality of Service) ,该指标用来衡量技术提供视频服务的效果,反映线上视频播放技术的运作情况(例如性能和稳定性)。具体的指标值包括视频秒开率、视频卡顿率、视频播放成功率等等。

业务需要中台定义这些指标及其计算口径,并提供其业务域内的实时和离线数据。

undefined

播放管控能力

undefined

2021 年双 11,用户反馈手淘在后台偷跑 17G 流量

视频的引入给业务在终端带去了全新的用户体验,但是如果使用不当,轻则造成突然播放声音影响用户体验,重则在移动端浪费流量造成用户经济上的损失。因此,一方面业务既期望中台能够提供便捷的定制能力,另一方面也期望中台能有「兜底」的能力,在发生问题时能够及时止损。这就是播放器的播放管控能力。

例如:

  1. 权限类的:是否允许自动播放、是否允许后台播放、是否允许流量播放。这些配置还与用户的手淘配置相关,最终影响到用户的流量使用;
  2. 优化类的:是否启用硬解、是否启用缓存、是否启用数据埋点。这些配置设定目的是对用户终端硬件的消耗及播放性能之间进行权衡。

undefined

技术如何满足这些诉求?

undefined

中台为满足业务视频接入的诉求,提供了一套完整的视频接入方案,覆盖业务从视频发布到视频播放的完整链路。从端视角来看,方案中包含了视频上传SDK,视频中台服务,视频播放器三大模块。链路中通过业务标识和视频标识来保障业务域内视频流转的可追溯。

业务发布页使用视频上传SDK:

const uploader = await createUploader({ bizCode: 'foo' });
const fileInfo = await uploader.startUpload(file);

业务主页使用中台服务接口获取 videoId:

const videoIds = await mtop.request({
  api: 'mtop.taobao.cloudvideo.video.query',
  data: { playScenes, from },
});

业务详情页使用视频播放器:

<Videox 
  sourceProvider={{
    playScenes: 'foo',  // 业务标识
    from: 'common',  // 播放场景
    src: '260544347323',  // 视频标识(videoId)
  }}
/>

备注:

  1. 业务主页也可能直接使用视频播放组件;
  2. 业务通常使用短视频全屏页而非建详情页来实现视频播放和互动。

播放器架构

基于业务的诉求和当下的生产关系,播放器的整体架构遵循两个大的原则:一是要薄,让业务有选择权,基于能力组合进行定制;二是要白盒,让业务有发现和定位问题的能力而不是强依赖中台。

因此播放器架构遵循关注点分离的原则,分为以下几层,每一层解决一个领域内的问题(业务诉求点),使用特定的技术栈(技术发力点):

undefined

基于这个分层产出播放器整体架构图:

undefined

播放器内核

播放内核解决视频能不能播的问题,并提供一组可控的 API。根据大淘宝业务对多端视频播放的诉求,结合多端场景下稳定性和性能最优解的权衡,中台播放内核有面向 Web 场景使用前端技术栈的播放内核 videox-core ,以及面向移动端场景使用原生技术栈的播放内核 native-core

面向 Web 场景

浏览器已经提供了 <video /> 标签用于播放视频,为什么还需要自研播放内核呢?主要原因是浏览器对于流媒体格式以及视频编码的支持度有限。针对大淘宝的视频播放诉求来说,FLV 及 HLS 协议、 H.265 编码格式及全景视频类型在主流浏览器普遍不支持。

Web 播放内核 videox-core 的实现原则是 API 与原生 <video /> 标签对齐,遵循 W3C 规范。因此,实现自研内核需要熟悉 W3C 规范的内容,理解浏览器视频播放的工作过程,充分了解流媒体协议(FLV/HLS)和媒体格式(MP4/HEVC)的知识以及媒体处理工具(FFmpeg)的使用,并运用最新的浏览器相关性技术(MSE/WebGL/WebAssembly…)来完成。

下面概述一下内部的实现原理。

通常前端播放视频,使用 <video /> 就完事了:

undefined

但在大淘宝的业务场景中,最常遇到的是浏览器不支持的容器格式的情况。对于非标容器格式通常的处理流程如下(以 TS 为例):

undefined

基本思路都是使用 JS 请求视频资源数据并进行解封装,再将其转换成 MP4 格式传递给 <video /> 播放,这种字节拼装的方案需要借助 MSE API 的能力。相应的处理模块及其输入输出:

另一种是不支持的编码格式的情况。对于非标编码格式处理流程如下(以 MP4 为例):

undefined

与上面的流程相比,后置的处理模块有所不同。这两个模块的作用及其输入输出:

结合标准支持情况和非标的容器及编码格式支持情况,videox-core 内部视频播放的整体处理流程如下:

undefined

上面还只是一种容器格式和一种编码格式的场景。在大淘宝的业务场景中,需要支持多种容器格式和多种编码格式。所以 Demuxer 内部有多个容器解析器(Parser),Docoder是由渲染控制器(Controller)调度的。整体内部程序设计类图如下:

undefined

播放内核作为单独 npm 包对外提供使用:

<div id="container" />
<script module>
import Videox from '@ali/videox-core';

const containerEl = document.getElementById('container');
const videox = new Videox({
  container: containerEl,
  src: '//example.com/video.mp4',
});
videox.play();
</script>

videox-core 程序设计的思考:

  1. 为什么不全部根据「不支持」来实现整体流程?是基于运行性能上的考虑:
    1. 当格式不支持+编码支持时,使用 MSE 硬解性能更优;
    2. 当格式支持+编码支持时 <video /> 原生性能更优。
  2. 为什么 Demuxer 不用 FFmpeg+WASM 来实现?
    1. 包大小、代码可维护性和移动端兼容性方面的考虑。
      1. Demuxer 使用 FFmpeg 打包后的体积特别大(少则几M,多则十几M);
      2. 万一出现 Bug,调试 FFmpeg 的源码效率特别低,也不可能去更新 FFmpeg 的源码;
      3. 同时 WASM 在移动端兼容性是比较差的,播放器应尽最大可能提升整体方案的兼容性。
    2. 架构灵活性的考虑。Demuxer 下不同封装格式使用不同的 Parser 实现,未来可以单独基于组合的方式实现面向单个封装格式的播放器。
  3. 不支持的容器格式处理为什么不基于或集成 flv.js / hls.js 等库来实现?
    1. Decoder 需要接收的是码流,flv/hls 输出的是视频流,因此无法直接复用;
    2. 两者都是功能完备的播放器实现,无法在多种容器格式和编码格式诉求的处理流程中单独被引用。 社区实现 FLV 和 HLS 支持 H.265 普遍做法也是基于这两个库来复写部分逻辑。

面向 Native 场景

undefined

面向 Native 场景,中台提供了两个 SDK 供业务进行接入:

在移动端下,这些 SDK 即可以被使用原生技术栈的业务进行接入,也可以被集成到 MiniApp、PHA/WindVane、Weex 等渲染容器内。

Native SDK 详细介绍可参考客户端的文档说明

播放器组件

播放内核满足了业务多端视频播放的诉求,但业务还有视频交互、交互定制、多视频管理等诉求的需要,同时播放内核在业务前端系统进行集成接入的效率也不高。VideoX 提供了播放器组件来解决这两个问题。

在大淘宝,业务前端面向 Web 场景以 React 框架进行项目开发,面对多端场景以 Rax 框架进行项目开发。因此,VideoX 提供了 react-videoxrax-videox 两个组件以满足业务接入的需要,并将上诉业务诉求以插件的形式供业务进行选配集成。

面向 React 项目

undefined

react-videox 用于在 React 项目中集成。主要包括以下能力:

react-videox 内部的主要模块包括:

Context

PlayerProvider 是播放内核 API 的封装,可以同步或更新播放内核状态 ,供内部控件进行消费,同时对外导出 Ref:

  1. 播放器内部的控件组件使用 PlayerProvider 导出的 Hooks 。以播放/暂停切换控件为例:
import React, { memo, useCallback } from 'react';
import { usePlayerActions, usePlayerState } from 'src/context/player';
import { Icon } from 'src/components/common/Icon';
import { Button } from 'src/components/common/Button';

export const PlayToggle = memo(() => {
  const { paused } = usePlayerState();
  const actions = usePlayerActions();

  const handleClick = useCallback(() => {
    actions.togglePlay();
  }, []);

  return (
    <Button
      onClick={handleClick}
      title={paused ? '播放' : '暂停'}
    >
      <Icon type={paused ? 'play' : 'pause'} />
    </Button>
  );
});
  1. 业务使用播放器组件时可以通过 Ref 的方式获取到播放内核的 API:
import React, { useRef, useCallback } from 'react';
import { Videox } from '@ali/react-videox';

function App() {
  const videoxRef = useRef();
  const handlePlay = useCallback(() => videoxRef.current.play(), []);
  const handlePause = useCallback(() => videoxRef.current.pause(), []);

  return (
    <>
      <Videox 
        ref={videoxRef}
        src="//example.com/video.mp4"
      />
      <div>
        <button onClick={handlePlay}>播放</button>
        <button onClick={handlePause}>暂停</button>
      </div>
    </>
  );
}

图标信息存储为 IconProvider,联合 Icon 组件,提供播放器内图标的展示和定制能力:

  1. IconProvider 的实现:
import React, { ReactNode, createContext, useContext, memo } from 'react';

const IconContext = createContext(null);
const iconConfig = {
  scriptUrl: '//at.alicdn.com/t/font_3356617_kjuxu8f44vm.js',
  prefix: 'videox-'
};

export function useIcon() {
  return useContext(IconContext);
}

export interface IconProviderProps {
  /**
  * Symbol 代码地址
  */
  iconScriptUrl?: string;
  /**
  * Symbol 前缀
  */
  iconPrefix?: string;
  children: ReactNode;
}

export const IconProvider = memo((props: IconProviderProps) => {
  const { children, iconScriptUrl = iconConfig.scriptUrl, iconPrefix = iconConfig.prefix } = props;
  return (
    <IconContext.Provider value={{ iconScriptUrl, iconPrefix }}>
      {children}
    </IconContext.Provider>
  );
});
  1. Icon 组件消费 Provider 数据:
import React, { memo, useMemo } from 'react';
import { createFromIconfontCN } from '@ant-design/icons';
import { useIcon } from 'src/context/icon';

export const Icon = memo((props: { type: string; }) => {
  const { type } = props;
  const { iconScriptUrl, iconPrefix } = useIcon();
  const IconFont = useMemo(() => createFromIconfontCN({ scriptUrl: iconScriptUrl }), [iconScriptUrl]);
  const iconPath = `${iconPrefix}${type}`;
  return (
    <IconFont type={iconPath} />
  );
});
  1. 业务使用播放器组件时可通过传参的方式定制图标
import React from 'react';
import { Videox } from '@ali/react-videox';

export default function App() {
  return (
    <Videox 
      src="//example.com/video.mp4"
      iconScriptUrl="//at.alicdn.com/t/font_3357872_s63l0u876n.js" // Symbol 代码地址
      iconPrefix="icon-" // Symbol 前缀
    />
  );
}
控件层

控件层控件层解决视频交互和交互定制的问题,包括了:

  1. 一系列实现视频交互能力的默认控件,例如进度条、倍数菜单、全屏切换等。这些控件各自是独立的,通过 PlayerProvider 同步整体状态。以静音/有声切换控件为例,控件的内部实现如下:
import React, { memo, useCallback } from 'react';
import classNames from 'classnames';
import { Icon } from 'src/components/common/Icon';
import { Button } from 'src/components/common/Button';
import { usePlayerActions, usePlayerState } from 'src/context/player';
import { ControlProps } from 'src/components/interfaces';

export const VolumeToggle = memo((props: ControlProps) => {
  const { className } = props;
  const { muted, volume } = usePlayerState();
  const actions = usePlayerActions();

  const handleClick = useCallback(() => {
    actions.toggleMuted();
  }, []);

  const isMuted = muted || volume === 0;

  return (
    <Button
      className={classNames(
        className,
        { muted: isMuted, },
      )}
      onClick={handleClick}
      title={isMuted ? '有声' : '静音'}
    >
      <Icon type={isMuted ? 'sound-off' : 'sound-on'} />
    </Button>
  );
});
  1. 控件管理器组件 ControlWrap 负责控件层交互(onClick/onDoubleClick/onMouseEnter…)定制,并加载内部默认的控件且允许禁用和排序、插入子组件等:
import React, { Children, ReactNode, useCallback } from 'react';
import { usePlayerActions, usePlayerState } from 'src/context/player';
import { BigPlay } from 'src/components/BigPlay';
import { ControlBar } from 'src/components/ControlBar';
import { ErrorControl } from 'src/components/ErrorControl';
import { LoadingControl } from 'src/components/LoadingControl';
import { Poster } from 'src/components/Poster';

function getDefaultControls(options) {
  const { controls } = options;
  return controls ?
    [
      <Poster key="Poster" order={0} />,
      <BigPlay key="BigPlay" order={1} />,
      <ControlBar key="ControlBar" order={2} />,
      <LoadingControl key="LoadingControl" order={3} />,
      <ErrorControl key="ErrorControl" order={4} />,
    ] :
    [];
}

function getControls(originalChildren: ReactNode, options) {
  const children = Children.toArray(originalChildren);
  const defaultChildren = getDefaultControls(options);
  return mergeAndSortChildren(children, options, defaultChildren);
}

interface ControlsWrapProps {
  children?: ReactNode;
  /**
   * 单击视频容器切换播放/暂停
   */
  clickToPlay?: boolean;
  /**
   * 双击视频容器切换全屏/非全屏
   */
  doubleClickToRequestFullscreen?: boolean;
  /**
   * 是否显示控件
   */
  controls?: boolean;
}

export function ControlWrap(props: ControlsWrapProps) {
  const { children, clickToPlay, doubleClickToRequestFullscreen, ...options } = props;
  const { isFullscreen } = usePlayerState();
  const actions = usePlayerActions();

  const handleClick = useCallback(() => {
    clickToPlay && actions.togglePlay();
  }, [clickToPlay]);
  const handDoubleClick =useCallback(() => {
    if (doubleClickToRequestFullscreen) {
      isFullscreen ? actions.exitFullscreen() : actions.requestFullscreen();
    }
  }, [isFullscreen, doubleClickToRequestFullscreen]);

  return (
    <div
      onClick={handleClick}
      onDoubleClick={handDoubleClick}
    >
      {getControls(children, options)}
    </div>
  );
}

mergeAndSortChildren 函数负责加载子组件,履约禁用(disable)、排序(order)等控件参数,执行参数合并等操作。实现源码

基于此实现,在业务使用 react-videox 时,可以:

  1. 配置控件层交互
function App() {
  return (
    <Videox 
      src="//example.com/video.mp4"
      clickToPlay={true} // 点击切换视频播放状态
      doubleClickToRequestFullscreen={true} // 双击切换视频全屏状态
    />
  );
}
  1. 配置默认控件
function App() {
  return (
    <Videox src="//example.com/video.mp4">
      {/* 禁用控件层的控件 */}
      <BigPlay disabled />
      <ControlBar>
        {/* 禁用控制栏的控件 */}
        <VolumeControl disabled />
        {/* 将全屏切换按钮放到最左边 */}
        <FullscreenToggle order={0} />
      </ControlBar>
    </Videox>
  );
}
  1. 新增定制控件
import React from 'react';
import { Videox, ControlBar, usePlayer } from '@ali/react-videox';

function CustomControl() {
  const { state, actions } = usePlayer(); // 返回播放器的 API
  // const state = usePlayerState(); // 返回播放器的最新属性
  // const actions = usePlayerActions(); // 返回播放器的方法
  return (
    <div style={style}>
      是否正在播放:{`${!state.paused}`}
      <button onClick={() => actions.togglePlay()}>
        {state.paused ? '播放' : '暂停'}
      </button>
    </div>
  );
}

export default () => {
  return (
    <Videox src="//example.com/video.mp4">
      {/* 添加定制控件到控件层 */}
      <CustomControl />
      <ControlBar>
        {/* 添加定制控件到控制栏 */}
        <div style={{ backgroundColor: 'blue' }}>
          Tag
        </div>
      </ControlBar>
    </Videox>
  );
}

面向 Rax 项目

undefined

rax-videox 用于在 Rax 项目中集成。主要包括以下能力:

在大淘宝下的多端场景下,交互定制能力的诉求并不强烈,因此视频交互能力当前渲染容器下的 <video /> 标签提供。

react-videox 内部的主要模块包括:

API 适配层

API 适配的实现思路大致如下:

  1. 由不同的组件实现各自渲染容器下的参数和 API 适配:
.
├── index.tsx                 // 入口文件
├── empty                     // 空组件实现
├── miniapp-native            // 小程序语法的组件实现
├── miniapp-runtime           // Rax 小程序运行时语法的组件实现
├── webview                   // WebView 下的组件实现
├── weex                      // Weex 下的组件实现
└── windvane                  // PHA/WindVane 下的组件实现

以 Weex 下组件适配为例,实现的示例代码大意如下:

import { useRef, forwardRef, useImperativeHandle, memo, useMemo, useCallback } from 'rax';
import setNativeProps from 'rax-set-native-props';
import create from 'lodash.create';
import { ScreenModeMap, contentModeMap } from 'src/common/constant';

export const Videox = memo(forwardRef((props, ref) => {
  /**
   * 参数适配
   */
  const { 
    live,
    src,
    controls,
    style = {},
    objectFit = 'contain',
    orientation = 'vertical',
    onPlay,
    onPlaying,
    onPrepared,
    onLoadedMetadata,
    // 更多参数...
    ...otherProps
  } = props;
  const videoRef = useRef(null);
  const VideoPlusComponent = useMemo(() => createVideoPlusComponent(live), [live, src]);

  /**
   * API 适配
   */
  useImperativeHandle(ref, () => {
    return create(videoRef.current, {
      requestFullscreen: (direction?: number) => {
        let landscape = false; 
        if (direction === 90 || direction === -90) {
          landscape = true;
        }
        setNativeProps(videoRef.current, { screenMode: ScreenModeMap.fullScreen, landscape, });
      },
      exitFullscreen: () => setNativeProps(videoRef.current, { screenMode: ScreenModeMap.inlineScreen, landscape: false, }),
      // 更多 API...
    });
  }, [VideoPlusComponent]);

  /**
   * 事件适配
   */
  const handlePrepared = useCallback(() => {
    onPrepared?.();
    onLoadedMetadata?.();
  }, [onPrepared, onLoadedMetadata]);
  const handlePlaying = useCallback(() => {
    onPlay?.();
    onPlaying?.();
  }, []);
  // 更多事件...

  return ( 
    <VideoPlusComponent 
      src={src}
      hideControl={!controls}
      controlsViewHidden={!controls}
      contentMode={contentModeMap[objectFit]}
      size={objectFit}
      landscape={!(orientation === 'vertical')}
      type={ live ? 'live' : 'video' }
      {...otherProps}
      onPlaying={handlePlaying}
      onPrepared={handlePrepared}
      ref={videoRef}
      className="videox-video"
      style={style}
    />
  );
}));

function callbackToPromise(fn) {
  return new Promise((resolve) => fn((e) => resolve(e.result)));
}

function createVideoPlusComponent(live: boolean) {
  return forwardRef(function(props, ref) {
    return live ? <video {...props} ref={ref} /> : <videoplus {...props} ref={ref} />
  });
}
  1. 在入口文件处根据环境在运行时判断使用的组件:
import { isWeb, isMiniApp, isWeex, isWindVane } from 'universal-env';
import { supportNativeView } from '@ali/rax-composite-view-factory';

let exports;
if (isWindVane && ( supportNativeView('wvvideo') || supportNativeView('wvlivevideo') )) {
  exports = require('./windvane');
} else if (isMiniApp) {
  exports = require('./miniapp-runtime');
} else if (isWeb) {
  exports = require('./webview');
} else if (isWeex) {
  exports = require('./weex');
} else {
  exports = require('./empty');
}

const Videox = exports?.default || exports;
export default Videox;
  1. 声明组件的包导出配置
{
  "name": "@ali/rax-videox",
  "version": "0.3.0",
  "main": "lib/index.js",
  "module": "es/index.js",
  "miniappConfig": {
    "main": "lib/miniapp-native/index"
  },
  "exports": {
    ".": {
      "weex": "./es/weex/index.js",
      "web": "./es/index.js",
      "miniapp": "./es/miniapp-runtime/index.js",
      "default": "./es/index.js"
    },
    "./*": "./*"
  },
  "files": [ "es", "lib", "dist"  ]
}

exports 字段使得 Rax 项目引用在打包特定容器下 bundle 时只加载组件的特定容器下实现代码。

同层渲染对接层

同层渲染是允许将 Native 组件和 WebView DOM 元素混合在一起进行渲染的技术,能够保证 Native 组件和 DOM 元素体感一致,渲染层级、滚动感受、触摸事件等方面几乎没有区别。

在 PHA/WindVane 下已完成同层渲染的接入,同时 native-core 通过与容器对接,在 PHA/WindVane 下注册了 wvvideowvlivevideo 两个同层渲染组件供前端进行使用,前端可通过 <object> 引入:

<object id="my_map" type="application/view" width="200" height="100">
    <param name="viewType" value="wvvideo"/>
    <param name="bridgeId" value="my_wvvideo_0"/>
    <param name="data" value="origin value"/>
</object>

其中:

除了传递参数,同层渲染组件还可以监听事件、调用方法,但方式与普遍的 DOM 元素有所不同,在不同的端上(iOS/Android)还有所差异。当页面上有多个同层渲染组件时,还需要保证 bridgeId 的唯一性。

如此之下,同层渲染组件的使用成本还是比较高的,且普通的 Rax 组件相比有明显的不同,不符合 Rax 体系内开发者的使用习惯。因此 Rax 团队提供了 @ali/rax-composite-view-factory 来更好地管理和桥接同层渲染组件。使用方式如文档所诉,这里就不再展开:

import { useRef, useCallback, useEffect } from 'rax';
import { createCompositeComponent } from '@ali/rax-composite-view-factory';

const videoConfig = {
  properties: {
    src: {
      type: String,
      default: '',
    },
  },
  events: [
    'playing',
  ],
  methods: [
    'play',
    'pause',
  ],
};
const Video = createCompositeComponent('wvvideo', videoConfig, 'video');

function App() {
  const videoRef = useRef(null);
  const handlePlaying = useCallback(() => {
    // dosomthing...
  }, []);
  useEffect(() => {
    videoRef.current.play();
  }, []);

  return (
    <Video
      src="//example.com/video.mp4"
      onPlaying={handlePlaying}
      ref={videoRef}
    />
  );
};

插件化能力

类似多视频管理、播控服务、数据埋点等能力,一方面是选配的,两一方面在 rax-videoxreact-videox 上有诉求,因此在播放器组件中,是通过 HOC 的形式来实现的。这样业务就可以各取所需,同时中台的维护成本也得以控制。

以播控服务为例,使用方式是:

import Videox, { withSourceProvider } from '@ali/rax-videox';
const VideoxWithSourceProvider = withSourceProvider(Videox);

function App() {
  return (
    <VideoxWithSourceProvider 
      sourceProvider={{
        vendor: 'taobao', // 播控服务商标识
        playScenes: 'guangguang', // 业务标识
        from: 'list', // 播放场景标识
        src: '319050647677', // 视频标识(videoId)
      }}
    />
  );
}

在播放器组件中的 HOC 是基于公共的 npm 生成属于 Rax 或 React 的 HOC:

import Rax from 'rax';
import { createHOC } from '@ali/videox-source-provider';

export const withSourceProvider = createHOC(Rax);

亦或是单独作为工具库进行调用:

import { query } from '@ali/videox-source-provider';

const data = await query( {
  vendor: 'taobao', // 播控服务商标识
  playScenes: 'guangguang', // 业务标识
  from: 'list', // 播放场景标识
  src: '319050647677', // 视频 ID
});
const { sources } = data;
const source = sources[0];

// 播放策略
playerSettings.autoplay; // 自动播放
playerSettings.muted; // 静音

// 资源信息
source.src; // 播放地址
source.bitrate; // 比特率
source.quality; // 清晰度

其他 HOC 的实现也基本类似。

播放器配套设施

undefined

播放器配套设施有:

  1. 保障视频播放体验的:测试方案、日志方案、数据埋点方案等,在播放器整体架构中称之为 videox-utils
  2. 保障开发者体验提升业务接入效率的的:教程文档及代码示例,通过官网 videox-site 承载。

Utils

VideoX 在这一方面仍处于早期的建设阶段。下面主要是调研的内容和阶段性工作:

测试方案

日志方案:能够记录播放器在运行时的日志,并在播放器发生错误时主动进行上报,并提供日志下载的途径供用户反馈问题时提交。

数据埋点方案:基于播放器事件的进行数据采集,并产出 QoS 和 QoE 数据。

import VideoxTracker from '@ali/videox-tracker';

const video = document.getElementById('my-video');
const videoxTracker = new VideoxTracker();
videoxTracker.start(video);

Site

官网包括以下部分的内容:

关于未来的一点思考

VideoX 不是一个全新的技术,实际上它已经发展了很多年。但最近一年我们才真正把它当做一个技术产品在运作和对外提供服务。未来 VideoX 需要持续修好内功,为大淘宝业务提升全链路的画质等视频播放体验。落在的具体技术指标上,就是提升播放内核的稳定性、性能和架构开放性,重点是直播的稳定性、 H.265 Seek 的性能和 ARTC 协议的支持。相应的需要对播放器配套设施需要升级,包括测试方案及其覆盖度的完善、日志能力的完备、体验评价体系及全链路画质及质量监控的建设。