视频播放的那些事
作者: 发布于:
  • 视频作为淘宝教育业务的基础服务,本文根据自身在手淘中视频播放的实践,谈谈在手淘中视频播放遇到的问题及其解决方案。

播放器

  • 在手淘过去一年多的历史长河中存在五种类型的播放器。
  • 原生 HTML5 video 标签
  • Android 5.3.2 版本之后的 UC 内核增强 SAC 播放器
  • Android 5.4.9 版本之后的 UC HAC 播放器
  • Android 5.3.2 版本之前的 Glue native 播放器
  • Android 5.3.2 版本及其之后的 PlayBuddy 播放器
  • 下面从支持平台,loading 动画,全屏,模拟全屏和兼容性五个方面对各个播放器做个横向对比。
类型video IOSvideo AndroidUC SACUC HACPlayBuddyGlue
平台IOS手淘android手淘android手淘>=5.3.2android手淘>=5.4.9android手淘>=5.3.2android手淘<5.3.2
loading可定制可定制可定制不可定制,丑陋
控件可定制性可以可以不可以可以不可以不可以
全屏支持竖全屏不支持支持竖全屏可以支持支持
模拟全屏支持支持不支持有bug----
兼容性较好较好较好一般,不兼容 android 5.0及以上

接口与事件封装

上面介绍了手淘中可供 WebView 选择的播放器,对于业务方而言迫切需要一个解决方,无需关心底层差异。为此,我们屏蔽移动端不同系统平台、宿主环境、播放器的实现细节和兼容性问题,提供统一的接口和事件,具体如下:

  • 方法
  • play 播放
  • pause 暂停
  • stop 停止
  • show 显示
  • hide 隐藏
  • requestFullscreen 全屏
  • exitFullScreen 退出全屏
  • getCurrentTime 获取当前播放时间
  • setCurrentTime 设置播放时间
  • getDuration 获取视频时长
  • setPoster 设置背景图
  • destory 销毁
  • reset 重置视频
  • 事件
  • timeupdate 进度更新
  • ended 停止
  • error 错误
  • play (专指video)
  • pause 停止(专指video)
  • firstpaint 视频真正开始播放(专指video)
  • controls 播放控件(专指video)
  • 播放
  • 暂停
  • 进度更新
  • 全屏
  • loading

兼容性处理

接下来谈谈在开发过程中遇到的各种小问题及其解决办法。

video

  • 内联播放。iPhone 在视频播放时默认全屏播放,参考
  • WebView 中,可以对 UIWebView 做如下配置,并且在 video 中配置 webkit-playsinline 属性即可:

webview.allowsInlineMediaPlayback = YES;

