在uniapp中使用了某些前端库或iframe,需要操作这些库中的dom的时候, 而uni上又没有document等基础对象。也就无法操作这些dom去实现一些交互逻辑,那么,涉及到这些的前端类库就无法使用,例如html2、canvas、image、video。而要用这些怎么办,这是用就出现了renderjs这种视图层工具来进行渲染。大幅降低逻辑层和视图层的通讯损耗,提供高性能视图交互能力
使用方法:
- 在原有script下方再添加script,其中lang="renderjs"固定, module="demo",module的名字任意起
- 可以通过 this.$ownerInstance 获取当前组件的 ComponentDescriptor 实例。类似于vm
- 视图层绑定事件通过 module名称 . 逻辑层定义方法,有两个参数,1.事件对象event,2. 当前组件实例ComponentDescriptor
- 两个script间的通信需要通过:this.$ownerInstance 全局组件实例 或者 事件参数ComponentDescriptor 身上的callMethod方法,去抛出方法、传值,类似于vue组件间emit
- 可以使用 vue 组件的生命周期不可以使用 App、Page 的生命周期
使用参考:https://blog.csdn.net/dabaooooq/article/details/129272111
代码
uniapp在APP端video层级最高,不能被其它覆盖,引入videojs实现,使用renderjs实现原生的DOM操作。
javascript
<template>
<div class="full_screen_video" v-show="visible" :class="{ 'normal': !fullScreen }">
<!-- 遮盖video 添加点击事件 -->
<div class="video_mask" @click="foldShortVideo" ></div>
<p v-if="fullScreen" class="back" @click="closeDialogHandle">
<image src="@i/common/back_white.svg" alt=""></image>
<span>{{ title}}</span>
</p>
<view
:info="videoItem"
:change:info="videos.updateVideo"
:options="videoOptions"
:change:options="videos.optionsChange"
class="video_box"
ref="videoEle"
id="videoEle">
</view>
<view class="video_control_box" v-if="fullScreen" >
<!-- 开始结束时间 -->
<view class="video_time">
<span>{{ formatTime(videoCurrentTime) }} </span> <span>{{ formatTime(videoTotalTime) }}</span>
</view>
<!-- 视频播放器 -->
<div class="video_control">
<p class="disp_ac">
<image v-if="!isStart" src="@i/video/icon_video_start.svg" mode="widthFix" @tap="startAndStop('start')" alt=""></image>
<image v-else src="@i/video/icon_video_stop.svg" mode="widthFix" @tap="startAndStop('stop')" alt=""></image>
<image src="@i/video/icon_video_next.svg" mode="widthFix" @tap="playNext" alt="" v-if="videoList.length > 0"></image>
</p>
<p>
<view :class="{'normal_mode':normalMode}" class="formation_select_box mr60" @click="showOption = !showOption">
<span class="select_text">{{ speedValue +'x'}}</span>
<view class="select_menu" :class="{ ani: showOption }" v-show="showOption">
<view class="select_menu_item" v-for="item in speedOptions" :key="item.value">
<span :class="{'active': speedValue === item.value}" @click="changeSpeed(item.value)">{{ item.label +'x'}}</span>
</view>
</view>
</view>
<image v-if="normalMode" src="@i/video/full_screen.svg" mode="widthFix" @tap="fullScreenVideo" alt=""></image>
</p>
</div>
</view>
<view v-else class="video_control_box video_control_box_small" >
<div class="time_bar_box">
<div class="time_btn">
<image v-if="!isStart" src="@i/video/icon_video_start.svg" mode="widthFix" @tap="startAndStop('start')" alt=""></image>
<image v-else src="@i/video/icon_video_stop.svg" mode="widthFix" @tap="startAndStop('stop')" alt=""></image>
<image src="@i/video/icon_video_next.svg" mode="widthFix" @tap="playNext" alt="" v-if="videoList.length > 0"></image>
</div>
<div class="time_bar">
<span>{{ formatTime(videoCurrentTime) }} </span> <span>{{ formatTime(videoTotalTime) }}</span>
</div>
</div>
<div class="video_btn">
<view class="formation_select_box mr50" @click="showOption = !showOption">
<span class="select_text">{{ speedValue +'x'}}</span>
<view class="select_menu" :class="{ ani: showOption }" v-show="showOption">
<view class="select_menu_item" v-for="item in speedOptions" :key="item.value">
<span :class="{'active': speedValue === item.value}" @click="changeSpeed(item.value)">{{ item.label +'x'}}</span>
</view>
</view>
</view>
<image src="@i/video/download.svg" mode="widthFix" @tap="downloadVideo" alt=""></image>
<image v-if="normalMode" src="@i/video/full_screen.png" mode="widthFix" @tap="fullScreenVideo" alt=""></image>
</div>
</view>
<!-- 视频列表 -->
<div class="video_list" v-if="showVideoList && videoList.length" >
<scroll-view scroll-x="true" >
<div class="video_list_item">
<div v-for="(item, index) in videoList" :key="index" class="video_item" :class="{'active': index === currentVideoIndex}" @click="playVideo(item, index)">
<div class="video_img">
<image class="snapshot" :src="item.snapshot && !item.imgError ? item.snapshot : defaultSnapshot" @error="item.imgError = true" alt="snapshot" ></image>
<span class="total_time" v-if="item.end && item.start">
{{formatTime(item.end - item.start)}}
</span>
</div>
<div class="clip_info">
<div class="text_over">{{item.name || item.label || ''}}</div>
<div class="video_list_time">{{ formatDate(item.createTime).split('-').slice(0,3).join('/').split(' ').slice(0,1).join() }}</div>
</div>
</div>
</div>
</scroll-view>
</div>
</div>
</template>
<script>
export default {
name: 'pad-video-play', // 在主视频内播放的组件,短片会跳转到主视频对应的时间
props: {
// 控制显示隐藏
visible: {
type: Boolean,
default: false
},
// 主视频下的短视频播放列表
videoList: {
type: Array,
default:() => []
},
// 主视频播放源
videoMain: {
type: Object,
default: () => { }
},
// 视频类型, 默认为全屏播放
videoType: {
type: String,
default: 'full_screen'
},
},
data() {
return {
videoCurrentTime: 0, // 视频当前时间
videoTotalTime: 0, // 视频总时长
isStart: false, // 是否开始
volumeValue: 1, // 音量初始值(0-1)
showVideoList: true, // 是否显示视频列表
currentVideoIndex: -1, // 当前播放的视频下标
defaultSnapshot: require("static/images/video/default_snapshot.svg"), // 默认缩略图
videoOptions: {
closeDialog: false
}, // 视频操作选项
fullScreen: this.videoType === 'full_screen', // 是否是全屏
speedValue: '1.0', // 初始播放速度
showOption:false, // 控制倍速弹窗
speedOptions: [ // 阵型选项
{ label: '2.0', labelEn: '2.0', value: '2.0' },
{ label: '1.5', labelEn: '1.5', value: '1.5' },
{ label: '1.25', labelEn: '1.25', value: '1.25' },
{ label: '1.0', labelEn: '1.0', value: '1.0' },
{ label: '0.5', labelEn: '0.5', value: '0.5' },
],
videoItem: {},
title:''
};
},
computed: {
// 是否是正常模式下的视频播放
normalMode() {
let boolean = this.videoType === 'normal';
return boolean;
}
},
watch: {
currentVideoIndex(newVal) {
this.$emit('change-video-index', newVal);
},
videoMain: {
handler (newVal) {
this.videoItem = {
url: newVal.url
}
this.title = newVal.name
},
immediate: true,
deep:true
},
visible(newVal) {
if (newVal) {
this.videoOptions = {
closeDialog: false,
volumeChangeOption: { volume: 1 }
}
this.volumeValue = 1;
}
}
},
methods: {
foldShortVideo(){
this.showVideoList = !this.showVideoList
},
// 关闭弹框
closeDialogHandle () {
this.videoOptions = {
closeDialog: true
}
if (this.videoType === 'full_screen') {
// setTimeout(() => {
this.$emit('cancelVideo', false);
// },100)
} else {
this.fullScreen = false;
}
this.showVideoList = false;
this.$emit('update:visible',false)
},
// 开始视频
onPlayerPlay() {
this.speedValue = '1.0'
this.isStart = true;
},
// 实时更新
onTimeUpdate(currentTime){
this.videoCurrentTime = currentTime;
},
// 暂停视频
onPlayerPause() {
this.isStart = false;
},
// 加载视频源数据
onLoadedmetadata(totalTime) {
this.videoTotalTime = totalTime;
},
// 开始或暂停视频
startAndStop(type) {
this.videoOptions = {
startAndStopOption: { type }
}
},
// 改变视频音量
volumeChange(volume) {
this.volumeValue = volume;
this.videoOptions = {
volumeChangeOption: { volume }
}
},
// 播放视频
playVideo(item, index) {
this.speedValue = '1.0'
// // 在正常模式播放下, 不可重复点击当前视频
if (this.normalMode && item && this.currentVideoIndex === index) {
return;
}
this.$set(this.videoItem, 'startTime', item.start);
this.currentVideoIndex = index;
this.videoList.forEach(item => {
item.forceUpdate = false
})
item.forceUpdate = true
},
// 改变视频的速度
changeSpeed(speed) {
this.speedValue = speed || '1.0';
this.videoOptions = {
playbackRate: { speed }
}
},
// 播放下一个
playNext(){
this.speedValue = '1.0'
this.videoList.forEach(item => {
item.forceUpdate = false
})
let index = this.currentVideoIndex;
if(this.currentVideoIndex+1<this.videoList.length){
this.currentVideoIndex = index+1;
this.$set(this.videoList[index+1],'forceUpdate',true)
this.$set(this.videoList, index + 1, this.videoList[index + 1]);
this.$set(this.videoItem, 'startTime', this.videoList[index+1].start);
}else{
this.currentVideoIndex = 0;
this.$set(this.videoList[0],'forceUpdate',true)
this.$set(this.videoList, 0, this.videoList[0]);
this.$set(this.videoItem, 'startTime', this.videoList[0].start);
}
},
// 进入/退出 全屏视频
fullScreenVideo() {
this.fullScreen = !this.fullScreen;
},
// 格式化时间
formatTime(result) {
let h = Math.floor(result / 3600) < 10 ? "0" + Math.floor(result / 3600) : Math.floor(result / 3600);
let m = Math.floor((result / 60) % 60) < 10 ? "0" + Math.floor((result / 60) % 60) : Math.floor((result / 60) % 60);
let s = Math.floor(result % 60) < 10 ? "0" + Math.floor(result % 60) : Math.floor(result % 60);
return Math.floor(result / 3600) == 0 ? m + ":" + s : h + ":" + m + ":" + s;
},
// 格式化日期
formatDate(createTime){
let date = new Date(createTime);
let str = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
return str;
}
}
};
</script>
<script module="videos" lang="renderjs">
import './video-offset.js'; // videojs 引入
import Videojs from 'video.js';
import 'video.js/dist/video-js.css';
export default {
data() {
return {
videoPlayer: null, // 当前视频播放器
}
},
mounted() {
},
methods: {
// 初始化视频
initVideoJS(item) {
if(this.videoPlayer){
this.videoPlayer.dispose();
this.videoPlayer = null;
}
let that = this;
let videoEle = document.createElement('video');
videoEle.style = 'width:100%; height:100%';
videoEle.setAttribute("class", "video-js vjs-big-play-centered");
let videos = document.getElementById('videoEle');
console.log(videos);
videos.appendChild(videoEle)
let option = {
controls: true, // 是否显示控制条
preload: 'auto', // 是否预加载视频数据
muted: false, // 是否静音
language: 'zh-CN', // 设置语言
autoplay: true, // 自动播放, 正常模式下不进行自动播放
sources: [ // 视频源
{
type: "video/mp4",
src: item.url
}, {
type: "video/webm",// webm格式
src: item.url
}, {
type: "video/mov", // mov格式
src: item.url
},
],
controlBar: { // 设置控制条组件
children: [
{ name: 'progressControl' }, // 播放进度条
// // { name: 'fullscreenToggle' } // 全屏按钮
]
}
};
// video.js初始化实例化的对象
this.videoPlayer = Videojs(videoEle, option, function onPlayerReady() {
// 开始视频
this.on('play', function() {
that.$ownerInstance.callMethod('onPlayerPlay');
})
// 暂停视频
this.on('pause', function() {
that.$ownerInstance.callMethod('onPlayerPause');
})
// 实时更新
this.on('timeupdate', function() {
let currentTime = that.videoPlayer ? that.videoPlayer.currentTime() : 0;
that.$ownerInstance.callMethod('onTimeUpdate', currentTime);
})
// 加载视频源数据
this.on('loadedmetadata', function() {
let totalTime = parseInt(that.videoPlayer.duration()) || 0;
that.$ownerInstance.callMethod('onLoadedmetadata', totalTime);
})
});
},
// 监听 videoData 数据变更
updateVideo(newValue) {
if(!newValue || !newValue.url) return;
if(this.videoPlayer && newValue.startTime){
this.videoPlayer.currentTime(newValue.startTime)
return
}
this.initVideoJS(newValue);
},
// 监听改变视频操作选项
optionsChange(newValue) {
if (newValue && newValue.startAndStopOption) {
// 开始或暂停视频
let { type } = newValue.startAndStopOption;
type === 'start' ? this.videoPlayer.play() : this.videoPlayer.pause();
} else if (newValue && newValue.volumeChangeOption) {
// 改变视频音量
let { volume } = newValue.volumeChangeOption;
this.videoPlayer && this.videoPlayer.volume(volume);
} else if (newValue && newValue.closeDialog) {
console.log('销毁了')
// 销毁videoJs实例
this.videoPlayer && this.videoPlayer.dispose();
this.videoPlayer = null
} else if (newValue && newValue.continuePlay) {
// 继续播放视频
let { url } = newValue.continuePlay;
this.initVideoJS(url);
} else if(newValue && newValue.playbackRate){
let { speed } = newValue.playbackRate;
// 改变视频的播放速度
this.videoPlayer && this.videoPlayer.playbackRate(speed);
}
}
}
}
</script>
<style lang='less' scoped>
.video_mask {
position: absolute;
width: 100%;
height: calc(100% - 62.81rpx);
top: 0;
left: 0;
background-color: transparent;
z-index: 1;
}
::v-deep {
// 视频style
.video_box{
width: 100%;
height: 100%;
video{
object-fit:fill;
}
}
.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar{
opacity: 1;
}
.video-js{
position: relative;
.vjs-control-bar {
width:663.32rpx;
left: 43.34rpx;
bottom: 47.9rpx;
background-color: transparent;
z-index: 105;
opacity: 1;
}
.vjs-play-progress{
background: #38CB89;
&::before{
color: #38CB89;
}
}
.vjs-slider{
background-color: rgba(255, 255, 255, 0.36);
}
}
}
.disp_ac{
display: flex;
align-items: center;
}
/* 文字超出部分设置为... */
.text_over {
max-width: 85%;
word-break: break-all;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2; /* 这里是超出几行省略 */
overflow: hidden;
}
.full_screen_video{
width: 100%;
height: 100%;
position: fixed;
z-index: 102;
left: 0;
top: 0;
box-sizing: border-box;
color: #fff;
font-family: 'SysFontR';
// 默认视频样式
&.normal{
width: 100%;
height: 100%;
position: relative;
.video_list,.video_shrink{
display: none;
}
.video_control_box{
width: 100%;
height: 37.69rpx;
position: absolute;
left: 0;
bottom: 4.71rpx;
z-index: 5;
display: flex;
flex-direction: column;
align-items: center;
padding: 0 10.05rpx;
box-sizing: border-box;
&.video_control_box_small{
.formation_select_box{
bottom: 7rpx;
.select_text{
font-size: 10.05rpx;
}
.select_menu{
bottom:0;
transform:scale(.8)
}
}
}
.time_bar_box{
width: 100%;
height: 12.25rpx;
display: flex;
align-items: center;
.time_btn{
image{
width: 11.93rpx;
height: 9.42rpx;
margin-right: 10.05rpx;
}
}
.time_bar{
flex: 1;
display: flex;
justify-content:space-between;
font-size: 9.74rpx;
color: #f3f3f3;
}
}
.video_btn{
width: 100%;
display: flex;
justify-content: flex-end;
text-align: right;
margin-top: 6.28rpx;
image{
width: 10.99rpx;
height: 10.05rpx;
margin-right: 15.7rpx;
}
}
}
.video_control{
height: 47.11rpx;
bottom: 4.71rpx;
image{
width: 10.05rpx !important;
height: 10.05rpx !important;
margin-right: 15.7rpx;
}
}
::v-deep {
.vjs-control-bar{
width: calc(100% - 74.75rpx - 37.69rpx);
left: 76.75rpx;
bottom: 27.84rpx;
}
}
}
.back{
position: absolute;
z-index: 200;
top: 0;
left: 0;
width: 100%;
height: 58.59rpx;
padding-left: 14.64rpx;
font-size: 13.82rpx;
display: flex;
align-items: center;
cursor: pointer;
background: linear-gradient(180deg, #000000 0%, rgba(0, 0, 0, 0) 100%);
image{
width: 6.44rpx;
height: 12.3rpx;
margin-right: 20.1rpx;
}
}
.formation_select_box{
position: relative;
border-radius: 3.77rpx;
display: flex;
align-items: center;
justify-content: center;
&.normal_mode{
right: 118rpx;
}
.select_text{
font-family: 'AkrobatMedium';
font-size: 11.31rpx;
color: #fff;
position: relative;
}
.select_menu {
width: 43.66rpx;
position: absolute;
bottom: 18.04rpx;
left: -13rpx;
z-index: 999;
padding: 13.82rpx 0 0;
text-align: center;
box-sizing: border-box;
border-radius: 3.77rpx;
background: #000000;
color: #F3F3F3;
.select_menu_item {
position: relative;
z-index: 2;
margin-bottom: 12.56rpx;
>span {
display: inline-block;
width: 100%;
height: 100%;
box-sizing: border-box;
font-size: 10.05rpx;
font-family: AkrobatRegular;
&.active{
color: #38CB89;
}
}
}
}
.ani {
animation: ani 0.2s;
}
@keyframes ani {
0% {
transform: scaleY(0);
}
100% {
transform: scaleY(1);
}
}
.mask_box {
width: 70vw;
height: 100vh;
position: fixed;
left: 0;
top: 60rpx;
z-index: 99;
background: transparent;
}
}
.video_time{
position: absolute;
bottom: 49.94rpx;
width: 100%;
box-sizing: border-box;
padding:0 15.7rpx;
line-height: 14.13rpx;
display: flex;
justify-content:space-between;
z-index: 104;
}
.video_control{
position: absolute;
left: 0;
z-index: 5;
bottom: 5.85rpx;
width: 161.13rpx;
height: 35.15rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 19.04rpx;
box-sizing: border-box;
width: 100%;
p{
display:flex;
align-items: center;
image{
width: 17.59rpx;
height: 17.59rpx;
margin-right: 15.23rpx;
// opacity: .6;
}
.video_volume{
position: relative;
.video_volume_slider{
display: none;
position: absolute;
right: 30px;
top: -85px;
padding-top: 5px;
background-color: rgba(0, 0, 0, 0.36);
&.hover{
display: inline-block;
}
}
}
}
}
.video_list{
position: absolute;
width: 100%;
top: 0;
right: 0;
z-index: 201;
background: rgba(29, 29, 29, 0.8);
backdrop-filter: blur(50px);
color: #fff;
padding-top: 25.13rpx;
padding-bottom: 6.28rpx;
box-sizing: border-box;
display: flex;
// transition: all 200ms;
.close_img{
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 236.81rpx;
width: 20.5rpx;
height: 75rpx;
}
.video_item{
width: 83.54rpx;
flex-shrink: 0;
display: flex;
flex-direction: column;
margin-left: 12.56rpx;
box-sizing: border-box;
display: flex;
&.active{
.text_over{
font-family: SysFontM;
color: #38CB89;
}
}
.video_img{
width: 100%;
height: 47.11rpx;
box-sizing: border-box;
position: relative;
image.snapshot{
border-radius: 3.77rpx;
width: 100%;
height: 100%;
}
.total_time{
font-family: SysFontR;
height: 12.89rpx;
position: absolute;
left: 2.92rpx;
bottom: 2.92rpx;
padding: 0 5.85rpx;
font-size: 8.2rpx;
line-height: 12.89rpx;
color: rgb(255, 255, 255);
background: rgba(30, 30, 30, .6);
}
}
.clip_info{
width: 100%;
box-sizing: border-box;
font-size: 8.2rpx;
color: rgba(255, 255, 255, .8);
position: relative;
padding: 4.71rpx 0;
// .clip_label{
// font-size: 10.68rpx;
// color: rgba(255, 255, 255,1);
// line-height: 14.06rpx;
// margin-bottom: 7.54rpx;
// display: flex;
// align-items: center;
// }
}
}
}
.video_list_item {
display: flex;
width: 100%;
overflow-x: auto;
}
.video_shrink{
position: fixed;
top: 50%;
transform: translateY(-50%);
right: 0;
width: 20.5rpx;
height: 75rpx;
}
.video_list_time {
font-family: AkrobatRegular;
margin-top: 3px;
}
}
</style>;