Skip to content

Commit c90e5cd

Browse files
committed
feat(ffmpeg): implement dynamic FFmpeg download system with runtime management (#155)
* feat(ffmpeg): implement dynamic FFmpeg download system with runtime management - Remove static FFmpeg bundling from build configuration - Add FFmpegDownloadService for cross-platform binary management - Implement automatic platform detection (Windows/macOS/Linux, x64/ARM64) - Add IPC channels for download progress monitoring and control - Integrate download service with existing FFmpegService architecture - Update build scripts to remove prebuild FFmpeg requirements - Add comprehensive test coverage for download functionality Changes: - electron-builder.yml: Remove extraResources FFmpeg bundling - package.json: Remove prebuild FFmpeg download from release scripts - FFmpegDownloadService.ts: New service with download/extract/management capabilities - FFmpegService.ts: Enhanced with download service integration and fallback logic - IpcChannel.ts: Add 9 new channels for download operations - ipc.ts: Register download service handlers for renderer communication - preload/index.ts: Expose download APIs to renderer process - useVideoFileSelect.ts: Updated to work with dynamic FFmpeg detection This implementation enables on-demand FFmpeg installation, reducing app bundle size by ~200MB while maintaining cross-platform compatibility and user experience. The system gracefully falls back to bundled → downloaded → system FFmpeg. * feat(settings): implement FFmpeg settings UI with download management - Add new FFmpegSettings component with status indicator and download controls - Remove deprecated FFmpeg build plugin from electron.vite.config.ts - Enhance IndicatorLight component with proper CSS-in-JS animation syntax - Add comprehensive i18n support for FFmpeg management (en-us, zh-cn) - Remove box-shadow from ant-btn components for cleaner UI appearance - Integrate FFmpeg settings into main SettingsPage navigation Changes: - FFmpegSettings.tsx: Complete UI implementation with download progress, path validation, and status management - electron.vite.config.ts: Remove build-time FFmpeg download plugin (shift to runtime approach) - IndicatorLight.tsx: Fix styled-components animation with proper css helper - i18n locales: Add 61 new translation keys for FFmpeg settings UI - ant.scss: Remove button shadows for consistent design system - SettingsPage.tsx: Add FFmpeg settings tab integration This implements the frontend interface for the dynamic FFmpeg download system, providing users with a comprehensive management UI for FFmpeg installation, status monitoring, and path configuration. * feat(ffmpeg): implement FFmpeg guidance dialog for enhanced user experience Add comprehensive FFmpeg download guidance system that transforms technical errors into user-friendly guidance with seamless navigation to settings and auto-download. **Components Added:** - FFmpegDownloadPrompt: Full-featured guidance dialog with benefits, effort info, and actions - Comprehensive internationalization support (zh-CN, en-US) **Hook Enhancements:** - useVideoFileSelect: Extended with FFmpeg prompt state management - Replaced technical error throwing with guided dialog display - Enhanced error detection for FFmpeg missing scenarios **Integration Updates:** - HomePage: State management for prompt visibility and component integration - EmptyState/VideoAddButton: Bidirectional state communication with parent - HeaderNavbar: Props forwarding for prompt handler **Features:** - Benefits explanation (compatibility, performance, reliability) - Installation effort communication - Auto-navigation to settings with download trigger - Seamless integration with existing video file selection workflow - Graceful error handling with user-centric messaging **Technical Details:** - Styled-components with theme variables and CSS custom properties - Modal-based UI with responsive design and accessibility - State management across component hierarchy - URL parameter-based auto-download triggering - Comprehensive TypeScript interfaces Transforms "视频处理组件未安装" technical errors into guided user experience that educates users about FFmpeg benefits and provides immediate resolution path. * test: fix FFmpegService mock for dynamic download system - Add getDownloadService method to FFmpegService mock - Include all FFmpegDownloadService interface methods in mock - Fix 43 failing test cases caused by missing mock method - All 554 test cases now pass successfully Resolves test failures introduced by FFmpeg dynamic download system implementation. * test: fix cross-platform FFmpeg executable name test - Update test to handle platform-specific executable names (ffmpeg vs ffmpeg.exe) - Fix Windows CI test failure where test expected 'ffmpeg' but got 'ffmpeg.exe' - Test now correctly validates system FFmpeg fallback behavior on all platforms - Maintains test coverage while supporting cross-platform compatibility Resolves Windows CI test failure in FFmpegService integration tests.
1 parent e81430e commit c90e5cd

23 files changed

Lines changed: 2560 additions & 243 deletions

electron-builder.yml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,6 @@ files:
4646
asarUnpack:
4747
- resources/**
4848
- '**/*.{metal,exp,lib}'
49-
extraResources:
50-
- from: resources/ffmpeg
51-
to: ffmpeg
52-
filter:
53-
- '**/*'
5449
copyright: Copyright © 2025 EchoPlayer
5550
win:
5651
executableName: EchoPlayer

electron.vite.config.ts

Lines changed: 0 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -2,92 +2,17 @@ import fs from 'node:fs'
22
import path from 'node:path'
33

44
import react from '@vitejs/plugin-react-swc'
5-
import { spawn } from 'child_process'
65
import { CodeInspectorPlugin } from 'code-inspector-plugin'
76
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
87
import { resolve } from 'path'
98

109
const isDev = process.env.NODE_ENV === 'development'
1110
const isProd = process.env.NODE_ENV === 'production'
1211

13-
// FFmpeg 下载插件
14-
function ffmpegDownloadPlugin() {
15-
return {
16-
name: 'ffmpeg-download',
17-
async buildStart() {
18-
// 只在生产构建时下载 FFmpeg
19-
if (!isProd) return
20-
21-
// 根据构建目标决定下载哪个平台
22-
const targetPlatform = process.env.BUILD_TARGET_PLATFORM || process.platform
23-
const targetArch = process.env.BUILD_TARGET_ARCH || process.arch
24-
25-
// 检查是否已存在,避免重复下载
26-
const ffmpegPath = path.resolve(
27-
'resources/ffmpeg',
28-
`${targetPlatform}-${targetArch}`,
29-
targetPlatform === 'win32' ? 'ffmpeg.exe' : 'ffmpeg'
30-
)
31-
32-
if (fs.existsSync(ffmpegPath)) {
33-
console.log(`FFmpeg already exists for ${targetPlatform}-${targetArch}`)
34-
return
35-
}
36-
37-
console.log(`Downloading FFmpeg for ${targetPlatform}-${targetArch}...`)
38-
39-
try {
40-
await new Promise<void>((resolve, reject) => {
41-
// 在不同环境中使用不同的命令来确保兼容性
42-
let command: string
43-
let args: string[]
44-
45-
if (process.platform === 'win32') {
46-
// Windows 环境:使用 npm run 调用脚本,更可靠
47-
command = 'npm'
48-
args = ['run', 'ffmpeg:download']
49-
} else {
50-
// Unix 环境:直接使用 tsx
51-
command = 'tsx'
52-
args = ['scripts/download-ffmpeg.ts', 'platform', targetPlatform, targetArch]
53-
}
54-
55-
const downloadScript = spawn(command, args, {
56-
stdio: 'inherit',
57-
shell: process.platform === 'win32',
58-
env: {
59-
...process.env,
60-
BUILD_TARGET_PLATFORM: targetPlatform,
61-
BUILD_TARGET_ARCH: targetArch
62-
}
63-
})
64-
65-
downloadScript.on('close', (code) => {
66-
if (code === 0) {
67-
console.log('FFmpeg Downloaded successfully')
68-
resolve()
69-
} else {
70-
reject(new Error(`FFmpeg Download failed with exit code: ${code}`))
71-
}
72-
})
73-
74-
downloadScript.on('error', (error) => {
75-
reject(error)
76-
})
77-
})
78-
} catch (error) {
79-
console.error('FFmpeg Download failed:', error)
80-
throw new Error(`Failed to download FFmpeg for ${targetPlatform}-${targetArch}: ${error}`)
81-
}
82-
}
83-
}
84-
}
85-
8612
export default defineConfig({
8713
main: {
8814
plugins: [
8915
externalizeDepsPlugin(),
90-
ffmpegDownloadPlugin(),
9116
{
9217
name: 'copy-files',
9318
generateBundle() {
@@ -125,41 +50,6 @@ export default defineConfig({
12550
}
12651
}
12752
}
128-
129-
// 复制 FFmpeg 文件到构建目录
130-
const ffmpegResourcesDir = path.resolve('resources/ffmpeg')
131-
if (fs.existsSync(ffmpegResourcesDir)) {
132-
const outResourcesDir = path.resolve('out/resources/ffmpeg')
133-
134-
try {
135-
// 确保输出目录存在
136-
fs.mkdirSync(outResourcesDir, { recursive: true })
137-
138-
// 复制整个 ffmpeg 目录
139-
const copyDirectoryRecursive = (src: string, dest: string) => {
140-
if (!fs.existsSync(src)) return
141-
142-
fs.mkdirSync(dest, { recursive: true })
143-
const items = fs.readdirSync(src)
144-
145-
for (const item of items) {
146-
const srcPath = path.join(src, item)
147-
const destPath = path.join(dest, item)
148-
149-
if (fs.statSync(srcPath).isDirectory()) {
150-
copyDirectoryRecursive(srcPath, destPath)
151-
} else {
152-
fs.copyFileSync(srcPath, destPath)
153-
}
154-
}
155-
}
156-
157-
copyDirectoryRecursive(ffmpegResourcesDir, outResourcesDir)
158-
console.log('FFmpeg files copied successfully')
159-
} catch (error) {
160-
console.warn('Failed to copy FFmpeg files:', error)
161-
}
162-
}
16353
}
16454
}
16555
],

package.json

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@
4848
"version:prerelease": "tsx scripts/version-manager.ts prerelease",
4949
"version:beta": "tsx scripts/version-manager.ts minor beta",
5050
"version:beta-patch": "tsx scripts/version-manager.ts patch beta",
51-
"release": "npm run ffmpeg:download-all && npm run build:release && electron-builder --publish onTagOrDraft",
52-
"release:all": "npm run ffmpeg:download-all && npm run build:release && electron-builder --publish always",
53-
"release:never": "npm run ffmpeg:download-all && npm run build:release && electron-builder --publish never",
54-
"release:draft": "npm run ffmpeg:download-all && npm run build:release && electron-builder --publish onTagOrDraft",
51+
"release": "npm run build:release && electron-builder --publish onTagOrDraft",
52+
"release:all": "npm run build:release && electron-builder --publish always",
53+
"release:never": "npm run build:release && electron-builder --publish never",
54+
"release:draft": "npm run build:release && electron-builder --publish onTagOrDraft",
5555
"migrate": "tsx src/main/db/migration-cli.ts",
5656
"migrate:up": "npm run migrate up",
5757
"migrate:down": "npm run migrate down",
@@ -71,9 +71,7 @@
7171
"ffmpeg:download": "tsx scripts/download-ffmpeg.ts current",
7272
"ffmpeg:download-all": "tsx scripts/download-ffmpeg.ts all",
7373
"ffmpeg:clean": "tsx scripts/download-ffmpeg.ts clean",
74-
"ffmpeg:test": "tsx scripts/test-ffmpeg-integration.ts",
75-
"prebuild": "npm run ffmpeg:download",
76-
"prebuild:release": "echo 'FFmpeg already downloaded by release script'"
74+
"ffmpeg:test": "tsx scripts/test-ffmpeg-integration.ts"
7775
},
7876
"dependencies": {
7977
"@ant-design/icons": "^6.0.1",

packages/shared/IpcChannel.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,18 @@ export enum IpcChannel {
7777
Ffmpeg_GetVideoInfo = 'ffmpeg:get-video-info',
7878
Ffmpeg_Warmup = 'ffmpeg:warmup',
7979
Ffmpeg_GetWarmupStatus = 'ffmpeg:get-warmup-status',
80+
Ffmpeg_GetInfo = 'ffmpeg:get-info',
81+
Ffmpeg_AutoDetectAndDownload = 'ffmpeg:auto-detect-and-download',
82+
83+
// FFmpeg 下载相关 IPC 通道 / FFmpeg download related IPC channels
84+
FfmpegDownload_CheckExists = 'ffmpeg-download:check-exists',
85+
FfmpegDownload_GetVersion = 'ffmpeg-download:get-version',
86+
FfmpegDownload_Download = 'ffmpeg-download:download',
87+
FfmpegDownload_GetProgress = 'ffmpeg-download:get-progress',
88+
FfmpegDownload_Cancel = 'ffmpeg-download:cancel',
89+
FfmpegDownload_Remove = 'ffmpeg-download:remove',
90+
FfmpegDownload_GetAllVersions = 'ffmpeg-download:get-all-versions',
91+
FfmpegDownload_CleanupTemp = 'ffmpeg-download:cleanup-temp',
8092

8193
// MediaInfo 相关 IPC 通道 / MediaInfo related IPC channels
8294
MediaInfo_CheckExists = 'mediainfo:check-exists',

src/main/__tests__/ipc.database.test.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,17 @@ vi.mock('../services/FFmpegService', () => ({
166166
getVideoInfo: vi.fn(),
167167
transcodeVideo: vi.fn(),
168168
cancelTranscode: vi.fn(),
169-
getFFmpegPath: vi.fn()
169+
getFFmpegPath: vi.fn(),
170+
getDownloadService: vi.fn(() => ({
171+
checkFFmpegExists: vi.fn(),
172+
getFFmpegVersion: vi.fn(),
173+
downloadFFmpeg: vi.fn(),
174+
getDownloadProgress: vi.fn(),
175+
cancelDownload: vi.fn(),
176+
removeFFmpeg: vi.fn(),
177+
getAllSupportedVersions: vi.fn(),
178+
cleanupTempFiles: vi.fn()
179+
}))
170180
}))
171181
}))
172182

src/main/ipc.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,51 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
474474
ipcMain.handle(IpcChannel.Ffmpeg_GetWarmupStatus, async () => {
475475
return FFmpegService.getWarmupStatus()
476476
})
477+
ipcMain.handle(IpcChannel.Ffmpeg_GetInfo, async () => {
478+
return ffmpegService.getFFmpegInfo()
479+
})
480+
ipcMain.handle(IpcChannel.Ffmpeg_AutoDetectAndDownload, async () => {
481+
return await ffmpegService.autoDetectAndDownload()
482+
})
483+
484+
// FFmpeg 下载服务
485+
const ffmpegDownloadService = ffmpegService.getDownloadService()
486+
ipcMain.handle(
487+
IpcChannel.FfmpegDownload_CheckExists,
488+
async (_, platform?: string, arch?: string) => {
489+
return ffmpegDownloadService.checkFFmpegExists(platform as any, arch as any)
490+
}
491+
)
492+
ipcMain.handle(
493+
IpcChannel.FfmpegDownload_GetVersion,
494+
async (_, platform?: string, arch?: string) => {
495+
return ffmpegDownloadService.getFFmpegVersion(platform as any, arch as any)
496+
}
497+
)
498+
ipcMain.handle(
499+
IpcChannel.FfmpegDownload_Download,
500+
async (_, platform?: string, arch?: string) => {
501+
return await ffmpegDownloadService.downloadFFmpeg(platform as any, arch as any)
502+
}
503+
)
504+
ipcMain.handle(
505+
IpcChannel.FfmpegDownload_GetProgress,
506+
async (_, platform?: string, arch?: string) => {
507+
return ffmpegDownloadService.getDownloadProgress(platform as any, arch as any)
508+
}
509+
)
510+
ipcMain.handle(IpcChannel.FfmpegDownload_Cancel, async (_, platform?: string, arch?: string) => {
511+
return ffmpegDownloadService.cancelDownload(platform as any, arch as any)
512+
})
513+
ipcMain.handle(IpcChannel.FfmpegDownload_Remove, async (_, platform?: string, arch?: string) => {
514+
return ffmpegDownloadService.removeFFmpeg(platform as any, arch as any)
515+
})
516+
ipcMain.handle(IpcChannel.FfmpegDownload_GetAllVersions, async () => {
517+
return ffmpegDownloadService.getAllSupportedVersions()
518+
})
519+
ipcMain.handle(IpcChannel.FfmpegDownload_CleanupTemp, async () => {
520+
return ffmpegDownloadService.cleanupTempFiles()
521+
})
477522

478523
// MediaParser (Remotion)
479524
ipcMain.handle(IpcChannel.MediaInfo_CheckExists, async () => {

0 commit comments

Comments
 (0)