* iPhone Safari 在 IOS >= 8 的系统中,有人也提出了一个[方案](https://link.zhihu.com/?target=https%3A//github.com/bfred-it/iphone-inline-video)

  • 自定义播放控件
  • 部分 Android 机型不支持内置控件,或者说内置控件无法正常使用;各个产品都有特定的视觉规范,默认控件的交互和视觉无法满足需求。因此,我们推荐默认不启用默认控件,采用自定义控件。
  • IOS 下播放时还可能还展示系统自带播放按钮,可以配置如下 CSS。

video::-webkit-media-controls-start-playback-button {
    display: none;
}

  • poster 视频底图
  • 在 iPhone 中视频加载完第一帧数据后会覆盖 Poster 底图展示第一帧画面,这时可以使用 DIV 覆盖在视频上方模拟,监听 timeupdate 事件做隐藏操作。
  • 在 UC WebView 中动态设置 poster 可能会导致手淘 crash,方案跟上方一样。
  • 在使用 Native 播放器时,在播放器未初始化时使用 DIV 替换 video 标签,并设置底图为背景图。
  • 播放首屏:IOS 通过监听 playing 事件可以准确获取视频播放的时间点;Android 中在该事件触发时,还没真正开始播放。我们通过监听 timeupdate 的事件做模拟处理。

_timeUpdate(e) {
 var currentTime = this.getCurrentTime();
 
 // 判断是否为首帧
 if (currentTime !== undefined && currentTime !== 0) {
     this.fire('firstpaint');
  }
}

_playing() {
  if (Env.os.ios) {
     this.fire('firstpaint');
  }
}

  • 视频切换:在android 4.4 以下版本,在视频切换时存在第一次切换不能正常播放,第二次才能正常播放情况。通过调试人肉分析,发现切换视频的 video 存在以下两个特征:readyState 值为 0,videoWidth 为 0。因此我们判断当两个属性为0时,则切换失败,再次调用播放逻辑。存在误判的可能,但是能保证正常工作。

isWork() {
    if (videoEl.readyState === 0 && videoEl.videoWidth === 0) {
        return false;
    }
        
    return true;
}

  • 全屏:手淘 IOS 支持竖全屏效果,Android 虽然具有全屏方法,但是被手淘限制,调用全屏方法无效。
  • 方案一:为了支持横全屏,我们使用 css3 的 rotate 对视频区域进行90度旋转,并且调用 bridge 接口隐藏 native 顶部的 navibar,并对自定义控件进行响应优化调整。基本到达 native全屏效果。当然顶部状态栏不能隐藏还是有些小瑕疵。同时旋转之后元素的 z-index失效,导致视频覆盖控件问题,可以通过设置 -webkit-transform: translate3d(0,0,0) 来修复

requestFullscreen() {
    var element = this.el[0];
    var method = FullscreenApi.requestFullscreen;

    if (method) {
        element[method]();
    } else if (element.webkitEnterFullscreen || element.enterFullScreen) {
        element.webkitEnterFullscreen && element.webkitEnterFullscreen();
        element.enterFullScreen && element.enterFullScreen();
    } else {
        // 模拟全屏
        // enterFullWindow();
    }
}

// 模拟全屏js核心代码
_mockFullscreen() {
    if (curEl.hasClass('normal')) {
        this.fullscreen = false;

        playerEl.css({
            width: this.originWidth,
            height: this.originHeight,
            left: 0
        }).removeClass('fullscreen');

        wrapperEl.css({
            width: this.wrapperOriginWidth,
            height: this.wrapperOriginHeight
        });

        videoEl.css('height', '100%');

        curEl.removeClass('normal');
        contentEl.removeClass('fullscreen');
    } else {
        this.fullscreen = true;

        this.originWidth = playerEl.width();
        this.originHeight = playerEl.height();

        this.wrapperOriginWidth = wrapperEl.width();
        this.wrapperOriginHeight = wrapperEl.height();

        playerEl.css({
            width: $(window).height(),
            height: $(window).width(),
            left: $(window).width()
        }).addClass('fullscreen');

        wrapperEl.css({
            width: $(window).height(),
            height: $(window).width()
        });

        videoEl.css('height', videoEl.height() - controlsHeight);

        curEl.addClass('normal');
        contentEl.addClass('fullscreen');
    }
}

* 效果图:


* 预览地址(请用手淘扫码):

  • 方案二。方案一只是模拟了横全屏效果,对于追求完美的处女座不能忍。还有其他方案吗?有时候只需要转换下思维,问题即可迎刃而解。既然是横屏播放,只需要让 WebView 横屏即可,同时在横屏之后重新调整控件即可,关键手淘提供了打开应用横全屏的接口。注意点:横屏之后需要禁止页面滚动,要不然全屏就露馅了,因为本质还是个 WebView。

if (this.transverseFullScreen) {
  if (curEl.hasClass('normal')) {
    curEl.removeClass('normal');
    this._transverseFullScreen(false).then(() => {
      $('body').removeClass('co-fullscreen').attr({ height: 'auto' });
      this.videoWrapperEl.height(this.videoOriginHeight).removeClass('fullscreen');

      this.player.fire('transversefullscreen', { fullscreen: false });
      this.resize();
  } else {
    curEl.addClass('normal');
    this._transverseFullScreen(true).then(() => {
      $('body').addClass('co-fullscreen').attr({ height: win.height() });
      this.videoWrapperEl.height(win.height()).addClass('fullscreen');

      this.player.fire('transversefullscreen', { fullscreen: true });
      this.resize();
    });
  }
  return;
}

  • Demo:
  • 手淘 IOS 扫码:
  • 方案3。在 UC HAC 方案视频提供全屏接口 UCSettings.setVideoViewFullscreenByDefault(true),开启后,视频全屏默认为横屏
  • 自动播放
  • 出于用户节省用户流量考虑,iPhone 下播放视频需要用户手动触发,即使配置了 autoplay 属性也是无效的。在业务中,特定场景还是需要视频能够自动播放,对此我们可以监听页面的 touchstart 事件,做如下处理:

if (this.auoplay && env.app.TB && env.network.wifi) {
if (player.getCurrentTime() > 0 && !player.isPause()) {
  return;
}
if (this.hasAutoPlay) {
  return;
}
this.hasAutoPlay = true;
startEl.trigger('click');

function autoplay() {
    doc.detach('touchstart', autoplay);
    if (player.getCurrentTime() > 0) {
        return;
    }
  startEl.trigger('click');
 }
 doc.on('touchstart', autoplay);
}

  • 其他
  • 部分机型手淘低版本使用 video 播放时,会出现有声音没画面的问题,升级手淘后即恢复。例如,小米4 手淘 4.2.0
  • IOS 5.1 和部分 android 手机暂停和开始按钮不触发点击事件(元素的 :after 为iconfont)。通过父元素添加background即可
  • Android UC 内核的播放器,在未设置 source 资源时,设置 poster 无效
  • Android UC 内核的播放器无法自定义控件和样式操作,但是可以正常的监听事件。
  • 直接替换 source 不会改变当前正在播放的视频,需要调用 load 方法。
  • UC 浏览器中 video 标签会被 UC 的播放器插件替换
  • 使用 m3u8 和 mp4 基本可以兼容所有机型
  • 在 IOS 视频初始化后设置 currrentTime 无效,在 loadedmetadata 事件触发后,设置 currentTime 即可。

native播放器

  • destroy:
  • Glue:Glue native 播放器在页面跳转,WebView 后退等操作时,不会自动析构,好的情况是视频依然在背后播放,有时候会直接导致手淘 crash。
  • PlayBuddy:在页面跳转时依然会继续播放

处理方式:页面跳转时需要手动的销毁native播放器。

  document.addEventListener('WV.Event.Page.Refresh', $.proxy(this.destory, this), false);
  document.addEventListener('WV.Event.Key.Back', $.proxy(this.destory, this), false);
  win.on('unload', $.proxy(this.destory, this));
  win.on('beforeunload', $.proxy(this.destory, this));

  • 定位
  • Glue 播放器使用 dip 作为播放器的定位单位,rem 布局会对页面进行缩放,导致定位位置和视频大小错误。同时定位时参数有小数点会导致播放器错误。

/__
 * 返回值需要是整数,否则会有异常
 */
_getVedioPos(isDpr) {
    var el = this.el,
        offset = el.offset(),
        dpr = 1;

    if (isDpr) {
        dpr = this._getDpr();
    }

    return {
        x: parseInt(offset.left / dpr),
        y: parseInt(offset.top / dpr),
        w: parseInt(el.width() / dpr),
        h: parseInt(el.height() / dpr)
    };
}

  • 视频源地址:Glue 播放器不支持以 // 开始的视频资源,例如 //video.xxx
  • PlayBuddy 播放器不会随着页面滚动而滚动。

总结

  • 如果业务中需要在手淘中播放视频,IOS 直接使用原生 video 即可。在 Android 中较为复杂,没有完全兼容的方案。建议使用 video,对于 Android 低版本建议使用native 播放器。随着uc内核接入,未来完全抛弃 native 方案也是可行的。
  • 本文基于过去一年在手淘视频播放过程中遇到问题的小结,后续会整理视频监控和视频娱乐化相关内容。

附:手淘同学播放器兼容性表

品牌机型手淘版本操作系统版本播放器控件视频列表切换试看控制观看进度同步切换模式问题
苹果6 plus5.2.78.11*
苹果65.2.78.11*
苹果5s5.2.78.11*
苹果55.2.78.11*
苹果4s5.2.78.11*
苹果4***
googlenexus 5*yun os 3×*
googlenexus 5*安卓 5×*
三星N71004.9*×*
三星NOTE44.9*模式二点最大化crash
三星NOTE34.9*×*
三星S45.2.7.3*模式二,播放有问题
三星I93004.3**
三星S35.2.8.2**
三星S55.2.7.3**
魅族MX2****
魅族MX4 PRO***
魅族魅蓝Note***
魅族MX3(安装不上)***
华为荣耀6**模式二可能播放不了
华为mate7**模式二可能播放不了
华为c8816**进度条拖动会跳
华为荣耀3c*4.4.2×一直展示loading的图片模式二播放不了
华为C8813*4.1.1高清视频不能播放
HTCMAX****
HTC816w***
VIVOFind5*4.1.1*
VIVOX3*4.2.2*
小米2S*4.3*高清的播放不了
小米3*****
小米4*4.4.4**
索尼M512*4.4.2*
索尼xperia 36l*4.1.2**
nubianx403*4.2.2×*模式一,模式二播放均有问题
锤子****
oppox907*4.0.3很难点到*
nexus5***第二种模式crash

注:模式1为video,模式2为glue native播放器