最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 自定义移动端视频组件

    正文概述 掘金(陈哲)   2020-12-31   485

    前言

    因在 APP Webview 内使用 video 标签会被浏览器劫持,导致在各机型上表现不一致。为了使 APP Webview 播放表现更一致,决定自定义 video 组件。

    先来看看最终效果:

    自定义移动端视频组件

    由于 iOS 在全屏状态下会使用原生的视频控制组件,所以在全屏状态下,Android、iOS 在全屏状态下均使用原生视频播放。

    使用 React 实现。

    一、步骤

    1. 数据结构
    2. DOM 元素及 CSS 样式编写
    3. 播放、暂停功能
    4. 时间显示
    5. 进度条
    6. 进入全屏、退出全屏控制,包含 Android、ios 兼容性

    二、实现

    数据结构

    组件状态需要视频播放状态、当前时间、总时长、是否是全屏状态。

    const initialState:VideoczState = {
      videoState: 'null',
      currentTime: 0,
      duration: 0,
      fullScreen: false,
    };
    
    const reducer = (state:VideoczState, action:VideoActionTypes) => {
      switch (action.type) {
        case VideoTypes.PLAY:
          return { ...state, videoState: 'play' };
        case VideoTypes.PAUSE:
          return { ...state, videoState: 'pause' };
        case VideoTypes.MODIFY:
          return { ...state, ...action.payload };
        case VideoTypes.ENTRYFULLSCREEN:
          return { ...state, fullScreen: true };
        case VideoTypes.QUITFULLSCREEN:
          return { ...state, fullScreen: false };
        default:
          throw new Error();
      }
    };
    

    DOM 元素及 CSS 样式编写

    自定义移动端视频组件

    在 DOM 结构上分为四个部分:视频区、底部控制区、遮罩区(中间播放/暂停图标)、底部简易进度条。其中遮罩区为覆盖整个 video 区域,而底部简易进度条是可以并进底部控制区组件中的。

    由于在移动端不需要音量调节,所以在组件中隐藏了音量调节。音量的实现和进度条是一样的,参考即可。

    自定义移动端视频组件

    播放、暂停功能

    播放、暂停功能就很简单了,直接调用 video 的 API 即可,代码参考:

    const handleVideoPause = () => {
        videoRef.current && videoRef.current.pause();
      };
    
    const handleVideoPlay = () => {
      videoRef.current && videoRef.current.play();
    };
    

    时间显示

    由于 video 的 currentTime、duration 为毫秒数,需要对这两个毫秒数做格式化处理:

    // 时间转换
    export const formatDuraton = (time:number) => {
      let str = '00:00';
      if(time > 0) {
        const hour = Math.floor(time / 3600);
        const min = Math.floor(time / 60) % 60;
        const sec = Math.floor(time % 60);
        const minStr = min >= 10 ? `${min}` : `0${min}`;
        const secStr = sec >= 10 ? `${sec}` : `0${sec}`;
        if(hour > 10) {
          str = `${hour}:${minStr}:${secStr}`;
        }
        if(hour > 1) {
          str = `0${hour}:${minStr}:${secStr}`;
        } else {
          str = `${minStr}:${secStr}`;
        }
      }
      return str;
    };
    

    其中 duration 为视频的总时长,在视频未加载的情况下,无法获取到 duration,需要监听 video ondurationchange 钩子获取。注意:不能使用 onloadedmatedata 来获取 duration,此钩子在部分 Android 机中无法获得响应。

    而 currentTime 为当前视频播放的时间节点,需要监听 ontimeupdate 钩子函数。

    进度条显示

    进度条的显示、拖拽、点击是自定义视频组件的重点。进度条的难点在于拖动的时候,需要知道拖动的终点占视频的百分之多少,从而调整视频当前时间,实现方式有两种,一种是使用 div 元素,一种是使用 input 元素,这里把两种实现方式都写一遍。

    自定义移动端视频组件

    1. div 实现方式

    使用 div 想知道拖动、点击时百分占比的话,需要通过 getBoundingClientRect 来获取进度条相对于屏幕的位置和拖动/点击时相对于屏幕的位置,从而计算出视频播放时间变化的百分比。

    先来看看进度条结构,它包含三个部分:整体进度条、左侧已播放段、时间指针(thumb)

    {/* 进度条 */}
    <div
      id="progress"
      styleName="progress"
      style={progressStyle}
    >
    // 左侧已播放
    <div style={progressSuppleStyle}>
    	<span styleName="inside" style={insideStyle} />
    </div>
    // 时间指针
    <span
      styleName="point"
      style={pointStyle}
      onMouseDown={handleMouseDownProgress}
      onTouchStart={handleMouseDownProgress} />
    </div>
    

    需要通过监听鼠标移动事件,来判断用户拖动,注意在 PC 端和移动端监听的事件名不一样:

    // 监听手指放在时间指针上
    useEffect(() => {
        const hanldeMouseUp = () => {
          if(!progressLockRef.current) {
            progressLockRef.current = true;
            dispatch({ type: VideoTypes.PAUSE });
          }
        };
        document.body.addEventListener('mouseup', hanldeMouseUp);
        document.body.addEventListener('touchend', hanldeMouseUp);
    
        return () => {
          document.body.removeEventListener('touchend', hanldeMouseUp);
          document.body.removeEventListener('mouseup', hanldeMouseUp);
        };
      }, []);
    
    /**
       * 进度条拖动
       */
      useEffect(() => {
        const mouseMoveListener = (event:any) => {
          // 设置一个开关,只有用户放在时间指针上时才解锁
          // 防止用户在其它地方移动时,触发进度条拖动
          if(progressLockRef.current) return;
          
          const client = getClient(event);
          if(!client) return;
          const { clientX } = client;
    
          // 获取进度条位置
          const elePosition = getEleRelativeScreenPosition('progress');
          if(!elePosition) return;
          
          const { eleWidth: progressWidth, eleBorderLeft: progressBorderLeft, eleBorderRight: progressBorderRight } = elePosition;
    
          if(clientX <= progressBorderLeft) {
            // 拖动范围超出进度条左侧
            dispatch({ type: VideoTypes.MODIFY, payload: { currentTime: 0 } });
          }else if(clientX >= progressBorderRight) {
            // 拖动范围超出进度条右侧
            dispatch({ type: VideoTypes.MODIFY, payload: { currentTime: duration } });
          }else {
            // 拖动范围在进度条内
            const percent = (clientX - progressBorderLeft) / progressWidth;
            const time = Math.floor(duration * percent);
            dispatch({ type: VideoTypes.MODIFY, payload: { currentTime: time } });
          }
        };
    
        // PC 端
        window.addEventListener('mousemove', mouseMoveListener, false);
        // 移动端
        window.addEventListener('touchmove', mouseMoveListener, false);
    
        return () => {
          window.removeEventListener('mousemove', mouseMoveListener);
          window.removeEventListener('touchmove', mouseMoveListener, false);
        };
      }, [duration]);
    
    // utils.js
    // 获取元素位置
    export const getEleRelativeScreenPosition = (ele:HTMLElement|string) => {
      let _ele;
      if(typeof ele === 'object') {
        _ele = ele;
      } else if(typeof ele === 'string') {
        _ele = document.getElementById(ele);
      }
      if(!isElement(_ele) || !_ele) return null;
    
      const client = _ele.getBoundingClientRect();
      const eleWidth = _ele ? _ele.offsetWidth : 0;
      const eleHeight = _ele ? _ele.offsetHeight : 0;
      // 元素左侧边界相对于屏幕左侧的位置
      const eleBorderLeft = _ele ? client.left : 0;
      // 元素右侧边界相对于屏幕左侧的位置
      const eleBorderRight = _ele ? client.right : 0;
      // 元素顶部边界相对于屏幕顶部的位置
      const eleBorderTop = _ele ? client.top : 0;
      // 元素底部边界相对于屏幕顶部的位置
      const eleBorderBottom = _ele ? client.bottom : 0;
    
      return { eleWidth, eleHeight, eleBorderBottom, eleBorderLeft, eleBorderRight, eleBorderTop };
    };
    

    2. input 实现方式

    input 有 type=range 属性,原生支持拖动事件,实现起来比 div 简单很多。onChange 事件直接返回当前时间指针所占百分比

    <input
      css={inputCss}
      type="range"
      min="0"
      max="100"
      value={percent >> 0}
      step="1"
      onMouseDown={(e) => {
        operation.handleVideoPause();
      }}
      onMouseUp={(e) => {
        operation.handleVideoPlay();
      }}
      onTouchStart={(e) => {
        operation.handleVideoPause();
      }}
      onTouchEnd={(e) => {
        operation.handleVideoPlay();
      }}
    
      onChange={(e) => {
        e.preventDefault();
        e.stopPropagation();
        const time = Math.floor(duration * (Number.parseInt(e.target.value) / 100));
        // TODO: 修改 video currentTime
      }}
    />
    

    进入/退出全屏

    1. 进入全屏

    进入全屏直接使用 requestFullScreen API 即可,注意兼容性:

    const handleFullScreen = () => {
        const container = document.getElementById('video-container');
        if(!container) return;
    
        if(videoRef.current) {
          if (videoRef.current.requestFullscreen) {
            videoRef.current.requestFullscreen();
          } else if (videoRef.current.mozRequestFullScreen) {
            videoRef.current.mozRequestFullScreen();
          } else if (videoRef.current.webkitRequestFullscreen) {
            videoRef.current.webkitRequestFullscreen();
          } else if (videoRef.current.msRequestFullscreen) {
            videoRef.current.msRequestFullscreen();
          } else if (videoRef.current.webkitEnterFullScreen) {
            videoRef.current.webkitEnterFullScreen();
          }
      }
    

    在 iOS 中,只要视频进入全屏状态,无论 video 有没有设置 controls ,系统的全屏控制器都会接管 video,无法应用自定义的控制器。因此视频进入全屏状态后,无论在 iOS 还是 Android 均使用原生的全屏控制器。

    另外,在 Android 部分机型中,例如 OPPO 系列的手机,即使是处于 inline 的播放状态,也依然会使用系统自带的播放器,目前这个没有办法可以解决。

    2. 退出全屏

    组件还需要监听用户退出全屏,监听 fullscreenchange 再使用 document.fullscreenElement 判断是否有元素处于全屏状态。

    在 iOS 中无法监听到 fullscreenchange,搭配兼容性写法也依然无法监听。最后使用一个定时器去轮训检查。且在 iOS 中 document.fullscrrenElement 也始终返回 undefined,查阅了苹果的开发者文档,需要使用 webkitDisplayingFullscreen 才能检查到元素是否处于全屏状态。

    /**
       * 监听全屏
       */
      useEffect(() => {
        const handleFullScreenChange = () => {
          if(document.fullscreenElement === null) {
            dispatch({ type: VideoTypes.QUITFULLSCREEN });
          }
        };
        const handleIosInterval = () => {
          if(videoRef.current) {
            !videoRef.current.webkitDisplayingFullscreen && dispatch({ type: VideoTypes.QUITFULLSCREEN });
          }
        };
    
        // ios 无法监听到 fullscreenchange 事件,设置一个定时器检查
        const iosIntervalTimer = setInterval(handleIosInterval, 500);
    
        document.addEventListener('fullscreenchange', handleFullScreenChange, false);
        document.addEventListener('mozfullscreenchange', handleFullScreenChange, false);
        document.addEventListener('webkitfullscreenchange', handleFullScreenChange, false);
        document.addEventListener('msfullscreenchange', handleFullScreenChange, false);
        return () => {
          document.removeEventListener('fullscreenchange', handleFullScreenChange);
          document.removeEventListener('mozFullScreen', handleFullScreenChange);
          document.removeEventListener('webkitfullscreenchange', handleFullScreenChange);
          clearInterval(iosIntervalTimer);
        };
      }, []);
    

    兼容性问题处理

    在设计自定义视频播放组件时,原本设计的是通过控制 State,监听 State 的变化,然后修改 video 的状态。

    自定义移动端视频组件

    在 Chrome 中表现良好,但是在 ios Safari 中无效,提示错误为:

    自定义移动端视频组件

    原因是修改 State 是一个异步非实时的操作,而 Safari 不允许非用户触发的播放、暂停等行为。

    最后修改为直接操作 Video 的行为,然后监听 Video 行为变化从而改变 State 状态。

    自定义移动端视频组件

    总结

    观察了其它移动端自定义控制器,发现多是使用 div 的实现方式。目前 input 的实现方式符合实际需求,或许是有什么细节的地方需要使用 div 去实现。

    做移动端视频组件有很多兼容性问题需要注意,在 iOS 和 Android 端均有很多不一致的表现,并且很多 Android 手机厂商底层就不接受自定义视频控制器。


    起源地下载网 » 自定义移动端视频组件

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元