优化转码逻辑

This commit is contained in:
truewhile
2026-09-07 23:10:48 +08:00
parent 0e592105c4
commit c4b8114115
10 changed files with 314 additions and 45 deletions
+87 -17
View File
@@ -50,6 +50,7 @@ type TranscoderService struct {
mu sync.Mutex
jobs map[string]*hlsJob
strmResolve func(ctx context.Context, raw string) (*StrmPlayResult, error)
probe *FFprobeService
}
// hlsJob holds the live state of one ffmpeg run.
@@ -61,6 +62,9 @@ type hlsJob struct {
lastAccess time.Time
playlistOK bool
encoder string
// startSec is the source seek offset fed to ffmpeg (-ss). The HLS
// playlist itself always starts at t=0 for that session.
startSec float64
}
var (
@@ -92,13 +96,24 @@ func (t *TranscoderService) PlaylistPath(mediaID string) string {
return filepath.Join(t.HLSDir(mediaID), "index.m3u8")
}
// EnsureJob makes sure a transcode is running for mediaID. The function is
// non-blocking: it returns the playlist path immediately. The caller is
// expected to poll until WaitReady reports true.
// EnsureJob makes sure a transcode is running for mediaID from the start of
// the source. Prefer EnsureJobFrom when the player seeks into the middle.
func (t *TranscoderService) EnsureJob(ctx context.Context, mediaID string) (string, error) {
return t.EnsureJobFrom(ctx, mediaID, 0)
}
// EnsureJobFrom starts (or reuses) an HLS job that seeks the source to
// startSec before encoding. Reusing only happens when an active job already
// matches that offset; otherwise the previous job is cancelled and the HLS
// cache dir is wiped so the player can jump without waiting for a full
// head-to-tail transcode.
func (t *TranscoderService) EnsureJobFrom(ctx context.Context, mediaID string, startSec float64) (string, error) {
if !t.cfg.Transcoder.Enabled {
return "", ErrTranscodeDisabled
}
if startSec < 0 {
startSec = 0
}
m, err := t.repo.Media.FindByID(ctx, mediaID)
if err != nil {
return "", err
@@ -106,11 +121,16 @@ func (t *TranscoderService) EnsureJob(ctx context.Context, mediaID string) (stri
if m == nil {
return "", ErrMediaNotFound
}
t.mu.Lock()
if _, ok := t.jobs[mediaID]; ok {
t.touchJobLocked(mediaID)
t.mu.Unlock()
return t.PlaylistPath(mediaID), nil
if existing, ok := t.jobs[mediaID]; ok {
if sameHLSStart(existing.startSec, startSec) {
t.touchJobLocked(mediaID)
t.mu.Unlock()
return t.PlaylistPath(mediaID), nil
}
existing.cancel()
delete(t.jobs, mediaID)
}
t.mu.Unlock()
@@ -118,27 +138,34 @@ func (t *TranscoderService) EnsureJob(ctx context.Context, mediaID string) (stri
if err != nil {
return "", err
}
input.StartSec = startSec
t.maybeFillDuration(ctx, m, input)
if _, err := t.resolveFFmpegPath(); err != nil {
return "", err
}
outDir := t.HLSDir(mediaID)
// Wipe prior segments so a mid-file restart cannot serve stale early chunks.
_ = os.RemoveAll(outDir)
if err := os.MkdirAll(outDir, 0o750); err != nil {
return "", err
}
t.mu.Lock()
if _, ok := t.jobs[mediaID]; ok {
t.touchJobLocked(mediaID)
t.mu.Unlock()
return t.PlaylistPath(mediaID), nil
if existing, ok := t.jobs[mediaID]; ok {
if sameHLSStart(existing.startSec, startSec) {
t.touchJobLocked(mediaID)
t.mu.Unlock()
return t.PlaylistPath(mediaID), nil
}
existing.cancel()
delete(t.jobs, mediaID)
}
if max := t.maxConcurrent(); max > 0 && len(t.jobs) >= max {
t.mu.Unlock()
return "", ErrTranscodeBusy
}
outDir := t.HLSDir(mediaID)
if err := os.MkdirAll(outDir, 0o750); err != nil {
t.mu.Unlock()
return "", err
}
jobCtx, cancel := context.WithCancel(context.Background())
job := &hlsJob{
mediaID: mediaID,
@@ -147,6 +174,7 @@ func (t *TranscoderService) EnsureJob(ctx context.Context, mediaID string) (stri
startedAt: time.Now(),
lastAccess: time.Now(),
encoder: t.effectiveEncoder(),
startSec: startSec,
}
t.jobs[mediaID] = job
t.mu.Unlock()
@@ -156,6 +184,14 @@ func (t *TranscoderService) EnsureJob(ctx context.Context, mediaID string) (stri
return t.PlaylistPath(mediaID), nil
}
func sameHLSStart(a, b float64) bool {
const tol = 0.75
if a < b {
return b-a < tol
}
return a-b < tol
}
// SetStrmPlayTargetResolver wires STRM URL resolution so ffmpeg can transcode
// remote .strm media (HTTP 直链 or local source path) after direct play fails.
func (t *TranscoderService) SetStrmPlayTargetResolver(resolve func(ctx context.Context, raw string) (*StrmPlayResult, error)) {
@@ -165,6 +201,40 @@ func (t *TranscoderService) SetStrmPlayTargetResolver(resolve func(ctx context.C
t.strmResolve = resolve
}
// SetProbe wires ffprobe so STRM/HLS jobs can persist source duration when the
// media row still has duration_sec=0 (common for .strm that was never probed).
func (t *TranscoderService) SetProbe(probe *FFprobeService) {
if t == nil {
return
}
t.probe = probe
}
func (t *TranscoderService) maybeFillDuration(ctx context.Context, m *model.Media, input transcodeInput) {
if t == nil || t.probe == nil || m == nil || m.DurationSec > 0 || strings.TrimSpace(input.Source) == "" {
return
}
var (
res *ProbeResult
err error
)
if isHTTPSource(input.Source) {
res, err = t.probe.ProbeHTTP(ctx, input.Source, input.Headers)
} else {
res, err = t.probe.Probe(ctx, input.Source)
}
if err != nil || res == nil || res.DurationSec <= 0 {
return
}
m.DurationSec = res.DurationSec
if t.repo == nil || t.repo.DB == nil {
return
}
if err := t.repo.DB.WithContext(ctx).Model(&model.Media{}).Where("id = ?", m.ID).Update("duration_sec", res.DurationSec).Error; err != nil && t.log != nil {
t.log.Debug("persist probed duration failed", zap.String("media_id", m.ID), zap.Error(err))
}
}
func (t *TranscoderService) resolveTranscodeInput(ctx context.Context, m *model.Media) (transcodeInput, error) {
if m == nil {
return transcodeInput{}, ErrMediaNotFound