Skip to content

Commit e750ff5

Browse files
committed
feat(media): support cloud codec via ffmpeg
1 parent 97e885f commit e750ff5

2 files changed

Lines changed: 86 additions & 4 deletions

File tree

internal/transcode/local_runner.go

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,8 +295,11 @@ func (r *LocalRunner) runJobLegacy(ctx context.Context, job *Job, profile Profil
295295
}
296296

297297
// watchSegments 周期性扫描输出目录,把新切片登记到 Cache
298+
// 【内存优化】把扫描频率从 500ms 降到 2s,减少每次 os.ReadDir 和 parseFFmpegPlaylistDurations
299+
// 触发的字符串/字节切片分配。chunk 模式下每个 chunk 完成时还会主动调用 publishChunkSegments,
300+
// 所以这里只需要兜底登记中间切片,2s 频率完全够用。
298301
func (r *LocalRunner) watchSegments(job *Job, profile Profile, dir string, stop chan struct{}) {
299-
t := time.NewTicker(500 * time.Millisecond)
302+
t := time.NewTicker(2 * time.Second)
300303
defer t.Stop()
301304
for {
302305
select {
@@ -439,9 +442,35 @@ func (r *LocalRunner) publishChunkSegments(job *Job, profile Profile, dir string
439442
}
440443
}
441444

445+
// playlistDurCache 缓存 m3u8 解析结果,按文件 mtime+size 失效。
446+
// 【内存优化】scanAndPublish 每 2 秒一次,每次都会解析同一个 m3u8(长视频几百 KB),
447+
// strings.Split 会产生大量临时字符串切片造成 GC 压力。用缓存减少 99% 的重复解析。
448+
type playlistDurCacheEntry struct {
449+
mtime time.Time
450+
size int64
451+
durs map[int]float64
452+
}
453+
454+
var (
455+
playlistDurCache = make(map[string]*playlistDurCacheEntry)
456+
playlistDurCacheMu sync.Mutex
457+
)
458+
442459
// parseFFmpegPlaylistDurations 解析 ffmpeg 写出的 m3u8,返回 seq -> duration 的映射
443460
// 用于获取每个切片的真实时长,避免后端用硬编码 segDur 导致总时长显示偏差
444461
func parseFFmpegPlaylistDurations(path string) map[int]float64 {
462+
fi, err := os.Stat(path)
463+
if err != nil {
464+
return map[int]float64{}
465+
}
466+
// 命中缓存:mtime + size 都没变就直接复用
467+
playlistDurCacheMu.Lock()
468+
if e, ok := playlistDurCache[path]; ok && e.mtime.Equal(fi.ModTime()) && e.size == fi.Size() {
469+
playlistDurCacheMu.Unlock()
470+
return e.durs
471+
}
472+
playlistDurCacheMu.Unlock()
473+
445474
out := make(map[int]float64)
446475
data, err := os.ReadFile(path)
447476
if err != nil {
@@ -469,6 +498,14 @@ func parseFFmpegPlaylistDurations(path string) map[int]float64 {
469498
pendingDur = 0
470499
}
471500
}
501+
// 写入缓存
502+
playlistDurCacheMu.Lock()
503+
playlistDurCache[path] = &playlistDurCacheEntry{
504+
mtime: fi.ModTime(),
505+
size: fi.Size(),
506+
durs: out,
507+
}
508+
playlistDurCacheMu.Unlock()
472509
return out
473510
}
474511

server/handles/transcode.go

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@ import (
55
"net/http"
66
"net/url"
77
"os"
8+
"path/filepath"
89
"strconv"
910
"strings"
1011
"time"
1112

1213
"github.com/OpenListTeam/OpenList/v4/internal/conf"
14+
"github.com/OpenListTeam/OpenList/v4/internal/driver"
1315
"github.com/OpenListTeam/OpenList/v4/internal/fs"
16+
"github.com/OpenListTeam/OpenList/v4/internal/op"
1417
"github.com/OpenListTeam/OpenList/v4/internal/setting"
1518
"github.com/OpenListTeam/OpenList/v4/internal/sign"
1619
"github.com/OpenListTeam/OpenList/v4/internal/transcode"
@@ -65,10 +68,21 @@ func TranscodePlay(c *gin.Context) {
6568
mgr := transcode.Default()
6669
mgr.Start()
6770

68-
// 构造源签名 URL(让 Worker 通过 /d/ 直接下载)
71+
// 【内存优化】优先尝试解析为本地文件路径,让 ffmpeg 直接读取本地文件,
72+
// 完全绕过 HTTP /d 代理路径——避免 net.Downloader 给每个 range 请求分配
73+
// 高达 MaxBufferLimit (默认系统内存 5%) 的大缓冲。这是降低 Go 进程内存
74+
// 占用最有效的方法。如果不是本地驱动则退回到 HTTP 签名 URL。
6975
apiURL := common.GetApiUrlFromRequest(c.Request)
70-
signedPath := sign.Sign(req.Path)
71-
sourceURL := fmt.Sprintf("%s/d%s?sign=%s", apiURL, encodePath(req.Path), signedPath)
76+
var sourceURL string
77+
if localFile, ok := resolveLocalFilePath(req.Path); ok {
78+
// ffmpeg 接受本地路径作为输入,无需 file:// 前缀
79+
sourceURL = localFile
80+
fmt.Printf("[transcode] using local file path for ffmpeg: %s\n", localFile)
81+
} else {
82+
// 远程驱动:构造签名 HTTP URL
83+
signedPath := sign.Sign(req.Path)
84+
sourceURL = fmt.Sprintf("%s/d%s?sign=%s", apiURL, encodePath(req.Path), signedPath)
85+
}
7286

7387
// 创建 Job
7488
job := transcode.NewJob()
@@ -110,6 +124,37 @@ func encodePath(p string) string {
110124
return strings.Join(parts, "/")
111125
}
112126

127+
// resolveLocalFilePath 尝试把 OpenList 路径解析为宿主机文件系统的实际路径。
128+
// 仅对本地驱动 (Local) 有效,其他驱动(云盘等)返回 false。
129+
//
130+
// 这是内存优化的关键路径:本地文件场景下让 ffmpeg 直接读文件,可以完全绕过
131+
// HTTP /d 代理路径上的 net.Downloader 大块缓冲(默认每个 range 请求最高
132+
// 占用 MaxBufferLimit = 系统内存 5%,多 chunk 并发 + 多 range 累计可达数 GB)。
133+
func resolveLocalFilePath(rawPath string) (string, bool) {
134+
storage, actualPath, err := op.GetStorageAndActualPath(rawPath)
135+
if err != nil || storage == nil {
136+
return "", false
137+
}
138+
if storage.Config().Name != "Local" {
139+
return "", false
140+
}
141+
rooter, ok := storage.(driver.IRootPath)
142+
if !ok {
143+
return "", false
144+
}
145+
root := rooter.GetRootPath()
146+
if root == "" {
147+
return "", false
148+
}
149+
full := filepath.Join(root, actualPath)
150+
// 只接受真实存在的文件,避免传给 ffmpeg 一个无效路径
151+
fi, err := os.Stat(full)
152+
if err != nil || fi.IsDir() {
153+
return "", false
154+
}
155+
return full, true
156+
}
157+
113158
// ============================================================
114159
// 播放端:/tc/:job/:token/... Master/Variant playlist & 切片
115160
// ============================================================

0 commit comments

Comments
 (0)