近期做了一个需求,h5 页面,展示视频列表,点击视频,全屏播放。开始还以为全屏是不是需要读写 app 的标题栏,事实证明多想了,video 标签本身全屏的时候,可以让视频之外的地方全黑,ios 和 android 都一样。
但 ios 播放视频的时候,会自动将 video 放在最高层,然后全屏播放。
为了统一效果,播放视频的时候,统一用一个黑色遮罩层。
video,其实本身还是挺多事的,本文只是简单的播放,以后再涉及 video 的时候,心里有个数。video 的所有属性和事件可以参考MDN 的介绍。
如果是 PC 端的,推荐DPlayer。移动端,效果可能没那么好。
播放效果和简单逻辑
可能全屏效果比很多插件要好些,不然样式问题,也是闹心。
实现其实没啥大逻辑。
播放组件关键逻辑就是,播放的时候,先检测是否能播放,若不能则显示加载中。
播放组件的逻辑:
- 判断是否能播放,不能播放显示加载中
- 播放中出错的话,提示
- 播放结束,通知父组件
列表的逻辑:
- 展示列表
- 传递必要属性
- 接受子组件的事件
列表项组件项组件的逻辑:
- 属于展示组件,额外增加一个图片失败的监测
附注代码
videoPlayer 的代码
<template>
<div class="video-box" v-if="isShowPlayer">
<video
controls
:src="info.url"
:poster="info.cover"
ref="video"
@canplay="canPlay"
@error="error"
@ended="ended"
controlslist="nodownload"
disablePictureInPicture
></video>
<div class="close-box" @click="clickClose">
<img
src="https://blog-huahua.oss-cn-beijing.aliyuncs.com/blog/code/close.png"
alt="关闭"
/>
</div>
<div class="loadingBox" v-if="isLoading">
<img
class="loading-icon"
src="https://blog-huahua.oss-cn-beijing.aliyuncs.com/blog/code/loading.gif"
alt="正在加载..."
/>
</div>
</div>
</template>
<script>
export default {
props: {
info: { required: true, type: Object }, // url cover属性
isShowPlayer: { default: false }
},
data() {
return {
isCanPlay: false,
isLoading: false
};
},
mounted() {
if (this.isCanPlay) {
this.$refs.video.play();
return;
}
this.isLoading = true;
},
methods: {
canPlay() {
this.isCanPlay = true;
this.isLoading = false;
this.$refs.video.play();
},
error() {
alert("视频出错了,请联系客服人员");
this.showLoading = false;
},
ended() {
this.$emit("ended");
},
clickClose() {
this.$emit("update:isShowPlayer", false);
}
}
};
</script>
<style scoped>
.video-box {
position: fixed;
z-index: 9999;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: #000;
}
video {
width: 100%;
}
.close-box {
position: fixed;
z-index: 10000;
top: 0px;
right: 0;
padding: 20px;
}
.close-box img {
width: 24px;
}
.loadingBox {
position: fixed;
z-index: 9999;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: transparent;
}
.loading-icon {
width: 50px;
padding: 10px;
border-radius: 10px;
background-color: rgba(0, 0, 0);
}
</style>
列表 的代码
<template>
<div class="paper">
<div class="list">
<play-item
:info="item"
class="item"
@clickItem="clickItem(index)"
v-for="(item, index) in playList"
:key="index"
></play-item>
</div>
<videoPlayer
v-if="isShowPlayer"
:isShowPlayer.sync="isShowPlayer"
:info="curVideo"
@ended="ended"
></videoPlayer>
<div class="loadingBox" v-if="isLoading">
<img
class="loading-icon"
src="https://blog-huahua.oss-cn-beijing.aliyuncs.com/blog/code/loading.gif"
alt="正在加载..."
/>
</div>
</div>
</template>
<script>
import PlayItem from "./components/PlayItem";
import VideoPlayer from "./components/VideoPlayer";
export default {
components: { PlayItem, VideoPlayer },
data() {
return {
// 播放列表
playList: [],
// 是否正在加载
isLoading: true,
// 是否显示播放器
isShowPlayer: false,
// 当前播放视频的索引
curVideoIndex: null
};
},
computed: {
curVideo() {
return this.playList[this.curVideoIndex];
}
},
mounted() {
this.getPlayList();
},
methods: {
getPlayList() {
setTimeout(() => {
this.isLoading = false;
const list = [
{
title: "黑色毛衣",
url:
"https://blog-huahua.oss-cn-beijing.aliyuncs.com/blog/code/video.mp4"
},
{
title: "发如雪",
url:
"https://blog-huahua.oss-cn-beijing.aliyuncs.com/blog/code/video2.mp4"
},
{
title: "千里之外",
url:
"https://blog-huahua.oss-cn-beijing.aliyuncs.com/blog/code/video.mp4"
}
];
list.forEach(
item =>
(item.cover =
"https://blog-huahua.oss-cn-beijing.aliyuncs.com/blog/code/cover.png")
);
this.playList = list;
}, 20);
},
clickItem(index) {
this.curVideoIndex = index;
this.isShowPlayer = true;
},
ended() {
const isLast = this.curVideoIndex === this.playList.length - 1;
if (isLast) {
alert("这是最后一个视频了~~");
return;
}
this.curVideoIndex++;
}
}
};
</script>
<style scoped>
.list {
padding: 16px;
}
.item {
margin-bottom: 16px;
}
.loadingBox {
position: fixed;
z-index: 9999;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: transparent;
}
.loading-icon {
width: 50px;
padding: 10px;
border-radius: 10px;
background-color: rgba(0, 0, 0);
}
</style>
列表项组件 的代码
<template>
<div class="play-item-box" @click="clickItem">
<div class="left">
<img class="resource" :src="info.cover" @error="errorImg" alt="" />
<img
class="play-icon"
src="https://blog-huahua.oss-cn-beijing.aliyuncs.com/blog/code/play_icon.png"
alt=""
/>
</div>
<div class="right">
<div class="title">
{{ info.title }}
</div>
<div class="time">发送时间:2021-10-21 10:10:10</div>
</div>
</div>
</template>
<script>
export default {
name: "playItem",
props: { info: Object },
data() {
return {
defaultCover:
"https://blog-huahua.oss-cn-beijing.aliyuncs.com/blog/code/cover.png"
};
},
methods: {
clickItem() {
this.$emit("clickItem");
},
errorImg() {
this.info.coverImage = this.defaultCover;
}
}
};
</script>
<style scoped>
.play-item-box {
display: flex;
border-radius: 6px;
}
.left {
width: 33%;
position: relative;
height: 70px;
}
.left .resource {
display: block;
width: 100%;
border-radius: 6px;
height: 100%;
}
.play-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1;
width: 33px;
}
.right {
flex: 1;
margin-left: 10px;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 2px 0;
}
.title {
font-size: 16px;
font-weight: bold;
color: #333;
line-height: 1.3;
}
.time {
font-size: 12px;
color: #999;
}
.video {
position: absolute;
width: 100%;
height: 100%;
top: 0;
opacity: 0;
left: 0;
bottom: 0;
right: 0;
z-index: 11;
}
</style>