美文网首页Node.js专题
利用nodejs+ffmpeg制作视频转动图小程序

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

作者: Jacob_Jiang | 来源:发表于2020-03-12 21:46 被阅读0次

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

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

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

    环境

    安装依赖包

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

    在小程序后台开发设置里配置服务器域名在 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',
          })
        }
      }
    })
    

    演示

    体验

    体验

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

    小程序码

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

    相关文章

      网友评论

        本文标题:利用nodejs+ffmpeg制作视频转动图小程序

        本文链接:https://www.haomeiwen.com/subject/jonyjhtx.html