记录:vue移动端富文本内容替换自定义视频全屏实现
2023-12-13 14:07:56
使用vue3
<template>
<div v-html="content" class="html-content text-black ql-editor" @click="jumpLink($event)" v-if="htmlContent"></div>
</template>
<script setup lang="ts">
import { showImagePreview } from 'vant';
import { UtilService } from '@common/services/util.service';
import 'quill/dist/quill.core.css';
import 'quill/dist/quill.snow.css';
import 'quill/dist/quill.bubble.css';
import { onMounted, onUnmounted, ref } from 'vue';
import { Utils } from '@common/utils/utils';
import { log } from '@common/utils';
import LoopBtn from '@mobile/components/player/loopBtn';
import FullscreenBtn from '@mobile/components/player/fullscreenBtn';
import videoJs from 'video.js';
import 'video.js/dist/video-js.min.css';
let props = defineProps(['htmlContent', 'fillImg']);
let emits = defineEmits(['onImageOpen']);
//正在显示提示框,默认不显示
let showing = false;
//是否在预览图片
let previewImg = false;
let content = ref();
let myPlayerList = ref([]);
// 初始化
onBeforeMount(() => {
if (props.htmlContent.indexOf('<video') > -1) {
htmlContentFn();
}
});
onMounted(async () => {
if (props.htmlContent.indexOf('<video') > -1) {
if (count.value > 0) {
for (let idx = 0; idx < count.value; idx++) {
initPlayer({}, idx);
}
}
}
});
onBeforeUnmount(() => {
if (myPlayerList.value) {
myPlayerList.value.forEach((ele) => {
ele.dispose();
});
}
});
let count = ref(0);
function htmlContentFn() {
var index = props.htmlContent.indexOf('<video');
while (index !== -1) {
count.value++;
index = props.htmlContent.indexOf('<video', index + 1);
}
if (count.value > 0) {
let str = props.htmlContent;
for (let idx = 0; idx < count.value; idx++) {
let videoItem =
'<video id="media-player-' +
idx +
'" class="video-js vjs-default-skin vjs-big-play-centered" webkit-playsinline playsinline x5-playsinline x5-video-player-fullscreen="true"' +
' x5-video-player-type="h5" x5-video-orientation="portrait" preload="metadata">' +
' <source src="' +
getUrl(str) +
'" type="application/x-mpegURL"></video>';
let a = str.indexOf('<video src=');
let b = str.indexOf('</video>', a);
let d = str.substring(a, b + 8);
str = str.replace(d, videoItem);
}
content.value = str;
}
}
function getUrl(value: string) {
if (Utils.trim(value) != '') {
let data = '';
// @ts-ignore
value.replace(/<video [^>]*src=['"]([^'"]+)[^>]*>/, function (match, capture) {
data = capture;
});
return data;
}
return '';
}
/**
* 初始化播放器
*
* @param playOptions {@link Object}
*/
const initPlayer = (playOptions?: any, idx?: any): void => {
// 播放器配置
let options = getDefaultOptions();
Object.assign(options, playOptions);
// 倍速播放
options.playbackRates = [0.5, 1, 1.5, 2, 3, 5];
console.log('===================options==========', options);
// 判断 m3u8 视频
if (options.sources && options.sources.length > 0) {
options.sources.map((s: any) => {
try {
if (!Utils.isNullOrEmpty(s.src) && s.src.search(/.m3u8/) > -1) {
s.type = 'application/x-mpegURL';
}
} catch (e) {
log(e);
}
});
}
// -------------------------------------------------------------------------
// 初始化及初始化后的操作
// -------------------------------------------------------------------------
let $player;
$player = videoJs('media-player-' + idx, options, () => {
//循环播放
//@ts-ignore
videoJs.registerComponent('LoopBtn', LoopBtn);
const touchOptions = {};
const controlBarIndex = $player.controlBar.children().length - 1;
$player.controlBar.addChild('LoopBtn', touchOptions, controlBarIndex);
//全屏横屏播放
// @ts-ignore
videoJs.registerComponent('FullscreenBtn', FullscreenBtn);
const Index = $player.controlBar.children().length - 1;
$player.controlBar.addChild('FullscreenBtn', touchOptions, Index);
// 移除画中画按钮
$player.controlBar.removeChild('pictureInPictureToggle');
});
myPlayerList.value.push($player);
};
/**
* 播放器默认配置
*
* @return {@link Object}
*/
const getDefaultOptions = (): any => {
return {
autoplay: false, // 自动播放:true/false
controls: true, // 是否显示底部控制栏:true/false
playbackRates: [], // 默认不允许倍速播放
width: 800, // 视频播放器显示的宽度
height: 500, // 视频播放器显示的高度
loop: false, // 是否循环播放:true/false
muted: false, // 设置默认播放音频:true/false
poster: '', // 视频开始播放前显示的图像的URL。这通常是一个帧的视频或自定义标题屏幕。一旦用户点击“播放”图像就会消失
src: 'https://vjs.zencdn.net/v/oceans.mp4', // 要嵌入的视频资源url,The source URL to a video source to embed.
// techOrder: ['html5', 'flash'], // 使用播放器的顺序,下面的示例说明优先使用html5播放器,如果不支持将使用flash
notSupportedMessage: false, // 是否允许重写默认的消息显示出来时,video.js无法播放媒体源
plugins: {}, // 插件
sources: [{ src: 'https://vjs.zencdn.net/v/oceans.mp4', type: 'video/mp4' }], // 资源文件等价于html中的形式source标签
aspectRatio: '16:9', // 将播放器置于流体模式下,计算播放器动态大小时使用该值。
// 该值应该是比用冒号隔开的两个数字(如“16:9”或“4:3”)。
fluid: false, // 是否自适应布局,播放器将会有流体体积。换句话说,它将缩放以适应容器。
// 如果<video>标签有“vjs-fluid”样式时,这个选项会自动设置为true。
preload: 'metadata', // 建议浏览器是否在加载<video>元素时开始下载视频数据。(预加载)
// auto:立即加载视频(如果浏览器支持它)。一些移动设备将不会预加载视频,以保护用户的带宽/数据使用率。这就是为什么这个值被称为“自动”,而不是更确凿的东西
// metadata:只加载视频的元数据,其中包括视频的持续时间和尺寸等信息。有时,元数据会通过下载几帧视频来加载。
// none,
controlBar: {
fullscreenToggle: false,
},
};
};
function jumpLink(event: any) {
if (event.target.nodeName.toUpperCase() === 'IMG') {
// 预览图片
previewImg = true;
showImagePreview({
images: [event.target.currentSrc],
onClose() {
previewImg = false;
emits('onImageOpen', previewImg);
},
});
emits('onImageOpen', previewImg);
} else if (event.target.nodeName.toUpperCase() === 'A') {
// 处理文本内外链接,内连接跳转本系统,外链接如www.baidu.com
const targetData: string = event.target.getAttribute('data').replace(/'/g, '"');
const data = JSON.parse(targetData);
if (data && data.data && typeof data.data === 'string') {
const url: string = data.data;
UtilService.openBrowser(url);
} else if (data && data.data && typeof data.data === 'object') {
const activityId: string = data.data.acinvityId;
const type = data.data.type;
const title: string = data.data.title;
const sectionNum: number = data.data.sectionNum;
if (!showing) {
showing = true;
UtilService.goToActivity(activityId, type, title, sectionNum).then(() => (showing = false));
}
}
}
}
</script>
<style lang="scss" scoped>
.html-content {
width: 100%;
padding: 0;
margin: 0;
font-weight: normal;
video {
width: inherit;
max-width: 100%;
height: auto;
}
audio {
width: inherit;
max-width: 100%;
}
:deep(p) {
padding: 0;
//margin: 0 0 0.5rem;
word-break: break-all;
font-size: 0.938rem;
font-weight: normal;
}
a.lms-link {
color: var(--ion-color-primary);
}
a.lms-url {
color: var(--ion-color-primary);
}
img {
border-radius: 0.7rem;
width: 5.75rem;
height: 5.75rem;
object-fit: cover;
}
}
:deep(.vjs-loop-btn) {
img {
width: 1.1rem !important;
height: 1.1rem !important;
margin: auto;
}
}
:deep(.vjs-fullscreen-btn) {
img {
width: 0.87rem !important;
height: 0.87rem !important;
margin: auto;
}
}
//全屏样式
:deep(.vjs-fullscreen-enter) {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 999;
transform: rotate(90deg) translateY(-100vw) translateZ(100px);
transform-origin: 0 0;
transition: transform 0.2s;
height: 100vw !important;
width: 100vh !important;
max-width: 100vh !important;
.vjs-fullscreen-btn {
margin-right: 1.25rem;
}
}
</style>
解析:
// 富文本样式css
import 'quill/dist/quill.core.css';
import 'quill/dist/quill.snow.css';
import 'quill/dist/quill.bubble.css';
//videojs
import videoJs from 'video.js';
import 'video.js/dist/video-js.min.css';
//循环播放 和 全屏播放 按钮组件
import LoopBtn from '@mobile/components/player/loopBtn';
import FullscreenBtn from '@mobile/components/player/fullscreenBtn';
离开页面,重新进入报错
解决 VIDEOJS: WARN: Player “media-player-0” is already initialised. Options will not be applied.
onBeforeUnmount(() => {
if (myPlayerList.value) {
myPlayerList.value.forEach((ele) => {
ele.dispose();
});
}
});
通过videoJs注册组件
videoJs.registerComponent(‘FullscreenBtn’, FullscreenBtn);
videoJs.registerComponent(‘LoopBtn’, LoopBtn);
附带:
循环播放组件
//@ts-ignore
import videoJs, { VideoJsPlayer } from 'video.js';
import unLoop from '@mobile/assets/icon/unLoop.svg';
import loop from '@mobile/assets/icon/loop.svg';
// 这里直接获取的是Button组件,因为需要的其实就是控制条的一个button
const VideoButton = videoJs.getComponent('Button');
//@ts-ignore
class loopBtn extends VideoButton {
player: any;
loop: boolean = false;
icon: HTMLImageElement;
constructor(player: any, options: Object) {
super(player, options);
//@ts-ignore
this.addClass('vjs-control');
//@ts-ignore
this.addClass('vjs-button');
//@ts-ignore
this.addClass('vjs-loop-btn');
this.player = player;
}
// 自定义按钮的dom结构,createEl会在使用组件的时候自动调用
createEl(tag: string, props?: any, attributes?: any): HTMLButtonElement {
const nextBtnEl: HTMLButtonElement = document.createElement('button');
this.icon = document.createElement('img');
this.icon.src = unLoop;
// 将按钮图标放到按钮中
videoJs.dom.appendContent(nextBtnEl, this.icon);
return nextBtnEl;
}
handleClick(): void {
this.loop = !this.loop;
if (this.loop) {
//@ts-ignore
this.icon.src = loop;
this.player.loop(true);
} else {
//@ts-ignore
this.icon.src = unLoop;
this.player.loop(false);
}
}
}
export default loopBtn;
全屏播放组件
//@ts-ignore
import videoJs, { VideoJsPlayer } from 'video.js';
import unFullscreen from '@mobile/assets/icon/unFullscreen.svg';
import fullscreen from '@mobile/assets/icon/fullscreen.svg';
// 这里直接获取的是Button组件
const VideoButton = videoJs.getComponent('Button');
//@ts-ignore
class FullscreenBtn extends VideoButton {
player: any;
isFullscreen: boolean = false;
icon: HTMLImageElement;
that: any;
video: any;
constructor(player: any, options: Object) {
super(player, options);
//@ts-ignore
this.addClass('vjs-control');
//@ts-ignore
this.addClass('vjs-button');
//@ts-ignore
this.addClass('vjs-fullscreen-btn');
this.player = player;
this.video = this.player.el_;
this.that = this;
}
// 自定义按钮的dom结构,createEl会在使用组件的时候自动调用
createEl(tag: string, props?: any, attributes?: any): HTMLButtonElement {
const nextBtnEl: HTMLButtonElement = document.createElement('button');
this.icon = document.createElement('img');
this.icon.src = fullscreen;
// 将按钮图标放到按钮中
videoJs.dom.appendContent(nextBtnEl, this.icon);
return nextBtnEl;
}
handleClick(): void {
this.isFullscreen = !this.isFullscreen;
if (this.isFullscreen) {
//@ts-ignore
this.icon.src = unFullscreen;
// if (this.video.parentNode.requestFullscreen) {
// this.video.parentNode.requestFullscreen();
// }
// if (document.documentElement.requestFullscreen) {
// document.documentElement.requestFullscreen();
// }
this.video.classList.add('vjs-fullscreen-enter');
} else {
//@ts-ignore
this.icon.src = fullscreen;
// if (document.fullscreenElement) {
// document.exitFullscreen().then();
// }
this.video.classList.remove('vjs-fullscreen-enter');
}
}
}
export default FullscreenBtn;
总结:
难点1: 原生video全屏无水印,自定义video时水印放于同级父下,ios视频全屏无水印使用伪全屏适配。
难点2: 解决v-html富文本中包含多个视频时,替换掉原生video标签内容,动态渲染内容。
难点3: 离开页面重新进入,解决报错xxx is already initialised问题。
难点4: 多视频播放问题,在【初始化播放器】时let $player;重新赋值。
文章来源:https://blog.csdn.net/weixin_43849543/article/details/134968720
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!