实现原理是,使用node编写video访问的地址,使用ffmpeg转码视频流

使用的是electron-vue创建的项目,项目搭建就不做过多介绍了,项目搭建好后

注意:打包方式选择electron-packager

一、修改electron-vue对于使用electron-package打包的bug,修改ffmpeg.exe资源打包路径

 

修改ffmpeg.exe资源打包路径,一般的前端代码会被打包到asar文件中,我试了ffmpeg.exe放在里面会找不到

 

找到.electron-vue/build.js,electron-vue对于electron-package的打包配置有bug,需要手动修改下,部分源码如下

 

const Multispinner = require('Multispinner') // 引入缺少的包,npm install --save Multispinner

async function build () {
  greeting()

  del.sync(['dist/electron/*', '!.gitkeep'])
  // 和下面的tasks名字重复了,这里改下名字
  const tasks1 = ['main', 'renderer']
  const m = new Multispinner(tasks1, {
    preText: 'building',
    postText: 'process'
  })

  let results = ''

  const tasks = new Listr(
    [
      ... // 这里代码不变
    ],
    { concurrent: 2 }
  )

  await tasks
    .run()
    .then(async () => {
      process.stdout.write('\x1B[2J\x1B[0f')
      await bundleApp() // 添加这句话
      console.log(`\n\n${results}`)
      console.log(`${okayLog}take it away ${chalk.yellow('`electron-builder`')}\n`)
      process.exit()
    })
    .catch(err => {
      process.exit(1)
    })
}
// 这个函数修改比较多 function bundleApp () {
 // 打包配置,将ffmpeg.exe不放进asar文件内 const buildConfig = { mode: 'production', arch: 'x64', asar: true, dir: path.join(__dirname, '../'), icon: path.join(__dirname, '../build/icons/icon'), ignore: /(^\/(src|test|\.[a-z]+|README|yarn|static|dist\/web))|\.gitkeep|(node_modules\/ffmpeg-static-electron)/, // 不进行压缩的文件和目录 extraResource: 'node_modules/ffmpeg-static-electron/', // 不放进asar文件内部 out: path.join(__dirname, '../build'), overwrite: true, platform: process.env.BUILD_TARGET || 'all' } return new Promise((resolve, reject) => { packager(buildConfig, (err, appPaths) => { if (err) { console.log(`\n${errorLog}${chalk.yellow('`electron-packager`')} says...\n`) console.log(err + '\n') reject(err) } else { console.log(`\n${doneLog}\n`) resolve() } }) }) }

 

二、新建文件src/render/assets/js/server.js,用来写node接口

1.根据portfinder获取电脑可用端口,启动服务

const express = require('express')
const app = express()
const cors = require('cors')

const portfinder = require('portfinder')
import {setStore, configKeys, getStore} from '../js/electron-store' // 记录在本地的数据,方便全局使用
app.use(cors())
let videoPort
// http端口 // 获取可用端口,开启视频服务器 portfinder.getPort({ port: 1938 // 最小端口号 }, (err, port) => { if (err) { console.error(err) } else { videoPort = port setStore(configKeys.PORT, videoPort) app.listen(videoPort, () => console.log('服务启动')) } })

2.获取ffmpeg.exe使用路径,使用的获取路径的库,在打包后都会找不到,到最后只能自己手写了

下载ffmpeg-static-electron npm i --save ffmpeg-static-electron 

const os = require('os')
// 获取ffmpeg路径
function getFfmpegPath () {
if (process.env.NODE_ENV === 'development') {
return require('ffmpeg-static-electron').path
} else {
return
getFfmpegExe()
}
}

