Skip to main content

3 posts tagged with "Electron"

View All Tags

用 Electron 从 0 开发一个桌面软件(三)

· 4 min read
阿毛
人生得意须尽欢

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

这个我的个人网站

alt text

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

最近没什么空,公司让我改个JSP的项目,折腾了两天。前两天改的东西验收上线了, 现在来继续做做我的格式转换器。

前端的代码我都没有这么写,都是用deepseek 生成的,生成完之后我在改改。 今天做好了可以批量选择视频进行格式转换,可以设置存储路径。

  1. 这次主要遇到的有两个小问题
  • 本来以为在渲染进程通过 input 就直接能获取,但是试了之后发现其实有些信息书获取不到的, 比如帧数, 码率等。后面有采用和转码同样的方式,先传到主进程,生成文件,再通过 ffmpeg.ffprobe 来进行获取

function getVideoDetail(fileInfo: fileInfoDTO): Promise<VideoDetailDTO | null> {
return new Promise(async (resolve) => {
const url = await tempVideo(fileInfo)
fs.stat(url, (err, stats) => {
if (err) {
resolve(null)
return
}
const fileSizeInBytes = stats.size
const fileSizeInMB = (fileSizeInBytes / (1024 * 1024)).toFixed(2)

ffmpeg.ffprobe(url, (err, metadata) => {
if (err) {
console.error('获取视频元数据时出错:', err)
resolve(null)
return
}

// 获取视频流
const videoStream = metadata.streams.find((stream) => stream.codec_type === 'video')
// const audioStream = metadata.streams.find((stream) => stream.codec_type === 'audio')

if (videoStream) {
const frameRateArray = videoStream.r_frame_rate.split('/');
const frameRate = parseInt(frameRateArray[0], 10);
console.log(frameRate);

const obj: VideoDetailDTO = {
url: url,
name: fileInfo.name,
size: fileSizeInMB,
width: videoStream.width,
height: videoStream.height,
duration: metadata.format.duration,
codec_name: videoStream.codec_name,
r_frame_rate: frameRate,
bit_rate: Number((videoStream.bit_rate / 1000).toFixed(0)),
color_space: videoStream.color_space,
format_name: metadata.format.format_name
}
resolve(obj)
} else {
resolve(null)
}
})
})
})
}

  1. 文件存储路径的选择,我本来以为和 input 上传一样,选择一个文件夹即可, input 选择之后呢也获取不到真实的地址的, 后来搜了一下用了 electron 的 dialog 模块来选择目录

主进程

/**
* 选择 目录
* @param map
* @param event
* @param obj
*/
SELECT_FOLDER: (map: getMapFuns, event: Electron.IpcMainEvent): void => {
console.log('---')
const win = map?.getMainWin?.()
if (win) {
dialog.showOpenDialog(win, {
properties: ['openDirectory'] // 设置属性为 'openDirectory' 以仅打开目录选择器。
}).then(result => {
console.log(result)
if (!result.canceled) { // 检查用户是否点击了取消按钮。
event.reply('SELECT_FOLDER_RESP', result.filePaths[0])
} else {
event.reply('SELECT_FOLDER_RESP', null)
}
}).catch(err => {
console.error(err); // 处理可能的错误。
event.reply('SELECT_FOLDER_RESP', null)
});
}
}

下次更新先吧格式转换进度做出来

后面我想继续做的可能有几个功能

  • 视频和音频的格式转换
  • 视频的切片
  • 定时任务,每隔一段时间扫描指定文件夹, 如果有新的未转换格式的文件,就去转换后放到指定文件夹 (这个需求是我家的nas,下载的视频,有写格式播放不了,手动去转有点麻烦,做了这个功能后以后自动扫描文件,就不用手动去转格式了 )

用 Electron 从 0 开发一个桌面软件(二) ffmpeg 在 Electron 中的使用

· 6 min read
阿毛
人生得意须尽欢

大家好,我是一名,跨境行业 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 完美解决

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

视频转码demo

这是页面代码

页面很简单就, 一个 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
}


用 Electron 从 0 开发一个桌面软件(一)

· 2 min read
阿毛
人生得意须尽欢

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

这个我的个人网站

我是一名前端程序员,其实很久之前就知道 Electron, 也折腾过一段时间,主要是了解了一下主进程,渲染进程什么的, 但是呢一直没有做过什么东西,最近呢,在写博客,要不做一个 electron 的桌面软件? 正好博客的内容也有了。

做什么呢? 不能太简单也 不能太难。

想了一下,想到一件事儿:有一次我的车被剐蹭了,我的行车记录仪呢,是那种老的, 插U盘的,后来呢我就吧我的U盘拔下来 插到电脑上,想看一下当时的视频,结果呢电脑打开了,看到视频格式呢是 h.265 , 我以前呢都没有见过这种格式,我电脑也 打不开,结果就到网上去搜索格式转换器,结果一连下了好几个,开始下载的时候,标题都是说是免费的,真用的时候就让你冲 会员,把我给气的。 好,这次就做这个视频的格式转换器了。

好,确定了做什么,那今天就先把框架搭一搭

我决定用 electron-vite 来搭建项目, tailwindcss 来做样式

... OK 倒腾了一下,项目搭建完成,tailwindcss 也整合进去了,明天开始正式编码

alt text