Skip to content

Commit b412a58

Browse files
committed
feat(media-server): implement runtime runtime management system (#204)
* feat(media-server): implement runtime runtime management system - add UV bootstrapper with cross-platform install, caching, progress, validation - manage Python venv lifecycle, dependency installs, cleanup, and phased progress - introduce MediaServerService with lifecycle control, health checks, and port management - extend FFmpeg service to handle FFprobe with unified downloads and version updates - build settings UI sections for media server and plugins with real-time status - wire up 38 IPC channels covering UV, venv, FFmpeg/FFprobe, and server control - update build configs, add ffprobe download script, auto-start server, and bump backend ref * test: Update tests * test(main): mock electron app for ipc handlers * Update src/renderer/src/pages/settings/FFprobeSection.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Apply suggestion from @coderabbitai[bot] Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * refactor(types): extract common Platform and Arch types to shared package - Create packages/shared/types/system.ts with Platform and Arch type definitions - Remove duplicate type definitions from FFmpegDownloadService - Remove duplicate type definitions from UvBootstrapperService - Import shared types from @shared/types/system in both services This change improves code maintainability by centralizing common system type definitions. * fix(media-server): clear port and notify frontend on process exit Ensure media server port is properly cleared and frontend is notified when the process stops or crashes to prevent stale port usage. Changes: - Update notifyPortChanged signature to accept number | null - Clear this.port to null in both normal stop and crash exit handlers - Notify frontend with null port when server stops - Update SessionService listener to handle null port notifications - Add appropriate logging for port clearing events This prevents the frontend from continuing to use expired ports after the media server has stopped, requiring manual app restart. * fix(settings): prevent setTimeout memory leak in MediaServerSection Fix potential memory leak and unmounted component state update in MediaServerSection by properly managing setTimeout lifecycle: - Add successTimeoutRef to store timeout handle - Clear timeout in effect cleanup on component unmount - Clear timeout before creating new one in handleInstall - Clear timeout in error handler to prevent orphaned timers - Set ref to null after clearing to maintain clean state This prevents "Can't perform a React state update on an unmounted component" warnings and ensures proper cleanup of async operations. Co-Authored-By: Claude <noreply@anthropic.com> * fix(media-server): improve status display during installation and startup - Add 'starting' status immediately after installation completes - Move fetchServerInfo() to finally block to ensure status refresh - Prevent confusing 'stopped' status display during startup process * fix(python-venv): preserve final install progress state for callers Remove finally block that immediately clears installProgress, preventing callers from reading final "completed" or "error" state. Instead: - Clear installProgress at start of new installation - Preserve final state after completion/error for caller consumption - Remove unnecessary finally block cleanup This ensures MediaServerSection and other callers can reliably read the final installation state. Co-Authored-By: Claude <noreply@anthropic.com> * refactor(types): centralize media server types to shared package - Create shared types module for Media Server and Python Venv - Add media-server.ts with MediaServerInfo, MediaServerStatus, PythonVenvInfo, and InstallProgress types - Remove duplicate type definitions from MediaServerService, PythonVenvService, and MediaServerSection - Update imports to use @shared/types across main and renderer processes - Maintain backward compatibility by re-exporting types from service files - All type checks passing
1 parent 51c7b89 commit b412a58

28 files changed

Lines changed: 6459 additions & 172 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,7 @@ docs/.vitepress/cache/deps
5050
dist-test
5151

5252
resources/ffmpeg/
53+
resources/ffprobe/
5354
resources/media-server
5455
.ffmpeg-cache
56+
.ffprobe-cache

electron-builder.yml

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,31 +18,12 @@ protocols:
1818
- echoplayer
1919
icon: icons
2020
files:
21-
- '!**/.vscode/*'
22-
- '!src/*'
23-
- '!electron.vite.config.{js,ts,mjs,cjs}'
24-
- '!{.eslintcache,eslint.config.mjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
25-
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
26-
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
27-
- '!docs'
28-
- '!scripts'
29-
- '!.swc'
30-
- '!.bin'
31-
- '!*.log'
32-
- '!*.md'
33-
- '!**/*.{iml,o,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,xproj}'
34-
- '!**/*.{spec,test}.{js,jsx,ts,tsx}'
35-
- '!**/*.min.*.map'
36-
- '!**/*.d.ts'
37-
- '!**/dist/es6/**'
38-
- '!**/dist/demo/**'
39-
- '!**/*.{h,iobj,ipdb,tlog,recipe,vcxproj,vcxproj.filters,Makefile,*.Makefile}' # filter .node build files
40-
- '!**/{.DS_Store,Thumbs.db,thumbs.db,__pycache__}'
41-
# 修复包含规则 - 需要排除不必要的文件但保留迁移文件
42-
- '!**/*.{map,ts,tsx,jsx,less,scss,sass,css.d.ts,d.cts,d.mts,md,markdown,yaml,yml}'
43-
- '!db/**/*.{ts,md,yaml,yml}' # 排除 db 目录中的非 js 文件
44-
- '!**/{test,tests,__tests__,powered-test,coverage}/**'
45-
- '!**/{example,examples}/**'
21+
- from: .
22+
filter:
23+
- 'out/**' # 渲染进程打包后文件
24+
- 'resources/**' # 运行时资源(如果需要)
25+
- 'package.json'
26+
- '!**/*.map'
4627
asarUnpack:
4728
- resources/**
4829
- '**/*.{metal,exp,lib}'

electron.vite.config.ts

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -66,39 +66,43 @@ export default defineConfig({
6666
}
6767

6868
// 需要排除的文件和目录
69-
const excludePatterns = [
70-
'__pycache__',
71-
'.pyc',
72-
'.pyo',
73-
'.pyd',
74-
'.venv',
75-
'venv',
76-
'env',
77-
'.git',
78-
'.gitignore',
79-
'cache',
80-
'.vscode',
81-
'.idea',
82-
'.egg-info',
83-
'dist',
84-
'build',
85-
'.pytest_cache',
86-
'.mypy_cache',
87-
'.ruff_cache',
88-
'uv.lock',
89-
'.DS_Store',
90-
'assets'
69+
const excludeDirPatterns = [
70+
/^__pycache__$/,
71+
/^\.venv$/,
72+
/^venv$/,
73+
/^env$/,
74+
/^\.git$/,
75+
/^\.gitignore$/,
76+
/^\.vscode$/,
77+
/^\.idea$/,
78+
/^\.pytest_cache$/,
79+
/^\.mypy_cache$/,
80+
/^\.ruff_cache$/,
81+
/^cache$/,
82+
/^dist$/,
83+
/^build$/,
84+
/^assets$/,
85+
/\.egg-info$/
9186
]
87+
const excludeFileNames = new Set(['uv.lock', '.gitignore', '.DS_Store'])
88+
const excludeFileSuffixes = ['.pyc', '.pyo', '.pyd']
9289

9390
// 检查是否应排除
94-
const shouldExclude = (filePath: string): boolean => {
95-
const basename = path.basename(filePath)
96-
return excludePatterns.some((pattern) => {
97-
if (pattern.startsWith('.') && !pattern.includes('_')) {
98-
return basename.endsWith(pattern) || basename === pattern
91+
const shouldExclude = (entry: fs.Dirent): boolean => {
92+
const { name } = entry
93+
94+
if (entry.isDirectory()) {
95+
return excludeDirPatterns.some((pattern) => pattern.test(name))
96+
}
97+
98+
if (entry.isFile()) {
99+
if (excludeFileNames.has(name)) {
100+
return true
99101
}
100-
return basename.includes(pattern)
101-
})
102+
return excludeFileSuffixes.some((suffix) => name.endsWith(suffix))
103+
}
104+
105+
return false
102106
}
103107

104108
// 递归复制目录
@@ -113,7 +117,7 @@ export default defineConfig({
113117
const srcPath = path.join(src, entry.name)
114118
const destPath = path.join(dest, entry.name)
115119

116-
if (shouldExclude(srcPath)) {
120+
if (shouldExclude(entry)) {
117121
continue
118122
}
119123

packages/shared/IpcChannel.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,44 @@ export enum IpcChannel {
9090
FfmpegDownload_GetAllVersions = 'ffmpeg-download:get-all-versions',
9191
FfmpegDownload_CleanupTemp = 'ffmpeg-download:cleanup-temp',
9292

93+
// FFprobe 相关 IPC 通道 / FFprobe related IPC channels
94+
Ffprobe_GetPath = 'ffprobe:get-path',
95+
Ffprobe_CheckExists = 'ffprobe:check-exists',
96+
Ffprobe_GetVersion = 'ffprobe:get-version',
97+
Ffprobe_GetInfo = 'ffprobe:get-info',
98+
99+
// FFprobe 下载相关 IPC 通道 / FFprobe download related IPC channels
100+
FfprobeDownload_CheckExists = 'ffprobe-download:check-exists',
101+
FfprobeDownload_GetVersion = 'ffprobe-download:get-version',
102+
FfprobeDownload_Download = 'ffprobe-download:download',
103+
FfprobeDownload_GetProgress = 'ffprobe-download:get-progress',
104+
FfprobeDownload_Cancel = 'ffprobe-download:cancel',
105+
FfprobeDownload_Remove = 'ffprobe-download:remove',
106+
107+
// UV 相关 IPC 通道 / UV related IPC channels
108+
UV_CheckInstallation = 'uv:check-installation',
109+
UV_Download = 'uv:download',
110+
UV_GetProgress = 'uv:get-progress',
111+
UV_CancelDownload = 'uv:cancel-download',
112+
UV_GetInfo = 'uv:get-info',
113+
114+
// Python 环境相关 IPC 通道 / Python environment related IPC channels
115+
PythonVenv_CheckInfo = 'python-venv:check-info',
116+
PythonVenv_Initialize = 'python-venv:initialize',
117+
PythonVenv_ReinstallDependencies = 'python-venv:reinstall-dependencies',
118+
PythonVenv_Remove = 'python-venv:remove',
119+
PythonVenv_GetProgress = 'python-venv:get-progress',
120+
PythonVenv_GetMediaServerPath = 'python-venv:get-media-server-path',
121+
122+
// Media Server 相关 IPC 通道 / Media Server related IPC channels
123+
MediaServer_Start = 'media-server:start',
124+
MediaServer_Stop = 'media-server:stop',
125+
MediaServer_Restart = 'media-server:restart',
126+
MediaServer_GetInfo = 'media-server:get-info',
127+
MediaServer_GetPort = 'media-server:get-port',
128+
MediaServer_CheckHealth = 'media-server:check-health',
129+
MediaServer_PortChanged = 'media-server:port-changed', // 端口变更事件
130+
93131
// MediaInfo 相关 IPC 通道 / MediaInfo related IPC channels
94132
MediaInfo_CheckExists = 'mediainfo:check-exists',
95133
MediaInfo_GetVersion = 'mediainfo:get-version',

packages/shared/types/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/**
2+
* Shared types index
3+
*/
4+
export * from './database'
5+
export * from './media-server'
6+
export * from './mediainfo'
7+
export * from './system'
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* Media Server 和 Python Venv 相关的共享类型定义
3+
*/
4+
5+
/**
6+
* Media Server 状态
7+
*/
8+
export type MediaServerStatus =
9+
| 'stopped' // 已停止
10+
| 'starting' // 正在启动
11+
| 'running' // 运行中
12+
| 'stopping' // 正在停止
13+
| 'error' // 错误
14+
15+
/**
16+
* Media Server 信息
17+
*/
18+
export interface MediaServerInfo {
19+
status: MediaServerStatus
20+
pid?: number
21+
port?: number
22+
startTime?: number
23+
uptime?: number
24+
error?: string
25+
}
26+
27+
/**
28+
* Python 虚拟环境状态
29+
*/
30+
export interface PythonVenvInfo {
31+
exists: boolean
32+
venvPath?: string
33+
pythonPath?: string
34+
pythonVersion?: string
35+
hasProjectConfig: boolean
36+
hasLockfile: boolean
37+
}
38+
39+
/**
40+
* 环境安装进度
41+
*/
42+
export interface InstallProgress {
43+
stage: 'init' | 'venv' | 'deps' | 'completed' | 'error'
44+
message: string
45+
percent: number
46+
}

packages/shared/types/system.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Common platform and architecture types shared across main process services.
2+
export type Platform = 'win32' | 'darwin' | 'linux'
3+
export type Arch = 'x64' | 'arm64'

scripts/download-ffmpeg.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,12 @@ const FFMPEG_CONFIG: FFmpegDownloadConfig = {
3535
},
3636
darwin: {
3737
x64: {
38-
url: 'https://evermeet.cx/ffmpeg/ffmpeg-6.1.zip',
38+
url: 'https://evermeet.cx/ffmpeg/ffmpeg-8.0.zip',
3939
executable: 'ffmpeg',
4040
extractPath: 'ffmpeg'
4141
},
4242
arm64: {
43-
url: 'https://evermeet.cx/ffmpeg/ffmpeg-6.1.zip', // 通用二进制文件
43+
url: 'https://evermeet.cx/ffmpeg/ffmpeg-8.0.zip', // 通用二进制文件
4444
executable: 'ffmpeg',
4545
extractPath: 'ffmpeg'
4646
}

0 commit comments

Comments
 (0)