浏览器版本请使用91,不要使用最新的,否则连测试都不行
不支持safari浏览器!!!视频文件在操作过程中使用blob地址
vue页面
对应方法
getVideo (event) {
if (this.player) {
this.player.dispose()
}
const file = event.target.files[0]
if (file.type !== 'video/mp4') {
alert('请选择MP4格式视频!')
return
}
this.options.sources[0].src = ''
const vm = this
console.info(event)
console.info(file)
if (window.FileReader) {
var reader = new FileReader()
reader.readAsDataURL(file)
// 监听文件读取结束后事件
reader.onloadend = function (e) {
vm.fileObj = e.target.result
vm.options.sources[0].src = e.target.result
vm.$nextTick(() => {
vm.initPlay()
})
// console.log(e.target.result) // e.target.result就是最后的路径地址
}
}
}
3、对应的ffmpeg编码加水印导出
import { createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg'
const ffmpeg = createFFmpeg({ log: true })
async test () {
if (!ffmpeg.isLoaded()) {
await ffmpeg.load()
}
ffmpeg.setProgress(({ ratio }) => {
console.log(ratio)
/*
* ratio is a float number between 0 to 1.
*/
})
ffmpeg.FS('writeFile', 'overlay.png', await fetchFile('./overlay.png'))
ffmpeg.FS('writeFile', 'a.mp4', await fetchFile(this.fileObj))
// ffmpeg.FS('writeFile', 'a.mp4', await fetchFile('./a.mp4'))
await ffmpeg.run(
'-i',
'a.mp4',
'-i',
'overlay.png',
'-filter_complex',
"[0:v][1:v] overlay=10:10:enable='between(t,0,20)'",
'b.mp4'
)
console.info(ffmpeg.FS('readFile', 'b.mp4'))
console.info(fs)
this.offerFileAsDownload('b.mp4', ffmpeg)
// await fs.promises.writeFile('./test.mp4', ffmpeg.FS('readFile', 'b.mp4'))
process.exit(0)
},
4、将上传处理好的文件上传至vod
async uploadCos (fileObj) {
const vm = this
try {
const result = await this.up({
files: [fileObj],
dirType: 'ClientUsr',
onprogress: (file) => {
vm.percent = file.percent ? parseInt(file.percent * 100) : 0
}
})
vm.newUrl = result.video.url
alert('上传文件成功!' + result.video.url)
} catch (err) {
alert('上传文件失败' + err)
}
},
async test () {
if (!ffmpeg.isLoaded()) {
await ffmpeg.load()
}
ffmpeg.setProgress(({ ratio }) => {
console.log(ratio)
/*
* ratio is a float number between 0 to 1.
*/
})
ffmpeg.FS('writeFile', 'overlay.png', await fetchFile('./overlay.png'))
ffmpeg.FS('writeFile', 'a.mp4', await fetchFile(this.fileObj))
// ffmpeg.FS('writeFile', 'a.mp4', await fetchFile('./a.mp4'))
await ffmpeg.run(
'-i',
'a.mp4',
'-i',
'overlay.png',
'-filter_complex',
"[0:v][1:v] overlay=10:10:enable='between(t,0,20)'",
'b.mp4'
)
console.info(ffmpeg.FS('readFile', 'b.mp4'))
// this.offerFileAsDownload('b.mp4', ffmpeg)
const files = new window.File(
[new Blob([ffmpeg.FS('readFile', 'b.mp4')])],
'b.mp4',
{ type: 'video/mp4' }
)
this.uploadCos(files)
// await fs.promises.writeFile('./test.mp4', ffmpeg.FS('readFile', 'b.mp4'))
process.exit(0)
},
5、将上传的视频解析成一帧一帧的作为视轨
ffmpeg -i 2.mp4 -r 1 -ss 0 -vframes 5 -f image2 -s 352x240 image-%02d.jpeg
vue代码如下
// 上传视频后解析视频帧
async getVideoFrames () {
this.videoInfo.waterFrames = []
if (!ffmpeg.isLoaded()) {
await ffmpeg.load()
}
// 计算10个对应的时间点
// const averageDura = this.videoInfo.baseInfo.duration / 10
ffmpeg.FS('writeFile', 'source.mp4', await fetchFile(this.fileObj))
// console.info(ffmpeg.FS('readFile', 'frame.png'))
await ffmpeg.run('-i', 'source.mp4', '-r', '1', '-ss', '0', '-vframes', '15', '-f', 'image2', '-s', '88*50', '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 < 15; 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.videoInfo.waterFrames.push(this.arrayBufferToBase64(ffmpeg.FS('readFile', 'image-' + temp + '.png')))
console.info(this.videoInfo.waterFrames)
}
},
用的需要将uint8array图片转base64显示图
arrayBufferToBase64 (array) {
array = new Uint8Array(array)
var length = array.byteLength
var table = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
'w', 'x', 'y', 'z', '0', '1', '2', '3',
'4', '5', '6', '7', '8', '9', '+', '/']
var base64Str = ''
for (var i = 0; length - i >= 3; i += 3) {
var num1 = array[i]
var num2 = array[i + 1]
var num3 = array[i + 2]
base64Str += table[num1 >>> 2] +
table[((num1 & 0b11) << 4) | (num2 >>> 4)] +
table[((num2 & 0b1111) << 2) | (num3 >>> 6)] +
table[num3 & 0b111111]
}
var lastByte = length - i
if (lastByte === 1) {
var lastNum1 = array[i]
base64Str += table[lastNum1 >>> 2] + table[((lastNum1 & 0b11) << 4)] + '=='
} else if (lastByte === 2) {
// eslint-disable-next-line no-redeclare
var lastNum1 = array[i]
var lastNum2 = array[i + 1]
base64Str += table[lastNum1 >>> 2] +
table[((lastNum1 & 0b11) << 4) | (lastNum2 >>> 4)] +
table[(lastNum2 & 0b1111) << 2] +
'='
}
return base64Str
},
使用ffmpeg从视频中截取图像帧
1.问题
从视频中抽取图像帧,并按照指定命名规则保存。
2. 环境
centos 6.3 + ffmpeg 0.6.5
3. 方法
1)安装ffmpeg
ffmpeg 位于rpmforge中,如果你的centos没有配置rpmforge,请先配置rpmforge。
yum -y install ffmpeg
并安装对应的依赖包。
2)使用场景
1. ffmpeg -i inputfile.avi -r 1 -f image2 image-%05d.jpeg
-r 指定抽取的帧率,即从视频中每秒钟抽取图片的数量。1代表每秒抽取一帧。
-f 指定保存图片使用的格式,可忽略。
image-%05d.jpeg,指定文件的输出名字。
2. ffmpeg -i inputfile.avi -r 1 -s 4cif -f image2 image-%05d.jpeg
4cif 代表帧的尺寸为705x576.其他可用尺寸如下。
3. ffmpeg -i inputfile.avi -r 1 -t 4 -f image2 image-%05d.jpeg
-t 代表持续时间,单位为秒。
4. ffmpeg -i inputfile.avi -r 1 -ss 01:30:14 -f image2 image-%05d.jpeg
-ss 指定起始时间
5.ffmpeg -i inputfile.avi -r 1 -ss 01:30:14 -vframes 120 4cif -f image2 image-%05d.jpeg
-vframes 指定抽取的帧数
踩坑:视频缩放计算分辨率报错"width / height not divisible by 2"
出现该错误的原因是在于:视频的宽度必须是2的倍数,高度必须是2的倍数
参考代码
var video = document.createElement("video");
var canvas = document.getElementById("prevImgCanvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
video.addEventListener('loadeddata', function() {
reloadRandomFrame();
}, false);
video.addEventListener('seeked', function() {
var context = canvas.getContext('2d');
context.drawImage(video, 0, 0, canvas.width, canvas.height);
}, false);
var playSelectedFile = function(event) {
var file = this.files[0];
var fileURL = URL.createObjectURL(file);
video.src = fileURL;
}
var input = document.querySelector('input');
input.addEventListener('change', playSelectedFile, false);
function reloadRandomFrame() {
if (!isNaN(video.duration)) {
var rand = Math.round(Math.random() * video.duration * 1000) + 1;
video.currentTime = rand / 1000;
}
}
写出的vue代码如下
async extractFramesFromVideo (videoUrl) {
const vm = this
var video = document.createElement('video')
var canvas = document.createElement('canvas')
canvas.width = 88
canvas.height = 50
video.addEventListener('loadeddata', function () {
for (let i = 0; i < 14; i++) {
const temp = parseInt(video.duration / 14)
setTimeout(() => {
if (!isNaN(video.duration)) {
// var rand = Math.round(Math.random() * video.duration * 1000) + 1
video.currentTime = temp * i === 0 ? 0.1 : temp * i
}
}, 500 * i)
}
}, false)
video.addEventListener('seeked', function () {
var context = canvas.getContext('2d')
context.drawImage(video, 0, 0, canvas.width, canvas.height)
// 转换成base64形式
const img = canvas.toDataURL('image/jpeg') // 这个就是图片的base64
vm.videoInfo.waterFrames.push(img)
}, false)
video.src = videoUrl
},
该项目差不多告一段落了,优化了解析帧,视频最终上传至vod,方法里面保存了下载到本地的方法
最终代码如下
VideoCut组件
{{videoInfo.baseInfo.name}}
下载快照
添加图片
下载音频
保存
简易剪辑视频
Trim or cut video
打开文件
本地视频
平台视频
通过视频URL
环境准备中...
{{showFullLoadingText}}
Step组件步进器
{{transTime(initval)}}
▲
▼
引入的common.css
/*loading样式*/
.loading{
width: 150px;
height: 15px;
margin: 0 auto;
margin-top:100px;
}
.loading span{
display: inline-block;
width: 15px;
height: 100%;
margin-right: 5px;
border-radius: 50%;
background: #FFFFFF;
-webkit-animation: load 1.04s ease infinite;
}
.loading span:last-child{
margin-right: 0px;
}
@-webkit-keyframes load{
0%{
opacity: 1;
}
100%{
opacity: 0;
}
}
.loading span:nth-child(1){
-webkit-animation-delay:0.13s;
}
.loading span:nth-child(2){
-webkit-animation-delay:0.26s;
}
.loading span:nth-child(3){
-webkit-animation-delay:0.39s;
}
.loading span:nth-child(4){
-webkit-animation-delay:0.52s;
}
.loading span:nth-child(5){
-webkit-animation-delay:0.65s;
}
/*主背景*/
.video-cut{
min-width: 1024px;
background: linear-gradient(180deg,#003d75,#232b56);
height: 100%;
min-height: 700px;
width: auto;
min-width: 1200px;
/*选择视频部分*/
.select-video-box{
margin-top: 15%;
text-align: center;
.title{
color: #FFFFFF;
font-weight: 700;
font-size: 48px;
margin: 0;
}
.tips{
font-weight: 400;
font-size: 18px;
color: #FFFFFF;
opacity: 0.8;
}
.select-type{
line-height: 60px;
width: 250px;
margin: 30px auto;
.select-drop{
background: #FFFFFF;
margin-top: 5px;
.select-item{
cursor: pointer;
&:hover{
background: #0088ff20;
}
}
}
.select-type-inner{
background: #FFFFFF;
.open-file{
width: 198px;
color: #003d75;
border-right: 1px solid #003d75;
cursor: pointer;
&:hover{
box-shadow: 0px 0px 10px #FFFFFF;
}
}
.arrow{
width: 50px;
text-align: center;
cursor: pointer;
&:hover{
box-shadow: 0px 0px 10px #FFFFFF;
}
i{
color: #003d75;
&:hover{
color: #003d7575;
}
}
}
}
}
}
}
/*主要内容部分*/
.content{
/*头部菜单部分*/
.block-top{
margin-bottom: 15px;
display: flex;
justify-content: space-between;
.toolbar{
display: flex;
.tool-item{
opacity: 0.8;
vertical-align: middle;
background-color: rgba(0,0,0,.1);
color: #FFFFFF;
font-size: 13px;
cursor: pointer;
line-height: 20px;
margin-right: 5px;
padding: 6px 15px;
&:hover{
opacity: 1;
}
&.active {
opacity: 1;
background: #012d56;
}
&:first-child{
border-radius: 10px 0px 0px 10px;
}
&:last-child{
border-radius: 0px 10px 10px 0px;
}
.i-name {
margin-left: 5px;
display: inline-block;
cursor: pointer;
}
i {
height: 20px;
}
}
}
.toolbar-right{
display: flex;
color: #FFFFFF;
font-size: 13px;
line-height: 20px;
.reset{
padding-right: 18px;
}
.tool-item{
opacity: 0.8;
cursor: pointer;
&:hover{
opacity: 1;
}
}
}
}
.video-content{
}
}
/*视频部分*/
.video_box{
.video-name{
font-size: 12px;
text-align: center;
color: hsla(0,0%,100%,.5);
margin-bottom: 15px;
}
.video-js{
margin: 0 auto;
}
}
/*视频播放区域可操作的部分*/
.dragable-area {
height: 360px;
width: 800px;
position: absolute;
top: 0px;
left: calc((100% - 800px)/2);
.clear-water {
position: absolute;
right: 0;
top: 0;
transform: translate(50%,-50%);
color: green;cursor: pointer;
&:hover {
color: darkblue;
}
}
}
/*展示时间部分*/
.show-time-box {
.show-time{
color: #00e0ff;
}
}
/*视轨部分*/
.track-wrap{
box-sizing: border-box;
width: 1020px;
overflow: visible;
margin: 43px auto;
.drag-frames {
width:1000px;
transform: translateX(10px);
position: absolute;
top: 0;
left: 0;
ul,li{
list-style: none;
display: block;
margin:0;
padding:0;
}
.box{
width:1000px;
height:50px;
position: relative;
/*overflow:hidden;*/
overflow: inherit;
}
.left{
width:0px;
height:100%;
background-color: rgba(32,57,109,.5);
float:left;
}
.resize{
transform:translateX(-10px);
width:10px;
height:100%;
cursor: w-resize;
float:left;
background-color: #00e0ff;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 0;
border-top-right-radius: 0;
background-position: center;
background-repeat: no-repeat;
background-size: 5px auto;
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 3 15' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1.5 3a1.5 1.5 0 100-3 1.5 1.5 0 000 3zM1.5 9a1.5 1.5 0 100-3 1.5 1.5 0 000 3zM1.5 15a1.5 1.5 0 100-3 1.5 1.5 0 000 3z' fill='%23fff' clip-rule='evenodd' fill-rule='evenodd'/%3E%3C/svg%3E");
&::before{
content: attr(data-before);
color: #00e0ff;
position: absolute;
bottom: 0;
transform: translate(-50%, 30px);
width: max-content;
}
}
.resize:before {
}
.resize2{
transform:translateX(10px);
position:absolute;
width:10px;
height:100%;
cursor: w-resize;
float:right;
right:0;
background-color: #00e0ff;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
border-bottom-right-radius: 4px;
border-top-right-radius: 4px;
background-repeat: no-repeat;
background-position: center;
background-size: 5px auto;
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 3 15' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1.5 3a1.5 1.5 0 100-3 1.5 1.5 0 000 3zM1.5 9a1.5 1.5 0 100-3 1.5 1.5 0 000 3zM1.5 15a1.5 1.5 0 100-3 1.5 1.5 0 000 3z' fill='%23fff' clip-rule='evenodd' fill-rule='evenodd'/%3E%3C/svg%3E");
&::after{
content: attr(data-after);
color: #00e0ff;
position: absolute;
bottom: 0;
transform: translate(-50%, 30px);
width: max-content;
}
}
.right{
float:left;
width:0px;
height:100%;
background-color: rgba(32,57,109,.5);
}
.mid{
min-width: 5px;
position:relative;
float:left;
width:100%;
height:100%;
background: transparent;
}
}
.components_video-navigation{
position: relative;
}
.component_storyboard {
padding: 0 10px;
.frames {
display: flex;
width: 100%;
overflow: hidden
}
.frame {
img {
width: 88px;
height: 50px;
}
}
.frame.loaded{
background-image: none
}
}
.component_storyboard .frames .frame{
background-color: rgba(0,0,0,.8);
background-image: url('data:image/svg+xml;utf8,');
background-size: 25%;
background-position: 50%;
background-repeat: no-repeat;
flex-shrink: 0;
flex-grow: 0;
pointer-events: none
}
}
// 底部的操作菜单
.bottom-nav {
color: #FFFFFF;
display: flex;
justify-content: space-between;
.bottom-nav-item{
&:hover{
background-color: rgba(0,0,0,.5);
}
padding: 0 15px;
cursor: pointer;
text-align: center;
line-height: 50px;
height: 50px;
border-radius: 15px;
box-sizing: border-box;
border: 1px solid transparent;
position: relative;
margin-right: 20px;
margin-top: 10px;
display: inline-block;
background-color: rgba(0,0,0,.25);
}
.play-btn {
width: 100px;
i{
font-size: 16px;
}
}
.range-slide {
width: 160px;
padding: 8px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
}
}
// 游标的样式
.youbiao{
position: absolute;
left: 0px;
}
.youbiao-time{
width: 70px;
position: absolute;
left: 0px;
color:#333333;
transform: translate(-50%,-25px);
font-size: 12px;
background: #FFFFFF;
border-radius: 10px;
padding:3px;
text-align: center;
}
// 设置
.setting-box {
position: absolute;
width:fit-content;
border-radius: 20px;
background-color: #000;
border: none;
padding: 20px;
font-size: 14px;
transform: translate(-50%, -110%);
div{
width: auto;
}
}
// 整页加载中的loading
.full-loading{
position: absolute;
top: 0;
left: 0px;
width: 100%;
height: 100%;
background: #33333380;
text-align: center;
}
{{videoInfo.baseInfo.name}}
下载快照
添加图片
下载音频
{{videoInfo.audio.name?'替换':'添加'}}背景音
(已添加:{{videoInfo.audio.name}})
保存
简易剪辑视频
Trim or cut video
打开文件
本地视频
平台视频
通过视频URL
环境准备中...
{{showFullLoadingText}}