大家好,我是一名,跨境行业 saas 软件开发的前端程序员,阿毛
这个我的个人网站

用 Electron 从 0 开发一个桌面软件,决定做一个视频转码软件,第二天。

在这里插入图片描述

今天做了一下调研,发现一个库ffmpeg ,无论是编码格式,视频拼接,裁剪,音频分离,混流,图片处理等等都很方便的进行处理,我用ffmpeg 做了一下测试,发现确实很好用

但是呢 ffmpeg 需要先进行安装, 我打包成桌面应用之后,万一用户没有安装,咋办,那我的功能不就用不了了吗?

问了一下AI,他给我两个方案

  • 一个是,在运行时检测是否安装ffmpeg,没有安装的话,就提示去安装后才能使用。
  • 另一个是把 ffmpeg的 执行文件一起打包到 Electron 中

我觉得第二个方案应该要好一点,但是怎么才能把执行文件一起打包呢,
找了一下发现了 ffmpeg-static , 里面放的就是可执行文件, 用 ffmpeg.setFfmpegPath 设置可执行文件路径 ,完美解决。

    const ffmpeg = require("fluent-ffmpeg");
    const ffmpegPath = require('ffmpeg-static');
    ffmpeg.setFfmpegPath(ffmpegPath);

OK 开始做吧。

但是做的时候呢又遇到一个问题,input 上传文件呢,是拿不到文件的真实地址的,而 ffmpeg 是需要真实地址的
后来又折中了一下,拿到file之后呢,传给主进程,主进程生成一个临时文件,在用这个临时文件去转码,OK 完美解决

下一步呢,我准备先美化一下页面,看看别的转码软件都有哪些功能,我再加点类似的功能

这是页面代码

页面很简单就, 一个 input 和 测试转码 的按钮
点击会把 input 选的文件传给主进程 进行转码,现在转码的输出文件是写死的

import { useState } from "react"

function App(): JSX.Element {
  const [file, setFile] = useState<any>(null)

  const [fileInfo, setFileInfo] = useState<any>(null)

  const handleFileChange = (event): void => {
    const _file = event.target.files[0];
    setFile(_file)
    const reader = new FileReader();
    reader.onload = (e): void => {
      const fileContent = e?.target?.result;
      // 这里可以将 fileContent 和文件名等信息一起传递给主进程
      const fileInfo = {
        name: _file.name,
        content: fileContent
      };
      setFileInfo(fileInfo)
    };
    reader.readAsArrayBuffer(_file)
  };

  const testFun = (): void => {
    window.electron.ipcRenderer.send('VIDEO_CONVERSION_API', {
      fileInfo: fileInfo,
      outputFilePath: '/Users/mao/code/electron/ElectronConvert/output.mp4'
    })
  }

  return (
    <>
      <div className="max-w-md mx-auto p-6 space-y-6">
        {/* 文件上传区域 */}
        <div className="space-y-4">
          <label className="block">
            <span className="sr-only">选择文件</span>
            <input
              type="file"
              onChange={handleFileChange}
              className="block w-full text-sm text-gray-500
                  file:mr-4 file:py-2 file:px-4
                  file:rounded-full file:border-0
                  file:text-sm file:font-semibold
                  file:bg-blue-50 file:text-blue-700
                  hover:file:bg-blue-100"
            />
          </label>

          {/* 操作按钮 */}
          <button
            onClick={testFun}
            className={`w-full py-3 px-6 rounded-lg font-medium transition-all
                  ${file ?
                'bg-blue-600 text-white hover:bg-blue-700' :
                'bg-gray-100 text-gray-400 cursor-not-allowed'}`
            }
          >
            测试转码
          </button>
        </div>

        {/* 文件信息 */}
        {file && (
          <div className="p-4 bg-gray-50 rounded-lg border border-gray-200">
            <p className="text-sm font-medium truncate">{file.name}</p>
            <p className="text-xs text-gray-500 mt-1">
              {Math.round(file.size / 1024)} KB · {file.type}
            </p>
          </div>
        )}
      </div>
    </>
  )
}

export default App



这是主进程中的主要代码

import ffmpeg from 'fluent-ffmpeg'
import ffmpegPath from 'ffmpeg-static'
ffmpeg.setFfmpegPath(ffmpegPath)
import fs from 'fs'
import path from 'path'
import { tmpdir } from 'os'

interface fileInfoDto {
  name: string
  content: any
}

// 将 timemark 字符串转换为秒
function timemarkToSeconds(timemark): number {
  const parts = timemark.split(':')
  const hours = parseInt(parts[0], 10)
  const minutes = parseInt(parts[1], 10)
  const seconds = parseFloat(parts[2])
  return hours * 3600 + minutes * 60 + seconds
}

// 生成文件返回地址
const tempVideo = (fileInfo: fileInfoDto): Promise<string> => {
  return new Promise((resolve) => {
    const { name, content } = fileInfo
    // 生成临时文件路径
    const tempFilePath = path.join(tmpdir(), name)
    // 将文件内容保存为临时文件
    fs.writeFile(tempFilePath, Buffer.from(content), (err) => {
      if (err) {
        console.error('保存临时文件出错:', err)
        resolve('')
        return
      }
      resolve(tempFilePath)
    })
  })
}

const VideoConversion = async (options: { fileInfo: fileInfoDto; outputFilePath: string }) => {
  console.log(options)
  const inputFilePath = await tempVideo(options.fileInfo)
  let totalFrames = 0
  if (inputFilePath) {
    // ffmpeg -i /var/folders/h7/zj0ytbjd62x1b7kp7f7t9_9c0000gn/T/录屏2025-03-09 10.53.04.mov -y /Users/mao/code/electron/ElectronConvert/o.mp4
    ffmpeg(inputFilePath)
      .output(options.outputFilePath)
      .on('start', (commandLine) => {
        console.log('开始转码: ' + commandLine)
      })
      .on('codecData', (data) => {
        totalFrames = timemarkToSeconds(data.duration)
        console.log('视频长度', totalFrames, '秒')
      })
      .on('progress', (progress) => {
        const timemark = progress.timemark
        // 将 timemark 转换为秒
        const currentTime = timemarkToSeconds(timemark)
        const progressPercentage = (currentTime / totalFrames) * 100
        console.log(`当前转码进度: ${progressPercentage.toFixed(2)}%`)
      })
      .on('end', () => {
        console.log('转码完成')
      })
      .on('error', (err) => {
        console.error('转码出错: ' + err.message)
      })
      .run()
  }
}

export default {
  VideoConversion,
  tempVideo
}


Logo

音视频技术社区,一个全球开发者共同探讨、分享、学习音视频技术的平台,加入我们,与全球开发者一起创造更加优秀的音视频产品!

更多推荐