// 获取ffmpeg.exe使用路径
function getFfmpegExe () { var platform = os.platform() if (platform === 'darwin') { platform = 'mac' } else if (platform === 'win32') { platform = 'win' } const arch = os.arch() const curDir = process.cwd() // 获取当前所在位置 const basePath = path.resolve(curDir, '.\\resources\\ffmpeg-static-electron') const ffmpegPath = path.join( basePath, 'bin', platform, arch, platform === 'win' ? 'ffmpeg.exe' : 'ffmpeg' ) return ffmpegPath }

3.编写访问视频接口

下载fluent-ffmpeg, npm i --save-dev fluent-ffmpeg 

const ffmpegPath = getFfmpegPath()
const ffmpeg = require('fluent-ffmpeg')
ffmpeg.setFfmpegPath(ffmpegPath)

/**
 * 播放转码视频
 * sourceUrl 视频地址
 */
let ffmpegCommand = null
app.get('/video', function (req, res) {
  const pathSrc = req.query.sourceUrl
  if (ffmpegCommand !== null) {
    ffmpegCommand.kill()
    ffmpegCommand = null
  }
  ffmpegCommand = ffmpeg(pathSrc)
    .outputOptions([
      '-fflags',
      'nobuffer',
      '-preset',
      'superfast',
      '-rtsp_transport',
      'tcp',
      '-threads', // 多线程
      '2',
    '-ar', '22050', '-crf', '28' ]) .videoCodec('libx264') .audioBitrate(128) .inputFPS(25) .size('640x?') .aspect('4:3') .format('flv') // 只有flv成功了 .output(res) // .on('progress', function (progress) { // console.log('time: ' + progress.timemark) // }) .on('error', function (err) { console.log(err) console.log('An error occurred1: ' + err.message) }) .on('end', function () { console.log('Processiong finished!') }) .run() })

4.编写保存视频接口

/**
 * 保存视频
 * sourceUrl 视频地址
 * saveUrl 保存视频位置
 */
let ffmpegCommand1 = null
let time // 记录进行的时间
app.get('/saveVideo', function (req, res) {
  const id = req.query.id
  const pathSrc = req.query.sourceUrl
  let saveUrl = getFreeUrl(req.query.saveUrl, '.mp4', id)
  if (ffmpegCommand1 !== null) {
    ffmpegCommand1.kill()
    ffmpegCommand1 = null
  }
  let isLoad = false // 是否开始加载了
  ffmpegCommand1 = ffmpeg(pathSrc)
    .nativeFramerate()
    .seekInput(0)
    .videoCodec('copy')
    .audioCodec('copy')
    .addOutputOptions('-movflags +frag_keyframe+separate_moof+omit_tfhd_offset+empty_moov')
    .format('mp4')
    .save(saveUrl)
    .on('progress', function (progress) {
      time = progress.timemark
      console.log('time:' + progress.timemark)
      if (!isLoad) {
        isLoad = true
        res.status(200).send({ result: true }).end()
      }
    })
    .on('error', function (err) {
      console.log(err)
      console.log('An error occurred2: ' + err.message)
      res.status(500).send({ result: false })
    })
    .on('end', function () {
      console.log('Processiong finished!')
    })
  setTimeout(() => {
    if (!isLoad) {
      isLoad = true
      res.status(200).send({ result: true })
    }
  }, 5000)
})

/**
 * 获取可用的存储地址,文件名定义为数字
 * @param dir 存储位置
 * @param ext 文件后缀名
 */
function getFreeUrl (dir, ext, id) {
  const list = fs.readdirSync(dir)
  let targetList = [] // 该目录下指定后缀名的文件
  for (let i = 0; i < list.length; i++) {
    const stats = fs.statSync(dir + '\\' + list[i])
    if (stats.isFile() && list[i].indexOf(id) > -1) {
      // 文件后缀名
      const extname = path.extname(list[i])
      if (extname === ext) {
        targetList.push(list[i])
      }
    }
  }
  if (targetList.length === 0) {
    return dir + '\\' + id + '_1' + ext
  }
  targetList = targetList.sort((a, b) => {
    const name1 = a.substring(a.lastIndexOf('_') + 1, a.lastIndexOf('.'))
    const name2 = b.substring(b.lastIndexOf('_') + 1, b.lastIndexOf('.'))
    return name1 - name2
  })
  let lastFileName = targetList[targetList.length - 1]
  lastFileName = lastFileName.substring(lastFileName.lastIndexOf('_') + 1, lastFileName.lastIndexOf('.'))
  const newFileName = Number(lastFileName) ? Number(lastFileName) + 1 : 1
  return dir + '\\' + id + '_' + newFileName + ext
}

5.编写停止转码接口

在结束的时候,需要告诉ffmpeg停止操作,防止ffmpeg在后台一直执行损耗性能

// 结束转码操作,结束保存操作
app.get('/stopVideo', (req, res) => {
  console.log(ffmpegCommand1)
  try {
    if (ffmpegCommand1 !== null) {
      ffmpegCommand1.duration(time)
      ffmpegCommand1.ffmpegProc.stdin.write('q')
      ffmpegCommand1 = null
    }
    if (ffmpegCommand !== null) {
      ffmpegCommand.kill()
      ffmpegCommand = null
    }
    res.status(200).send({result: true})
  } catch (e) {
    console.error(e)
    res.status(500).send({result: false, e})
  }
})

三、引入server.js文件

在src/main/index.js里找到createWindow函数,引入server.js文件来启动node服务

四、使用接口

下载flv.js, npm i --save flv.js 

编写查看视频组件

 

<template>
  <div class="look-live" v-loading="loading">
    <video id="liveVideo" :src="src" controls autoplay @playing="play"></video>
  </div>
</template>

<script>
import flv from 'flv.js'
import {configKeys, getStore} from '../../assets/js/electron-store'
const fs = require('fs')
export default {
  name: 'look-live',
  props: {
    info: { // 视频流信息
      type: Object
    },
    saveUrl: { // 存储文件夹地址
      type: String
    }
  },
  data () {
    return {
      src: '', // 本地视频地址
      port: getStore(configKeys.PORT), // node后端地址
      loading: true // ffmpeg启动中
    }
  },
  mounted () {
    this.init()
  },
  // 结束录制需要把转码进程和保存进程关闭
  destroyed () {
    if (!this.localFile) {
      this.$axios.get(`http://localhost:${this.port}/stopVideo`)
    }
  },
  methods: {
    // 初始化
    init () {
     this.loadFly(this.info.url) if (this.saveUrl) { // 设置了存储地址就存视频 this.$axios.get(`http://localhost:${this.port}/saveVideo`, { params: { id: this.info.id, parentId: this.info.parentId, sourceUrl: this.info.url, saveUrl: this.saveUrl } }).catch(() => { this.$message.error('保存视频失败') }) } } }, // 初始化flv loadFly (url) { if (flv.isSupported()) { this.flvPlayer = flv.createPlayer({ type: 'flv', url: `http://localhost:${this.port}/video?sourceUrl=${url}` }) // 设置显示比例为16:9 const playerDom = document.getElementById('liveVideo') this.flvPlayer.attachMediaElement(playerDom) this.flvPlayer.load() this.flvPlayer.play() } }, // 播放中,取消加载样式 play () { this.loading = false } } } </script> <style scoped> video{ width: 800px; height: 550px; background-color: #333; } </style>

 

ffmpeg使用吐槽,在使用的时候转成不同的格式,音频和视频的配置方式都不一样,理论上应该是能实现边播放边存储,但是我没实现,只能写了两个接口分别实现了

ffmpeg使用转码参考https://blog.csdn.net/qq_29078329/article/details/124173770

 

posted on 2022-09-26 15:21  瑞诺拉  阅读(2777)  评论(0)    收藏  举报