文章目录
主题思路
1.封装视频播放组件。
2.封装视频上传组件。
3.如何使用。
一、封装视频播放组件。
在components公共组件里新建Videoback文件夹创建index.vue。
代码如下(示例):
<template>
<el-dialog
v-if="dialogVisible"
title="预览视频"
:close-on-click-modal="false"
:visible.sync="dialogVisible"
:width="width + 40 + 'px'"
>
<div
class="m-video"
:class="{ 'u-video-hover': !hidden }"
:style="`width: ${veoWidth}; height: ${veoHeight};`"
>
<video
ref="veoRef"
class="u-video"
:style="`object-fit: ${zoom};`"
:src="src"
:poster="veoPoster"
:autoplay="autoplay"
:controls="!originPlay && controls"
:loop="loop"
:muted="autoplay || muted"
:preload="preload"
crossorigin="anonymous"
@loadeddata="poster ? () => false : getPoster()"
@loadedmetadata="onVideoLoadedMetadata"
@timeupdate="onTimeUpdate"
@pause="showPlay ? onPause() : () => false"
@playing="showPlay ? onPlaying() : () => false"
@click.prevent.once="onPlay"
v-bind="$attrs"
>
您的浏览器不支持video标签。
</video>
<svg
v-show="originPlay || showPlay"
class="u-play"
:class="{ hidden: hidden }"
:style="`width: ${playWidth}px; height: ${playWidth}px;`"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M4.75 6.75C4.75 5.64543 5.64543 4.75 6.75 4.75H17.25C18.3546 4.75 19.25 5.64543 19.25 6.75V17.25C19.25 18.3546 18.3546 19.25 17.25 19.25H6.75C5.64543 19.25 4.75 18.3546 4.75 17.25V6.75Z"
></path>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M15.25 12L9.75 8.75V15.25L15.25 12Z"
></path>
</svg>
</div>
</el-dialog>
</template>
<script>
export default {
name: "Video",
props: {
src: {
// 视频文件url,必传,支持网络地址 https 和相对地址 require('@/assets/files/Bao.mp4')
type: String,
required: true,
default: ""
},
poster: {
// 视频封面url,支持网络地址 https 和相对地址 require('@/assets/images/Bao.jpg')
type: String,
default: ""
},
second: {
// 在未设置封面时,自动截取视频第 second 秒对应帧作为视频封面
type: Number,
default: 0.5
},
width: {
// 视频播放器宽度,单位 px
type: [String, Number],
default: 800
},
height: {
// 视频播放器高度,单位 px
type: [String, Number],
default: 450
},
/*
参考 MDN 自动播放指南:https://developer.mozilla.org/zh-CN/docs/Web/Media/Autoplay_guide
Autoplay 功能
据新政策,媒体内容将在满足以下至少一个的条件下自动播放:
1.音频被静音或其音量设置为 0
2.用户和网页已有交互行为(包括点击、触摸、按下某个键等等)
3.网站已被列入白名单;如果浏览器确定用户经常与媒体互动,这可能会自动发生,也可能通过首选项或其他用户界面功能手动发生
4.自动播放策略应用到<iframe>或者其文档上
autoplay:由于目前在最新版的Chrome浏览器(以及所有以Chromium为内核的浏览器)中,
已不再允许自动播放音频和视频。就算你为video或audio标签设置了autoplay属性也一样不能自动播放!
解决方法:设置视频 autoplay 时,视频必须设置为静音 muted: true 即可实现自动播放,
然后用户可以使用控制栏开启声音,类似某宝商品自动播放的宣传视频逻辑
*/
autoplay: {
// 视频就绪后是否马上播放,优先级高于 preload
type: Boolean,
default: false
},
controls: {
// 是否向用户显示控件,比如进度条,全屏等
type: Boolean,
default: true
},
loop: {
// 视频播放完成后,是否循环播放
type: Boolean,
default: false
},
muted: {
// 是否静音
type: Boolean,
default: false
},
preload: {
// 是否在页面加载后载入视频,如果设置了autoplay属性,则preload将被忽略;
type: String,
default: "metadata" // auto:一旦页面加载,则开始加载视频; metadata:当页面加载后仅加载视频的元数据 none:页面加载后不应加载视频
},
showPlay: {
// 播放暂停时是否显示播放器中间的暂停图标
type: Boolean,
default: true
},
playWidth: {
// 中间播放暂停按钮的边长
type: Number,
default: 96
},
zoom: {
// video的poster默认图片和视频内容缩放规则
type: String,
default: "contain" // none:(默认)保存原有内容,不进行缩放; fill:不保持原有比例,内容拉伸填充整个内容容器; contain:保存原有比例,内容以包含方式缩放; cover:保存原有比例,内容以覆盖方式缩放
}
},
data() {
return {
dialogVisible: false,
veoPoster: this.poster,
originPlay: true,
hidden: false
};
},
computed: {
veoWidth() {
if (typeof this.width === "number") {
return this.width + "px";
}
return this.width;
},
veoHeight() {
if (typeof this.height === "number") {
return this.height + "px";
}
return this.height;
}
},
mounted() {
if (this.autoplay) {
this.hidden = true;
this.originPlay = false;
}
/*
自定义设置播放速度,经测试:
在vue2中需设置:this.$refs.veoRef.playbackRate = 2
在vue3中需设置:veo.value.defaultPlaybackRate = 2
*/
// this.$refs.veoRef.playbackRate = 2
},
methods: {
show() {
this.dialogVisible = true;
},
/*
loadedmetadata 事件在元数据(metadata)被加载完成后触发
loadeddata 事件在媒体当前播放位置的视频帧(通常是第一帧)加载完成后触发
若在移动/平板设备的浏览器设置中开启了流量节省(data-saver)模式,该事件则不会被触发。
preload 为 none 时不会触发
*/
getPoster() {
// 在未设置封面时,自动获取视频0.5s对应帧作为视频封面
// 由于不少视频第一帧为黑屏,故设置视频开始播放时间为0.5s,即取该时刻帧作为封面图
this.$refs.veoRef.currentTime = this.second;
// 创建canvas元素
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
// canvas画图
canvas.width = this.$refs.veoRef.videoWidth;
canvas.height = this.$refs.veoRef.videoHeight;
ctx.drawImage(this.$refs.veoRef, 0, 0, canvas.width, canvas.height);
// 把canvas转成base64编码格式
this.veoPoster = canvas.toDataURL("image/png");
},
onPlay() {
if (this.originPlay) {
this.$refs.veoRef.currentTime = 0;
this.originPlay = false;
}
if (this.autoplay) {
this.$refs.veoRef.pause();
} else {
this.hidden = true;
this.$refs.veoRef.play();
}
},
onPause() {
this.hidden = false;
// console.log("视频暂停播放");
},
onPlaying() {
this.hidden = true;
},
/** 获取视频长度 */
onVideoLoadedMetadata() {
const duration = this.$refs.veoRef.duration;
// console.log(`视频长度: ${duration} 秒`);
},
// 视频播放进度更新时的处理函数
onTimeUpdate(event) {
const playedSeconds = event.target.currentTime;
// console.log(`已播放: ${playedSeconds} 秒`);
}
}
};
</script>
<style lang="scss" scoped>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.m-video {
display: inline-block;
position: relative;
background: #000;
cursor: pointer;
.u-video {
display: inline-block;
width: 100%;
height: 100%;
vertical-align: bottom;
}
.u-play {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
fill: none;
color: #fff;
pointer-events: none;
opacity: 0.7;
transition: opacity 0.3s;
path {
stroke: #fff;
}
}
.hidden {
opacity: 0;
}
}
.u-video-hover {
&:hover {
.u-play {
opacity: 0.9;
}
}
}
</style>
二、封装视频上传组件。
1.配置oss,在utils文件夹中创建oss.js
代码如下(示例):
let OSS = require("ali-oss");
let client = new OSS({
region: "oss-cn-hangzhou", // 填写Bucket所在地域。以华东1(杭州)为例,Region填写为oss-cn-hangzhou。
accessKeyId: "",
accessKeySecret: "",
authorizationV4: true,
bucket: "", // 填写Bucket名称,如“baidupro-oss”
});
let cdnUrl = ""; // 文件上传后的回调地址
export { client, cdnUrl };
2.在components公共组件里新建Updata文件夹创建Upvideo.vue。
代码如下(示例):
<template>
<div class="upda fx">
<!-- 视频可拖拽 -->
<div class="col" v-if="videoUrled.length != 0" style="margin-right: 10px">
<div>
<draggable
:disabled="disabled"
v-model="videoUrled"
filter=".forbid"
animation="300"
@update="onMove"
>
<transition-group>
<div
v-for="(item, index) in videoUrled"
:key="index"
style="display: inline-block; margin-right: 10px"
>
<div class="img-hover img_div">
<img
:src="nourl ? item : item.veoPoster"
alt="加载中..."
style="width: 100px; height: 100px"
class="imgCSSS"
/>
<div class="mask">
<h3 style="line-height: 50px">
<span
style="margin-right: 10px"
v-show="!disabled"
class="el-icon-delete click_fly"
@click="handleRemove(index)"
></span>
<span
v-show="!preopen"
class="el-icon-video-play click_fly"
@click="handlePreview(index)"
></span>
</h3>
</div>
</div>
</div>
</transition-group>
</draggable>
</div>
<div style="margin-top: -15px">{{ UpvideItem.name }}</div>
</div>
<el-upload
ref="upload"
accept="mp4/*"
:before-upload="handleBeforeUpload"
v-if="videoUrled.length != vidnumber && !loading"
:disabled="disabled"
action=""
:http-request="beforeUpload"
list-type="picture-card"
name="file"
:auto-upload="true"
:show-file-list="false"
:on-progress="handleExceed"
:on-success="
(response, file) => {
onSuccess(response, file);
}
"
>
<i slot="default" class="el-icon-plus" />
</el-upload>
<div class="loadCss" v-if="loading">
<el-progress
type="circle"
:percentage="progresNum"
:width="99"
></el-progress>
<div class="absdiv" @click="OnisCancel">取消上传</div>
</div>
<!-- 用于视频回显时canvas封面 -->
<div v-show="false">
<video
v-if="UpvideItem.url != ''"
ref="veoRef"
class="u-video"
:src="UpvideItem.url + '#t=1'"
:autoplay="false"
:muted="true"
preload="metadata"
crossorigin="anonymous"
@loadeddata="getPoster()"
v-bind="$attrs"
>
您的浏览器不支持video标签。
</video>
</div>
<!-- 第一步的视频播放组件 -->
<Videoback ref="videoer" :src="UpvideItem.url" :second="0" />
</div>
</template>
<script>
import { client, cdnUrl } from "@/utils/oss.js";
import { wholeUrl } from "@/api/phpUrl"; //引入接口
import draggable from "vuedraggable"
import { mapGetters } from "vuex";
import Videoback from "@/components/Videoback/index.vue";
export default {
name: "UpvideoUrl",
components: {
Videoback, //视频播放,
draggable,
},
props: {
// 最多上传视频数量
vidnumber: {
type: Number,
default: 1,
},
//是否禁用
disabled: {
type: Boolean,
default: null,
},
//是否可预览
preopen: {
type: Boolean,
default: null,
},
//没有key
nourl: {
type: Boolean,
default: false,
},
//视频回显列表
videoUrl: {
type: Array,
default() {
return [];
},
},
//限制上传视频大小
videoSize: {
type: Number,
default: 200,
},
},
computed: {},
data() {
return {
loading: false,
Actions: wholeUrl + "/basis/videoUpload",
phpUrl: window.localStorage.getItem("phpBaseUrl"),
//上传视频返回数据
UpvideItem: {
videoSize: "", //视频大小
name: "", //视频名称
url: "", //视频地址
videoTime: "", //视频的时长
videoWidth: "", //视频的宽
videoHeight: "", //视频的高
veoPoster: "", //视频展示图
},
// 视频放大显示
dialogImageUrl: "",
videoUrled: this.videoUrl,
isCancel: false, //取消上传,
progresNum: 0, //上传进度
};
},
watch: {
videoUrl: {
handler(val) {
this.videoUrled = val;
if (val.length > 0) {
console.log(val, "val");
Object.assign(this.UpvideItem, val[0]);
}
},
deep: true,
},
},
methods: {
handleExceed(file) {
if (file.loaded > 1000000 * this.videoSize) {
this.$refs.upload.abort();
}
},
// 视频上传回显
async onSuccess(response, file) {
await Object.assign(this.UpvideItem, this.$options.data().UpvideItem);
if (!response) return;
if (response.code != 200) return this.$message.error(response.message);
if (this.nourl) {
this.videoUrl.push(response.data.full_url);
} else {
this.videoUrl.push({ url: response.data.full_url });
}
this.$emit("changVideo");
this.UpvideItem.url = response.data.full_url;
this.UpvideItem.name = file.name;
this.UpvideItem.videoSize = (file.size / (1024 * 1024)).toFixed(2); // 将字节转换为MB;
},
/** 用canvas获取视频的封面 */
getPoster() {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
// canvas画图
canvas.width = this.$refs.veoRef.videoWidth;
canvas.height = this.$refs.veoRef.videoHeight;
ctx.drawImage(this.$refs.veoRef, 0, 0, canvas.width, canvas.height);
// 把canvas转成base64编码格式
this.UpvideItem.veoPoster = canvas.toDataURL("image/png");
this.UpvideItem.videoTime = this.$refs.veoRef.duration; // 获取视频的时长
this.UpvideItem.videoWidth = canvas.width; //获取视频的宽
this.UpvideItem.videoHeight = canvas.height; //获取视频的高
this.videoUrl.splice(0, 1);
this.videoUrl.push(this.UpvideItem);
console.log(this.UpvideItem, "视频的资料");
},
//拖拽视频
onMove() {
this.$emit("Upimgsun", this.videoUrled);
},
//删除视频
async handleRemove(index) {
this.videoUrl.splice(index, 1);
await Object.assign(this.UpvideItem, this.$options.data().UpvideItem);
this.$emit("changeimge");
},
//预览视频
async handlePreview(index) {
await this.$nextTick(() => {
this.$refs.videoer.show();
});
},
// 判断上传的是否为视频
handleBeforeUpload(file) {
if (file.size > 1000000 * this.videoSize) {
this.$message.error(`上传视频最大不能超过${this.videoSize}MB`);
return false;
}
var img = file.name.substring(file.name.lastIndexOf(".") + 1);
const suffix = img.toLowerCase() === "mp4";
const suffix2 = img.toLowerCase() === "mov";
if (!suffix && !suffix2) {
this.$message.error("上传视频文件格式支持尺寸为.mp4、.mov");
return false;
}
},
// 上传
async beforeUpload(file) {
this.secureUpload(file);
},
async secureUpload(file) {
this.loading = true;
this.isCancel = false;
//判断扩展名
const tmpcnt = file.file.name.lastIndexOf(".");
const exname = file.file.name.substring(tmpcnt + 1);
// 配置路径以及文件名称
const fileName = "files/" + file.file.uid + "." + exname; //'files/'为oss上传路径
const OSS_DOMAIN = "https://sphuix.com/"; //视频回显地址
try {
const result = await client.multipartUpload(fileName, file.file, {
progress: (p) => {
console.log(`进度: ${(p * 100).toFixed(2)}%`);
this.progresNum = Number((p * 100).toFixed(2));
if (this.isCancel) {
console.log("取消上传");
client.cancel();
}
},
partSize: 1024 * 1024, // 1MB分片
meta: { "x-oss-forbid-overwrite": "true" }, // 禁止覆盖
headers: { "Access-Control-Expose-Headers": "etag" }, // 显式声明
});
console.log("上传成功", result);
if (result.res.status == 200) {
this.$message.success("上传成功");
this.OnisCancel();
if (this.nourl) {
this.videoUrl.push(OSS_DOMAIN + fileName);
} else {
this.videoUrl.push({ url: OSS_DOMAIN + fileName });
}
this.$emit("changVideo");
this.UpvideItem.url = OSS_DOMAIN + fileName;
this.UpvideItem.name = file.file.name;
this.UpvideItem.videoSize = (file.file.size / (1024 * 1024)).toFixed(
2
); // 将字节转换为MB;
}
} catch (err) {
console.error("失败:", err);
}
},
/** 取消上传 */
OnisCancel() {
this.isCancel = true;
this.loading = false;
this.progresNum = 0;
},
},
};
</script>
<style lang="scss" scoped>
::v-deep .el-upload-list--picture-card .el-upload-list__item {
width: 100px;
height: 100px;
}
::v-deep .el-upload--picture-card {
width: 100px;
height: 100px;
line-height: 100px;
}
.mask {
z-index: 10;
position: absolute;
background: rgba(101, 101, 101, 0.6);
color: #ffffff;
opacity: 0;
top: 0;
left: 0;
width: 100px;
height: 100px;
border-radius: 10px;
// pointer-events: none;
}
.mask h3 {
text-align: center;
margin-top: 25%;
}
.img_div:hover .mask {
opacity: 1;
}
.img_div {
border-radius: 10px;
display: block;
position: relative;
}
.loadse {
width: 100px;
height: 100px;
border-radius: 10px;
border: 1px dashed #c0ccda;
}
.loadCss {
width: 100px;
height: 100px;
background-color: #fbfdff;
border: 1px dashed #c0ccda;
border-radius: 6px;
position: relative;
.absdiv {
position: absolute;
bottom: 15px;
width: 100px;
text-align: center;
cursor: pointer;
color: red;
}
}
::v-deep .el-progress--circle .el-progress__text {
top: 38%;
}
</style>
三、如何使用。
1.有视频需要回显,在Upvideo组件上定义 ref=“Upvideo”,然后在接受到视频链接后调用 this.$refs.Upvideo.getPoster()。
注意:我这里定义的入参是数组,videoUrl是数组。
<Upvideo
:videoUrl="videoUrl"
ref="Upvideo"
/>