Node.js专题

利用nodejs+ffmpeg制作视频转动图小程序

2020-03-12  本文已影响0人  Jacob_Jiang

利用 node.js + ffmpeg 制作视频转动图小程序,利用 ffmpeg 命令行实现,理论上可以ffmpeg所有功能

本文的实现支持以下特性:

  1. 开始时间 / 结束时间
  2. 分辨率
  3. 大小限制
  4. 帧率
  5. 倍速

环境

安装依赖包

使用npm 安装所需的依赖包

npm install express multer

搭建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,没有证书可以申请免费的证书。

在小程序后台开发设置里配置服务器域名在 requestuploadFile 合法域名填写上自己的服务器的域名(可以加端口号)

截图

路由 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',
      })
    }
  }
})

演示

体验

体验

小程序搜索 百万工具箱 或扫码体验

小程序码

如果你觉得这篇文章对你有用,记得点个赞哦~

上一篇 下一篇

猜你喜欢

热点阅读