Skip to content

Commit 0386f87

Browse files
committed
feat(media): support cloud codec via ffmpeg
1 parent c7c0cfa commit 0386f87

16 files changed

Lines changed: 3230 additions & 0 deletions

File tree

cmd/worker/main.go

Lines changed: 565 additions & 0 deletions
Large diffs are not rendered by default.

docs/transcode-protocol.md

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# OpenList 转码协议 v1(自研)
2+
3+
> 用于 OpenList Master 与远程 FFmpeg 转码 Worker 之间的通信。
4+
> 协议本身、所有依赖、推荐编码 (H.264/AV1) 均为开源免费,可商用。
5+
6+
## 总览
7+
8+
```
9+
Player ──► Master(/api/transcode/play) ──► Scheduler.Submit
10+
Worker ──► Master(/api/transcode/worker/register) # 启动一次
11+
Worker ──► Master(/api/transcode/worker/heartbeat) # 周期 10s
12+
Worker ──► Master(/api/transcode/worker/claim) # 长轮询拉任务
13+
Worker ──► Master(/api/transcode/worker/segment) # 流式回推切片
14+
Worker ──► Master(/api/transcode/worker/job/finish) # 上报完成
15+
Player ──► Master(/tc/{job}/{token}/master.m3u8) # 拉取播放列表
16+
Player ──► Master(/tc/{job}/{token}/{profile}/seg-N.ts) # 拉取切片
17+
```
18+
19+
## 通用约定
20+
- 所有请求体均为 JSON。
21+
- Worker 与 Master 间通过两类凭据鉴权:
22+
- **shared_secret**(管理端 `transcode_worker_secret` 设置项):用于 register / heartbeat / claim
23+
- **callback_token**(Master 在 job 中下发,一次性):用于 segment / job/finish
24+
- Header 形式:`Authorization: Bearer <token>`
25+
- 返回包统一使用 OpenList 通用响应:`{"code":200,"message":"success","data":{...}}`
26+
27+
## 端点详细
28+
29+
### POST /api/transcode/worker/register
30+
> 鉴权:Bearer = shared_secret
31+
32+
请求:
33+
```json
34+
{
35+
"name": "gpu-node-01",
36+
"version": "1.0.0",
37+
"capacity": 4,
38+
"hwaccel": ["nvenc"],
39+
"codecs_decode": ["h264","hevc","av1","vp9"],
40+
"codecs_encode": ["h264","hevc"],
41+
"max_resolution": "3840x2160",
42+
"tags": ["linux","amd64"]
43+
}
44+
```
45+
46+
响应:
47+
```json
48+
{
49+
"code":200,"message":"success",
50+
"data": {
51+
"worker_id":"wk_xxx",
52+
"heartbeat_interval":10,
53+
"claim_strategy":"pull",
54+
"protocol_version":"v1"
55+
}
56+
}
57+
```
58+
59+
### POST /api/transcode/worker/heartbeat
60+
> 鉴权:Bearer = shared_secret
61+
> 间隔:register 返回的 `heartbeat_interval`;服务端 30s 未收到则剔除 worker
62+
63+
请求:
64+
```json
65+
{ "worker_id":"wk_xxx", "load":0.4, "running":["job_a"], "free_slots":3 }
66+
```
67+
68+
响应:
69+
```json
70+
{ "code":200,"message":"success","data":{"ok":true,"kick":false,"cancel":["job_x"]}}
71+
```
72+
- `kick=true`:服务端要求 Worker 主动退出
73+
- `cancel`:需要 Worker 立刻终止的 job_ids(异步取消通道)
74+
75+
### POST /api/transcode/worker/claim
76+
> 鉴权:Bearer = shared_secret
77+
> 长轮询:当无任务时,服务端最多挂起 `wait` 秒(最大 30)
78+
79+
请求:
80+
```json
81+
{ "worker_id":"wk_xxx", "slots":1, "wait":20 }
82+
```
83+
84+
响应:
85+
```json
86+
{ "code":200,"message":"success","data":{"jobs":[{
87+
"id":"job_xxx",
88+
"path":"/movies/x.mkv",
89+
"source_url":"https://example.com/d/movies/x.mkv?sign=...",
90+
"profiles":[{
91+
"name":"1080p","video_codec":"h264","video_bitrate":"4000k",
92+
"scale":"1920:-2","audio_codec":"aac","audio_bitrate":"160k",
93+
"hwaccel":"nvenc"
94+
}],
95+
"output":{"format":"hls","segment_duration":6},
96+
"callback_token":"tk_xxx",
97+
"deadline":1731601234
98+
}]}}
99+
```
100+
101+
### PUT /api/transcode/worker/segment
102+
> 鉴权:Bearer = callback_token
103+
> Query 参数:
104+
> - `job` 任务 ID
105+
> - `profile` profile 名(如 `1080p`
106+
> - `seq` 切片序号(从 0 开始)
107+
> - `duration` 该切片秒数
108+
> - `final` 是否为最后一片(true/false)
109+
>
110+
> Body:原始 MPEG-TS 二进制(`Content-Type: video/mp2t`
111+
112+
响应:`204 No Content`
113+
114+
### POST /api/transcode/worker/job/finish
115+
> 鉴权:Bearer = callback_token
116+
117+
请求:
118+
```json
119+
{
120+
"job_id":"job_xxx",
121+
"status":"finished",
122+
"error":"",
123+
"stats":{"elapsed":312.4,"fps":120,"speed":2.1,"bytes_out":890123456}
124+
}
125+
```
126+
status 取值:`finished` / `failed` / `cancelled`
127+
128+
## 播放端
129+
130+
### POST /api/fs/transcode/play
131+
> 由前端播放器调用,用户登录态可见
132+
> 请求:`{"path":"/movies/x.mkv"}`
133+
>
134+
> 响应(无需转码):
135+
> `{"transcode":false,"reason":"size below threshold"}`
136+
>
137+
> 响应(已下发转码任务):
138+
> `{"transcode":true,"job_id":"job_xx","master_url":"https://.../tc/.../master.m3u8","profile":"1080p"}`
139+
140+
### GET /tc/:job/:token/master.m3u8
141+
返回多档位主 m3u8。
142+
143+
### GET /tc/:job/:token/:profile/playlist.m3u8
144+
阻塞等待首切片就绪(最多 30s)后返回 HLS 播放列表,未结束时不带 `#EXT-X-ENDLIST`
145+
146+
### GET /tc/:job/:token/:profile/seg-N.ts
147+
返回 N 号切片;如果尚未生成,最多等待 60s。
148+
149+
## 启动远程 Worker
150+
151+
```bash
152+
# 用 Master 配置中的 transcode_worker_secret 作为密钥
153+
go build -o openlist-worker ./cmd/worker
154+
155+
./openlist-worker \
156+
-master http://openlist.example.com:5244 \
157+
-secret <transcode_worker_secret> \
158+
-capacity 2 \
159+
-hwaccel nvenc \
160+
-ffmpeg /usr/bin/ffmpeg \
161+
-workdir /tmp/openlist-worker
162+
```
163+
164+
## License 说明
165+
- 协议:自研,可自由使用
166+
- FFmpeg:使用系统包提供的 LGPL 版本即可(含 libx264/libx265,libx264 是 GPL,建议在 Worker 镜像中遵循 GPL 公开/动态链接合规)
167+
- 推荐编码:H.264 互联网分发免授权 / AV1 永久免版税

internal/bootstrap/data/setting.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,69 @@ func InitialSettings() []model.SettingItem {
242242
{Key: conf.StreamMaxClientUploadSpeed, Value: "-1", Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE},
243243
{Key: conf.StreamMaxServerDownloadSpeed, Value: "-1", Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE},
244244
{Key: conf.StreamMaxServerUploadSpeed, Value: "-1", Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE},
245+
246+
// media settings
247+
{Key: conf.MediaTMDBKey, Value: "", Type: conf.TypeString, Group: model.MEDIA, Flag: model.PRIVATE},
248+
{Key: conf.MediaTMDBAPIURL, Value: "api.themoviedb.org", Type: conf.TypeString, Group: model.MEDIA, Flag: model.PRIVATE},
249+
{Key: conf.MediaDiscogsToken, Value: "", Type: conf.TypeString, Group: model.MEDIA, Flag: model.PRIVATE},
250+
{Key: conf.MediaDiscogsAPIURL, Value: "api.discogs.com", Type: conf.TypeString, Group: model.MEDIA, Flag: model.PRIVATE},
251+
{Key: conf.MediaStoreThumbnail, Value: "false", Type: conf.TypeBool, Group: model.MEDIA, Flag: model.PRIVATE},
252+
{Key: conf.MediaThumbnailMode, Value: "base64", Type: conf.TypeSelect, Options: "base64,local", Group: model.MEDIA, Flag: model.PRIVATE},
253+
{Key: conf.MediaThumbnailPath, Value: "/imgs", Type: conf.TypeString, Group: model.MEDIA, Flag: model.PRIVATE},
254+
{Key: conf.MediaScrapeConcurrency, Value: "5", Type: conf.TypeNumber, Group: model.MEDIA, Flag: model.PRIVATE},
255+
256+
// transcode settings (FFmpeg 云端/本地转码) - 默认全部关闭
257+
{Key: conf.TranscodeEnabled, Value: "false", Type: conf.TypeBool, Group: model.TRANSCODE, Flag: model.PRIVATE,
258+
Help: `开启后,超过阈值的媒体文件将通过 FFmpeg 转码后再播放;默认关闭`},
259+
{Key: conf.TranscodeRunMode, Value: "local", Type: conf.TypeSelect, Options: "local,remote,hybrid", Group: model.TRANSCODE, Flag: model.PRIVATE,
260+
Help: `local=仅使用本机内置 worker;remote=只使用远程 Worker 节点;hybrid=本地优先,超载后派发到远程`},
261+
{Key: conf.TranscodeMinSizeGB, Value: "5", Type: conf.TypeNumber, Group: model.TRANSCODE, Flag: model.PRIVATE,
262+
Help: `文件大于该 GB 数才走转码(小于则直链播放),0=任意大小都转码`},
263+
{Key: conf.TranscodeMinBitrateMbps, Value: "20", Type: conf.TypeNumber, Group: model.TRANSCODE, Flag: model.PRIVATE,
264+
Help: `视频码率超过该 Mbps 才转码,0=不限制`},
265+
{Key: conf.TranscodeSourceCodecs, Value: "hevc,h265,av1,vvc,vp9", Type: conf.TypeString, Group: model.TRANSCODE, Flag: model.PRIVATE,
266+
Help: `仅对这些源视频编码进行转码(逗号分隔)。常见高码率/兼容性差的编码:hevc,av1,vvc,vp9`},
267+
{Key: conf.TranscodeSourceExtensions, Value: "mkv,ts,m2ts,mov,avi,wmv,flv,rmvb,webm", Type: conf.TypeString, Group: model.TRANSCODE, Flag: model.PRIVATE,
268+
Help: `仅对这些后缀进行转码(逗号分隔,不带点),mp4 默认不转码可直接播放`},
269+
{Key: conf.TranscodeOutputFormat, Value: "hls", Type: conf.TypeSelect, Options: "hls,dash,mp4", Group: model.TRANSCODE, Flag: model.PRIVATE,
270+
Help: `输出封装格式,HLS 兼容性最好`},
271+
{Key: conf.TranscodeOutputCodec, Value: "h264", Type: conf.TypeSelect, Options: "h264,hevc,av1", Group: model.TRANSCODE, Flag: model.PRIVATE,
272+
Help: `重新编码后的视频编码,推荐 h264(最广兼容、互联网分发免授权费)`},
273+
{Key: conf.TranscodeOutputBitrate, Value: "4000k", Type: conf.TypeString, Group: model.TRANSCODE, Flag: model.PRIVATE,
274+
Help: `输出视频码率,例如 4000k / 6M。建议 1080p:4000k、720p:2500k`},
275+
{Key: conf.TranscodeOutputAudioCodec, Value: "aac", Type: conf.TypeSelect, Options: "aac,mp3,opus,copy", Group: model.TRANSCODE, Flag: model.PRIVATE,
276+
Help: `输出音频编码,aac 兼容性最好;copy=直接复制源音频流`},
277+
{Key: conf.TranscodeOutputAudioBitrate, Value: "160k", Type: conf.TypeString, Group: model.TRANSCODE, Flag: model.PRIVATE,
278+
Help: `输出音频码率`},
279+
{Key: conf.TranscodeOutputResolution, Value: "1920x1080", Type: conf.TypeSelect, Options: "source,3840x2160,2560x1440,1920x1080,1280x720,854x480", Group: model.TRANSCODE, Flag: model.PRIVATE,
280+
Help: `输出分辨率上限,超过该分辨率会下采样;source=保持源分辨率`},
281+
{Key: conf.TranscodeSegmentDuration, Value: "6", Type: conf.TypeNumber, Group: model.TRANSCODE, Flag: model.PRIVATE,
282+
Help: `HLS/DASH 切片时长(秒),越小首帧越快但请求数变多,推荐 4-10`},
283+
{Key: conf.TranscodeHWAccel, Value: "none", Type: conf.TypeSelect,
284+
Options: "none,auto,nvenc,qsv,vaapi,amf,videotoolbox",
285+
Group: model.TRANSCODE, Flag: model.PRIVATE,
286+
Help: `GPU 硬件加速:
287+
none = 纯 CPU(libx264)
288+
auto = 自动探测可用加速器
289+
nvenc = NVIDIA GPU(GeForce/Tesla/Quadro/RTX,需 NVIDIA 驱动)
290+
qsv = Intel 集显/独显 QuickSync(免费,功耗低)
291+
vaapi = Linux 通用 VA-API(支持 Intel/AMD)
292+
amf = AMD GPU(Windows AMF)
293+
videotoolbox = macOS 硬件加速`},
294+
{Key: conf.TranscodeFFmpegPath, Value: "ffmpeg", Type: conf.TypeString, Group: model.TRANSCODE, Flag: model.PRIVATE,
295+
Help: `FFmpeg 可执行路径,留空则使用 PATH 中的 ffmpeg`},
296+
{Key: conf.TranscodeFFprobePath, Value: "ffprobe", Type: conf.TypeString, Group: model.TRANSCODE, Flag: model.PRIVATE,
297+
Help: `FFprobe 可执行路径`},
298+
{Key: conf.TranscodeWorkerSecret, Value: "", Type: conf.TypeString, Group: model.TRANSCODE, Flag: model.PRIVATE,
299+
Help: `远程 Worker 注册时使用的共享密钥,留空则禁用远程 Worker 注册`},
300+
{Key: conf.TranscodeCachePath, Value: "data/transcode_cache", Type: conf.TypeString, Group: model.TRANSCODE, Flag: model.PRIVATE,
301+
Help: `转码切片缓存目录`},
302+
{Key: conf.TranscodeCacheMaxGB, Value: "20", Type: conf.TypeNumber, Group: model.TRANSCODE, Flag: model.PRIVATE,
303+
Help: `切片缓存最大容量(GB),超过后按 LRU 清理`},
304+
{Key: conf.TranscodeJobTimeoutMin, Value: "120", Type: conf.TypeNumber, Group: model.TRANSCODE, Flag: model.PRIVATE,
305+
Help: `单个转码任务超时分钟数,超时自动失败`},
306+
{Key: conf.TranscodeLocalConcurrency, Value: "1", Type: conf.TypeNumber, Group: model.TRANSCODE, Flag: model.PRIVATE,
307+
Help: `本机内置 worker 同时执行的转码任务数(local/hybrid 模式生效)`},
245308
}
246309
additionalSettingItems := tool.Tools.Items()
247310
// 固定顺序

internal/bootstrap/run.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/OpenListTeam/OpenList/v4/internal/conf"
1616
"github.com/OpenListTeam/OpenList/v4/internal/db"
1717
"github.com/OpenListTeam/OpenList/v4/internal/fs"
18+
"github.com/OpenListTeam/OpenList/v4/internal/transcode"
1819
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
1920
"github.com/OpenListTeam/OpenList/v4/server"
2021
"github.com/OpenListTeam/OpenList/v4/server/middlewares"
@@ -92,6 +93,10 @@ func Start() {
9293
InitOfflineDownloadTools()
9394
LoadStorages()
9495
InitTaskManager()
96+
// 初始化转码模块(总开关关时也会启动 manager,但本地 worker 仅在 enabled 时由 Manager.Start 内部按 run_mode 启动)
97+
if transcode.IsEnabled() {
98+
transcode.Default().Start()
99+
}
95100
if !flags.Debug && !flags.Dev {
96101
gin.SetMode(gin.ReleaseMode)
97102
}

internal/conf/const.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,40 @@ const (
161161
StreamMaxClientUploadSpeed = "max_client_upload_speed"
162162
StreamMaxServerDownloadSpeed = "max_server_download_speed"
163163
StreamMaxServerUploadSpeed = "max_server_upload_speed"
164+
165+
// media
166+
MediaTMDBKey = "media_tmdb_key"
167+
MediaTMDBAPIURL = "media_tmdb_api_url"
168+
MediaDiscogsToken = "media_discogs_token"
169+
MediaDiscogsAPIURL = "media_discogs_api_url"
170+
MediaThumbnailMode = "media_thumbnail_mode"
171+
MediaThumbnailPath = "media_thumbnail_path"
172+
MediaStoreThumbnail = "media_store_thumbnail"
173+
MediaScrapeConcurrency = "media_scrape_concurrency"
174+
175+
// transcode (云端/本地 FFmpeg 转码)
176+
TranscodeEnabled = "transcode_enabled" // 总开关,默认关
177+
TranscodeMinSizeGB = "transcode_min_size_gb" // 文件大于多少 GB 才转码
178+
TranscodeMinBitrateMbps = "transcode_min_bitrate_mbps" // 视频码率高于多少 Mbps 才转码(0=不限)
179+
TranscodeSourceCodecs = "transcode_source_codecs" // 仅对这些源编码进行转码
180+
TranscodeSourceExtensions = "transcode_source_extensions" // 仅对这些后缀进行转码
181+
TranscodeOutputFormat = "transcode_output_format" // 输出封装格式 hls/dash/mp4
182+
TranscodeOutputCodec = "transcode_output_codec" // 重新编码的视频编码
183+
TranscodeOutputBitrate = "transcode_output_bitrate" // 输出视频码率,例如 4000k
184+
TranscodeOutputAudioCodec = "transcode_output_audio_codec" // 输出音频编码
185+
TranscodeOutputAudioBitrate = "transcode_output_audio_bitrate" // 输出音频码率,例如 160k
186+
TranscodeOutputResolution = "transcode_output_resolution" // 输出分辨率上限
187+
TranscodeSegmentDuration = "transcode_segment_duration" // HLS 切片时长(秒)
188+
TranscodeHWAccel = "transcode_hwaccel" // 硬件加速类型 none/auto/nvenc/qsv/vaapi/amf/videotoolbox
189+
TranscodeFFmpegPath = "transcode_ffmpeg_path" // 自定义 ffmpeg 可执行路径
190+
TranscodeFFprobePath = "transcode_ffprobe_path" // 自定义 ffprobe 可执行路径
191+
TranscodeRunMode = "transcode_run_mode" // local / remote / hybrid
192+
TranscodeWorkerSecret = "transcode_worker_secret" // 远程 Worker 共享密钥
193+
TranscodeCachePath = "transcode_cache_path" // 切片缓存目录
194+
TranscodeCacheMaxGB = "transcode_cache_max_gb" // 缓存最大占用,超出 LRU 淘汰
195+
TranscodeJobTimeoutMin = "transcode_job_timeout_min" // 单个任务超时分钟
196+
TranscodeLocalConcurrency = "transcode_local_concurrency" // 本地内置 worker 并发数(run_mode=local/hybrid 时生效)
197+
TranscodeIdleTimeoutSec = "transcode_idle_timeout_sec" // 播放端无请求多少秒后自动停止转码(默认90秒)
164198
)
165199

166200
const (

internal/model/setting.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ const (
1313
S3
1414
FTP
1515
TRAFFIC
16+
MEDIA
17+
TRANSCODE
1618
)
1719

1820
const (

0 commit comments

Comments
 (0)