利用 node.js + ffmpeg 制作视频转动图小程序,利用 ffmpeg 命令行实现,理论上可以ffmpeg所有功能。
本文的实现支持以下特性:
- 开始时间 / 结束时间
- 分辨率
- 大小限制
- 帧率
- 倍速
环境
安装依赖包
使用npm 安装所需的依赖包
npm install express multer
- Express 是基于 Node.js 平台,
快速、开放、极简的 Web 开发框架 - Multer 是一个 Node.js 中间件,用于处理 multipart/form-data ,本文中用于上传视频文件
搭建Https服务器
// index.js
const express = require('express');
const fs = require('fs');
const path = require('path');
const http = require('http');
const https = require('https');
//static 托管静态文件 用于客户端访问gif图片
app.use('/public',express.static(path.join(__dirname,'public')));
//引入 ffmpegRouter.js
const ffmpegRouter= require('./ffmpegRouter')
app.use('/ffmpeg',ffmpegRouter);
// Configuare https
const options = {
key : fs.readFileSync('[你的key文件路径]'),
cert: fs.readFileSync("[你的pem文件路径]"),
}
http.createServer(app).listen(80); // http端口 (非必要)
https.createServer(options, app).listen(443); // https 端口
目前小程序只能访问可信的地址,需要SSL证书,即只能访问 https 不能是 http,没有证书可以申请免费的证书。
在小程序后台开发设置里配置服务器域名在 request 和 uploadFile 合法域名填写上自己的服务器的域名(可以加端口号)
路由 ffmpegRouter.js
// ffmpegRouter.js
const express = require('express')
const router = express.Router()
const fs = require('fs')
const child = require('child_process')
// multer ...
// router.post('/videoToGif', upload.single('file'), (req, res) => { ... }
module.exports = router
- express :引入路由。
- fs 文件系统 :文件操作
- child_process 子进程 :用于调用 ffmpeg 命令
multer 上传文件
const multer = require('multer')
let storage = multer.diskStorage({
destination: function(req,file,cb){
// ./uploads 为保存的文件夹路径
cb(null,'./uploads');
},
filename: function(req,file,cb){
//文件名取时间戳
let tmpname = +new Date();
//添加随机数防止文件名重复
let random = parseInt(Math.random() * 10000);
//获取文件类型
let type = file.originalname.split('.').pop();
cb(null,`${tmpname}${random}.${type}`);
}
});
let upload = multer({ storage })
主要代码
body 数据格式 和 Option 设置下文有说明
// upload 为 multer() 返回的值,‘flie’作为 form-data 的 key 值
router.post('/videoToGif', upload.single('file'), (req, res) => {
let file = req.file;
let { path, filename } = file;
let {
start, //开始时间
end, //结束时间
sizeLimit, //大小限制
dpi, //分辨率
framePerSecond, //每秒帧率
pts, //倍速
} = req.body;
// 类型检查
let type = file.originalname.split('.').pop(); // 文件名后缀
// 设置支持的格式
let allowTypes = ['avi', 'amv', 'dmv', 'mov', 'qt', 'flv', 'mpeg', 'mpg', 'm4v', 'm3u8', 'webm',
'mtv', 'dat', 'wmv', 'ram', '3gp', 'viv', 'mp4', 'rm', 'rmvb',];
if (!allowTypes.includes(type)) {
// 删除文件
fs.unlink(path, () => {
console.log(`文件类型不支持:${filename} `);
});
return res.send({ err: -2, msg: '文件类型不支持' });
}
// 命令行设置
const Option = {
list: ['-ss', '-to', '-i', '-fs', '-vf', '-s', '-r', '-y'],
init() {
this.list.forEach(x => this[x] = '')
},
add(name, value) {
this[name] += (this[name] ? ',' : '') + value;
},
get(name) {
return this[name] ? `${name} ${this[name]} ` : ''
},
toString() {
return this.list.reduce(((p,c) => p + this.get(c)),'')
}
}
Option.init() // 初始化设置项
// 时间
if (start && end){
if (Number(start) > Number(end)) {
return res.send({ err: -4, msg: '时间参数错误' })
}
Option.add('-ss',start)
Option.add('-to',end)
}
// 大小限制
if (sizeLimit && sizeLimit != '默认') {
Option.add('-fs', sizeLimit)
}
// 分辨率
if (dpi) {
if (dpi != '默认') {
if (dpi.endsWith('p')) {
Option.add('-vf', `scale=-2:${dpi.substr(0, dpi.length - 1)}`)
} else {
Option.add('-s',dpi)
}
}
}
// 帧率
if (framePerSecond && framePerSecond != '默认') {
Option.add('-r', framePerSecond);
}
// 倍速
if (pts && pts != '默认') {
pts = Number(pts)
pts = 1 / pts;
if (pts < 0.25) {
pts = 0.25
} else if (pts > 4) {
pts = 4
}
Option.add('-vf', `setpts=${pts}*PTS`)
}
// 输入目录
Option.add('-i', path);
// 输出目录
let rfilen = `public/picture/gif/${filename}.gif`
Option.add('-y', rfilen);
// 调用 toString 方法,导出配置项字符串
let optionStr = Option.toString()
//调用子进程的 exec 方法
child.exec(`ffmpeg ${optionStr}`, function (err) {
//处理完视频,删除上传的文件
fs.unlink(path, () => {
console.log('视频转GIF:' + filename);
//控制台打印 命令字符串 用于检查传递的命令是否格式正确
console.log('\033[37;44m' + optionStr + '\033[0m');
});
if (err) {
console.error(err)
res.send({ err: -1, msg: err })
} else {
//定时 3分钟后 删除生成的 gif文件
let limitTime = 3 * 60 * 1000
let expired = +new Date() + limitTime
setTimeout(() => {
fs.unlink(rfilen, () => {
console.log(`GIF文件:${filename} 已删除!`)
});
}, limitTime)
// stat 用于返回数据时返回文件大小
let stat = fs.statSync(rfilen)
res.send({
err: 0,
msg: '视频转gif处理成功,有效期3分钟!',
url: `https://【你的服务器地址】/${rfilen}`,
size: stat.size, // 文件大小
expiredIn: expired, // 过期时间 时间戳
});
}
})
})
注意:返回的 url 必须是 https 协议的,不然小程序界面调用 downloadFile 下载文件会失败。
数据格式
传 Falsy 或传 "默认"
表示不设置该项
名字 | 类型 | 说明 | 栗子 |
---|---|---|---|
start | Number | 开始时间 | 0 |
end | Number | 结束时间 | 10 |
sizeLimit | String | 大小限制 | 3M |
dpi | String | 分辨率 | 720p,640x480 |
framePerSecond | String | 帧率 | 30 |
pts | Number | 倍速,取值范围 [0.25,4] | 0.75,2.5 |
Option
const Option = {
list: ['-ss', '-to', '-i', '-fs', '-vf', '-s', '-r', '-y'],
init() {
this.list.forEach(x => this[x] = '')
},
add(name, value) {
this[name] += (this[name] ? ',' : '') + value;
},
get(name) {
return this[name] ? `${name} ${this[name]} ` : ''
},
toString() {
return this.list.reduce(((p,c) => p + this.get(c)),'')
}
}
}
Option.list
该字段的顺序就是导出字符串时的选项顺序
-ss :当用作输入选项时(在-i之前),在该输入文件中查找位置。(作为开始时间点)
-to :结束读取的时间点
-i :输入文件的地址
-fs : 设置文件大小限制,以字节表示。超过限制后不再写入字节块。输出文件的大小略大于请求的文件大小。
-vf :-filter:v的简称,创建滤波图并使用它来过滤流,本文用于修改倍速和分辨率
-s :设置帧大小,用于设置分辨率
-r :设置帧率
-y :输出文件地址,注意:重复名直接覆盖而不询问内容参考自:ffmpeg 文档
Option.init()
初始化设置,为 Option 添加 list 里的所有字段
Option.add(name, value)
为字段添加值,若不为空,则在前面添加 ","
来分隔
Option.get(name)
获取某个选项的值,把 key 和 value 拼接起来,自动在尾部添加空格,若没有数据则返回空字符串
Option.toString()
利用 Array.prototype.reduce()
方法,按照顺序返回所有字段字符串
打印结果
调用接口的输出小程序调用接口
wx.showLoading({
title: '努力转换中...',
})
wx.uploadFile({
// 调用 wx.chooseVideo 返回的 tempFilePath
filePath: this.data.tempFilePath,
formData: {
//开始时间,结束时间,大小限制、分辨率、帧率、倍速 前端自行发挥
start, end, sizeLimit, dpi, framePerSecond, pts,
},
name: 'file', // 对应后端 upload.single('file')
url: 'https://[你的服务器地址]/ffmpeg/videoToGif',
success: res => {
wx.hideLoading()
console.log(res)
if (res.statusCode === 200) {
let data = JSON.parse(res.data)
console.log(data)
if (data.err == 0) {
this.setData({
imgUrl: data.url,
// data.size 以字节为单位,转换为字符串
imgSize: this._changeByte(data.size),
// 过期时间
_expiredIn: data.expiredIn,
})
} else {
wx.showToast({
title: '服务器发生错误',
icon: 'none',
})
}
} else {
wx.showToast({
title: '发生错误',
icon: 'none',
})
}
}
})
演示
体验体验
小程序搜索 百万工具箱 或扫码体验
如果你觉得这篇文章对你有用,记得点个赞哦~
网友评论