实现原理是,使用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
 
                     
                    
                 
                    
                 

 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号