1. 阿里云后缀参数
- 其实OSS本身提供了视频截帧功能,OSS提供的视频截帧功能和OSS图片服务功能使用的方式是类似的,都是通过传入x-oss-process参数的方式来实现的。最简单的使用方式就是直接在public的object的url后面拼接处理字符串。比如如下的url:http://a-image-demo.oss-cn-qingdao.aliyuncs.com/demo.mp4?x-oss-process=video/snapshot,t_10000,m_fast
- 注意
- 当前仅支持对视频编码格式为H264的视频文件进行视频截帧。
- OSS当前没有默认保存视频截帧的操作,视频截帧的图片需手动下载到本地。
- 如果需要对private的object调用视频截图,需要使用SDK进行鉴权,接口和OSS图片服务的调用接口一致。
- 图片处理除了能够处理已经存在的图片内容,还能够截取出视频中的指定位置产生图片,完成视频截帧。
参数 | 描述 | 取值范围 |
---|---|---|
t | 截图时间 | 单位ms,[0,视频时长] |
w | 截图宽度,如果指定为0则自动计算 | 像素值:[0,视频宽度] |
h | 截图高度,如果指定为0则自动计算,如果w和h都为0则输出为原视频宽高 | 像素值:[0,视频高度] |
m | 截图模式,不指定则为默认模式,根据时间精确截图,如果指定为fast则截取该时间点之前的最近的一个关键帧 | 枚举值:fast |
f | 输出图片格式 | 枚举值:jpg、png |
- 示例
- 找到视频5s处的内容,输出为宽为800像素,高自动计算的png格式图片http://a-image-demo.oss-cn-qingdao.aliyuncs.com/demo.mp4?x-oss-process=video/snapshot,t_5000,m_fast,w_800,f_png
详见文档:阿里云oss视频上传后,如何获取视频封面
- 找到视频5s处的内容,输出为宽为800像素,高自动计算的png格式图片http://a-image-demo.oss-cn-qingdao.aliyuncs.com/demo.mp4?x-oss-process=video/snapshot,t_5000,m_fast,w_800,f_png
2.本地文件截取预览图
获取本地视频文件封面,实现本地预览功能,支持指定秒数
- 首先将本地文件通过
URL.createObjectURL(file.raw)
得到视频url本地地址 - 动态创建一个video标签,并利用
onloadeddata
方法设置具体时间点,并添加监听事件addEventListener
- 然后利用canvas绘制成图片
- 输出base64
- 使用
GetVideoCover({
url: URL.createObjectURL(fileList[0].raw),
time: 2,
success: (res) => {
//封面地址res.base64
//this.videocover=res.base64
}
});
- 关键代码
定义GetVideoCover.js
// 压缩图片
function clipAndCompressCover({ media, currentWidth, currentHeight, success }) {
// eslint-disable-next-line no-unused-vars
const that = this
const canvas = document.createElement('canvas')
const area = canvas.getContext('2d')
const currentScale = currentWidth / currentHeight
const targetScale = 750 / 420
let targetWidth = 0
let targetHeight = 0
let clipWidth = 0
let clipHeight = 0
let quality = 0.95 // 不要用1,会额外增大base64大小。
// // 根据视频宽高,决定截图大小
// if (currentScale >= targetScale) {
// targetHeight = currentHeight > 420 ? 420 : currentHeight
// targetWidth = targetHeight * currentScale
// } else {
// targetWidth = currentWidth > 750 ? 750 : currentWidth
// targetHeight = targetWidth / currentScale
// }
// clipWidth = targetWidth > 750 ? 750 : targetWidth
// clipHeight = targetHeight > 420 ? 420 : currentHeight
canvas.width = currentWidth
canvas.height = currentHeight
area.drawImage(
media,
0, 0,
currentWidth,
currentHeight,
)
var handler = function() {
const base64 = canvas.toDataURL('image/jpeg', quality)
getMediaSize({ src: base64 }).then(response => {
success && success(base64)
})
}
handler()
}
// 绘制视频封面图片
const GetVideoCover = ({ url, time, success }) => {
const video1 = document.createElement('video')
video1.src = url
video1.style.cssText = 'position:fixed; top:0; left:-100%; visibility:hidden'
video1.onloadeddata = function() {
const currentTime = time // 截图时间点
video1.addEventListener('timeupdate', function() {
// console.log('video1', video1.videoWidth, video1.videoHeight)
clipAndCompressCover({
media: video1,
currentWidth: video1.videoWidth,
currentHeight: video1.videoHeight,
success: function(base64) {
video1.remove(video1)
success({
base64
})
}
})
})
video1.currentTime = currentTime < 0 ? 1 : currentTime
}
// edge浏览器必须要追加到dom中,才能顺利执行相关事件。
document.body.appendChild(video1)
}
// 获取媒体资源的大小,返回一个Promise对象。用于解决无法直接获取视频或图片的文件大小。
function getMediaSize({
src
}) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
// eslint-disable-next-line no-unused-vars
xhr.onreadystatechange = _ => {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
resolve({
size: xhr.response.size
})
} else {
reject()
}
}
}
xhr.open('get', URL.createObjectURL(transformBase64ToBlob(src)))
xhr.responseType = 'blob'
xhr.send()
})
}
function transformBase64ToBlob(base64) {
let byteString
if (base64.split(',')[0].indexOf('base64') >= 0) {
byteString = atob(base64.split(',')[1])
} else {
byteString = unescape(base64.split(',')[1])
}
const mimeString = base64.split(',')[0].split(':')[1].split(';')[0]
const ia = new Uint8Array(byteString.length)
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i)
}
const blob = new Blob([ia], {
type: mimeString
})
return blob
}
export {
GetVideoCover
}
3.网络视频截帧(不推荐)
- 从后台拿到视频url以及相关配置(宽,高,比例,时长),本地创建video的获取方式存在某些视频格式有问题或者帧率不对,导致无法拿到相关配置的问题,所以可以跟后台协商,将相关配置在拿视频时一起回传过来后,提前计算好帧率,创建好canvas数组容器。
- 视频添加监听函数,当视频开始播放时,就创建一个canvas将当前帧绘制出来,将canvas转化为图片地址,再推入准备好的canvas数组。
- 处理完后,添加键盘监听事件,在html中用
<canvas>
标签遍历上一步处理好的canvas数组,直接展示绘制好的canvas,这样用户可以通过键盘的上下左右箭头预览视频的每一帧的画面,也可以根据项目添加滚动条或者输入框做预览帧交互。 - 缺点很大,超慢,原视频分辨率越大,处理的越慢,一个8s的视频要处理40s,而且浏览器很容易崩。
- 关键代码
<video
:src="coverFileUrl"
ref="video"
id="video"
controls
muted
autoplay
style="width:300px;"
></video>
<!-- coverFileUrl -->
<canvas
v-show="editImg.index==n"
v-for="(i,n) in canvasList"
:key="n"
:id="'myCanvas'+n"
:width="videoCig.width"
:height="videoCig.height"
:style="{transform:`scale(${scale})`}"
@mousedown="cropMove($event,'start')"
@mouseup="cropMove($event,'end')"
></canvas>
//给视频添加监听函数
initVideoplayer() {
if (!this.videoPlayer && this.$refs.video) {
this.videoPlayer = this.$refs.video;
// 视频播放 -- 点击播放
this.videoPlayer.addEventListener("play", e => {
let { frame, duration } = this.videoCig;
this.timer = setInterval(() => {
this.no += 1;
if (this.no > (duration - 1) * frame) {
clearInterval(this.timer);
this.timer = null;
// this.listenKeyDown(); //视频帧全部截取完毕再添加
} else {
this.cutPicture(null, this.no);
}
}, 1000 / frame);
});
// 视频播放 -- 播放结束
this.videoPlayer.addEventListener("ended", e => {
console.log("视频播放结束");
});
}
}
//截取当前帧的图片
cutPicture(item, index) {
let { width, height } = this.videoCig;
var v = item
? document.getElementById("img" + index)
: document.querySelector("video");
let canvas = document.getElementById("myCanvas" + index);
var ctx = canvas.getContext("2d");
ctx.drawImage(v, 0, 0, width, height);
let copyImg = ctx.getImageData(0, 0, width, height);
var oGrayImg = canvas.toDataURL("image/jpeg");
this.canvasList.splice(index, 1, copyImg);
if (index == 0) {
//将首帧设为封面
this.video.cover = oGrayImg;
}
}
4.采用ffmpeg
插件截帧(推荐)
-
需引入ffmpeg插件,详见文章vue2+vite利用ffmpeg实现纯前端视频剪切
-
可指定开始帧率、输出张数、尺寸、格式
-
缺点是加载该插件需要一定时间
-
优点是相比第三种视频播放+canvas的方法要快很多,二十多秒的视频几秒钟就处理完了,也不用担心浏览器崩的问题,除非你的视频文件很长。
-
参考文档: FFmpeg基本命令使用 、FFmpeg官网、 FFmpeg——在Vue项目中使用FFmpeg(安装、配置、使用)
-
关键代码
// 上传视频后解析视频帧
async getVideoFrames() {
try {
showLoading();
let { name, file, duration, ccbl } = this.ffVideo;
ffmpeg.FS("writeFile", name, await FFmpeg.fetchFile(file));
// 计算每秒需要抽的帧数
let step = Math.ceil(10 / duration),
allNum = Math.floor(step * duration);
await ffmpeg.run(
"-i",
name,//视频文件名
"-r",
`${step}`,//每秒抽取帧数
"-ss",
"0",//从0帧开始
"-vframes",
`${allNum}`,//所需截取总张数
"-f",
"image2",//图片格式
"-s",
`88*${Math.ceil((88 * ccbl[1]) / ccbl[0])}`, //图片尺寸需要整数,此处按比例计算高
"image-%02d.png"//图片命名
);
// ffmpeg -i 2.mp4 -r 1 -ss 0 -vframes 5 -f image2 -s 352x240 image-%02d.jpeg
for (let i = 0; i < allNum; i++) {
// await ffmpeg.run('-i', 'source.mp4', '-y', '-f', '-ss', averageDura * i, '1', 'frame.png')
let temp = i + 1;
if (temp < 10) {
temp = "0" + temp;
}
this.frames.pics.push(
arrayBufferToBase64(ffmpeg.FS("readFile", "image-" + temp + ".png"))
);
}
hideLoading();
retutn this.frames.pics
} catch (err) {
console.error("getVideoFrames", err);
hideLoading();
// throw err;
}
}