Uniapp 基于renderjs封装原生h5 video组件(解决自定义UI层级+兼容ios全屏问题)

子窗体方案请参考:

juejin.cn/editor/draf...

问题背景

在 UniApp 开发中,当使用原生组件(如 video)时,会遇到原生组件层级最高的限制,导致自定义的 UI 控件(如播放按钮、进度条等)无法覆盖在视频组件之上。这是由原生组件渲染层级决定的常规限制。


解决方案

我们可以使用renderjs来封装原生h5 video组件,从而实现自定义UI层级的控制

优缺点

  • 优点

    • 直接操作DOM:renderjs运行在视图层,可以直接操作DOM元素,方便对video组件进行精细的控制和自定义,如添加自定义的播放按钮、进度条等。
    • 高性能交互:可以大幅降低逻辑层和视图层之间的通信损耗,提供更流畅的交互体验,适合需要高性能视图交互的场景。
    • 使用Web的JS库:能够使用一些为Web设计的JS库,如echarts、f2等,结合video组件实现更丰富的功能和效果。
  • 缺点

    • app端性能损耗:在app端使用时,由于需要跨逻辑层和视图层通信,相比于纯逻辑层开发,仍会存在一定的性能损耗。
    • 开发复杂度高:需要同时处理逻辑层和视图层的代码,且视图层的开发与逻辑层有所不同,增加了开发和维护的难度。
    • 跨平台兼容性:虽然可以在app和H5中使用,但在不同平台上的表现可能会存在差异,需要进行额外的适配工作

实现步骤

先上效果图

1. 创建组件

VideoPlayer.vue 基础结构

html 复制代码
<template>
  <view
      class="player-wrapper"
      :id="videoWrapperId"
      :parentId="id"
      :randomNum="randomNum"
      :change:randomNum="domVideoPlayer.randomNumChange"
      :viewportProps="viewportProps"
      :change:viewportProps="domVideoPlayer.viewportChange"
      :videoSrc="videoSrc"
      :change:videoSrc="domVideoPlayer.initVideoPlayer"
      :command="eventCommand"
      :change:command="domVideoPlayer.triggerCommand"
      :func="renderFunc"
      :change:func="domVideoPlayer.triggerFunc"
  />
</template>
// 不支持setup
<script>
    export default {}
</script>

<script module="domVideoPlayer" lang="renderjs" src="./render-service/videoEl.js"></script>

2. 创建video组件

使用原生渲染语法编写 UI

js 复制代码
export default {
    ....
    // 聚合其他模块方法
    mixins: [coverEl, progressEl, fullscreen],
    methods: {
        async initVideoPlayer(src) {
          this.delayFunc = null;
          await this.$nextTick();
          if (!src) return;
          if (this.videoEl) {
            // 切换视频源
            if (!this.isApple() && this.loadingEl) {
              this.loadingEl.style.display = 'block';
            }
            this.videoEl.src = src;
            return;
          }

          const videoEl = document.createElement('video');
          this.videoEl = videoEl;
          // 开始监听视频相关事件
          this.listenVideoEvent();

          const { autoplay, muted, controls, loop, playbackRate, objectFit, poster } = this.renderProps;
          videoEl.src = src;
          videoEl.autoplay = autoplay;
          videoEl.controls = controls;
          videoEl.loop = loop;
          videoEl.muted = muted;
          videoEl.playbackRate = playbackRate;
          videoEl.id = this.playerId;
          // videoEl.setAttribute('x5-video-player-type', 'h5')
          videoEl.setAttribute('preload', 'auto');
          videoEl.setAttribute('playsinline', true);
          videoEl.setAttribute('webkit-playsinline', true);
          videoEl.setAttribute('crossorigin', 'anonymous');
          videoEl.setAttribute('controlslist', 'nodownload');
          videoEl.setAttribute('disablePictureInPicture', true);
          videoEl.style.objectFit = objectFit;
          poster && (videoEl.poster = poster);
          videoEl.style.width = '100%';
          videoEl.style.height = '100%';

          // 插入视频元素
          // document.getElementById(this.wrapperId).appendChild(videoEl)
          const playerWrapper = document.getElementById(this.wrapperId);
          playerWrapper.insertBefore(videoEl, playerWrapper.firstChild);

          // 插入loading 元素(遮挡安卓的默认加载过程中的黑色播放按钮)
          this.createLoading();

          // 创建自定义UI层
          this.createCover();
    },
    ....
    }
}

3. 创建自定义ui

js 复制代码
export default {
    methods: {
        // 创建自定义视频上的悬浮层:包含自定义控制栏,自定义头部导航,
        createCover() {
          const coverEl = document.createElement('div');
          this.coverEl = coverEl;
          coverEl.className = 'cover-wrapper';
          Object.assign(coverEl.style, this.CoverStyles.coverWrapper);

          const parentEl = document.getElementById(this.wrapperId);
          // 创建返回
          this.createBack(parentEl);
          // 创建进度条
          this.createControls();

          const mousedownHandler = (e) => {
            this.resetCoverTimer();
          };

          coverEl.removeEventListener('click', mousedownHandler);
          coverEl.addEventListener('click', mousedownHandler);

          parentEl.appendChild(coverEl);
        },
        
         // 创建控制栏
        createControls() {
          const controlsEl = document.createElement('div');
          this.controlsEl = controlsEl;
          controlsEl.className = 'controls-wrapper';
          Object.assign(controlsEl.style, this.CoverStyles.controlsWrapper);

          // 创建播放状态按钮
          this.createPlayButton();
          // 创建当前时间
          this.createCurrentTime();
          // 创建进度条
          this.createProgress();
          // 创建视频总时间
          this.createDuration();
          // 创建全屏
          this.createFullScreen();
          // 创建编辑
          this.createEdit();
          // 创建隐藏视频
          this.createHidden();

          // 最后整体创建控制面板
          this.coverEl?.appendChild(controlsEl);
        },
    }
}

