子窗体方案请参考:
问题背景
在 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/...