3.1 自定义进度条

js 复制代码
export default {
    methods: {
        ...
        initProgressBar(container) {
          // 进度条背景
          this.progressBar = document.createElement('div');
          this.progressBar.className = 'progress-bar';
          Object.assign(this.progressBar.style, this.ProgressStyles.progressBar);

          // 缓冲进度条
          this.bufferBar = document.createElement('div');
          this.bufferBar.className = 'buffer-bar';
          Object.assign(this.bufferBar.style, this.ProgressStyles.bufferBar);

          // 当前播放进度
          this.currentProgress = document.createElement('div');
          this.currentProgress.className = 'current-progress';
          Object.assign(this.currentProgress.style, this.ProgressStyles.currentProgress);

          this.thumb = document.createElement('div');
          this.thumb.className = 'thumb';
          Object.assign(this.thumb.style, this.ProgressStyles.thumb);
          // 创建实际显示的thumb
          const thumbDot = document.createElement('div');
          thumbDot.className = 'thumb-dot';
          Object.assign(thumbDot.style, this.ProgressStyles.thumbDot);
          this.thumb.appendChild(thumbDot);


          this.progressBar.appendChild(this.bufferBar);
          this.progressBar.appendChild(this.currentProgress);
          this.progressBar.appendChild(this.thumb);

          container.appendChild(this.progressBar);

          this.bindDragEvents();
        },
        ...
    }
}

遇到哪些问题

1. 为什么自定义ui层不写到业务逻辑层?

原因:视频和进度条之间交互较多,写到逻辑层增加通讯损耗

2. renderjs层代码量过多怎么拆分?

解决办法:1.使用标签引入render视图层的代码"<script module="domVideoPlayer" lang="renderjs" src="./render-service/videoEl.js >"

2.然后各模块通过mixins方式复用

3. app端横屏问题

借助html5+的api:

js 复制代码
if (isFullScreen) {
  plus.screen.lockOrientation('landscape-primary');
} else {
  plus.screen.lockOrientation('portrait-primary');
}

4. ios低版本无法使用RequestFullscreen相关的api,如何解决

查看兼容性:

可以看出safari on ios 在3.2-11.4版本均不支持,12-18.4后使用webkitRequestFullscreen 增加前缀兼容处理;

对于不支持的情况,这里使用css方案模拟全屏,解决思路

js 复制代码
  
// 全屏和退出全屏的样式
export const FullscreenStyles = {
  full: {
    position: 'fixed',
    top: 0,
    left: 0,
    width: '100%',
    height: '100%',
    zIndex: '9999',
  },
  init: {
    position: 'relative',
    top: 'unset',
    left: 'unset',
    width: 'auto',
    zIndex: 'unset',
  },
};

                                  
const FixedIndex = 9998; // 全屏时的层级

// 是否在全屏状态
isFullscreen(containerEl) {
  if (!containerEl) return;
  const isSupport = this.isSupportFullscreen();
  if (isSupport) {
    return !!fscreen.fullscreenElement;
  } else {
    const styles = containerEl.style;
    return styles && styles['z-index'] && styles['z-index'] === `${FixedIndex}`; // 判断是否处于全屏
  }
},


// 请求全屏
requestFullscreen(containerEl) {
  if (!containerEl) return;
  const isSupport = this.isSupportFullscreen();
  if (isSupport) {
    fscreen.requestFullscreen(containerEl);
  } else {
    Object.assign(containerEl.style, { ...this.FullscreenStyles.full, zIndex: FixedIndex });
  }
},

// 取消全屏
exitFullscreen(containerEl, initStyles = {}) {
  if (!containerEl) return;
  const isSupport = this.isSupportFullscreen();
  if (isSupport) {
    fscreen.exitFullscreen();
  } else {
    Object.assign(containerEl.style, {
      ...this.FullscreenStyles.init,
      ...initStyles,
    });
  }
},

参考资料

1.基于此开源组件的video开发:ext.dcloud.net.cn/plugin?id=1... 2.h5video简介: developer.mozilla.org/zh-CN/docs/...

完整代码见如下git仓库:

github.com/yc-lm/uniap...

相关推荐
2401_8784545316 分钟前
Es6进阶
前端·javascript·es6
经常见25 分钟前
手写call,bind,apply
javascript·面试
辛勤劳作30 分钟前
使用pdf.js实现文档阅读器踩坑记录
javascript
Cutey91631 分钟前
解决 Input Number 输入框出现科学计数法(如 -1e-18)的问题
前端·javascript·面试
辛勤劳作32 分钟前
pdf.js优化:自定义文本层
javascript
辛勤劳作34 分钟前
桌面端webview弹窗首屏速度优化
javascript
知否技术1 小时前
JSON.parse不是万能药!手把手教你写一个深拷贝!
前端·javascript
海底火旺2 小时前
为什么你的 JS 代码有时崩溃有时侥幸运行?LHS/RHS 的“潜规则”全解析
javascript
忆柒2 小时前
大量数据的渲染优化
前端·javascript·面试
一天睡25小时2 小时前
Vue 3 响应式原理:computed的模板解包机制
前端·javascript·vue.js