bug处理

This commit is contained in:
truewhile
2026-09-07 23:21:24 +08:00
parent c4b8114115
commit 5a47e6f52d
7 changed files with 108 additions and 21 deletions
+2 -1
View File
@@ -109,7 +109,8 @@ func filterHLSSegmentQuery(rawQuery string) string {
if i := strings.IndexByte(part, '='); i >= 0 {
key = part[:i]
}
if strings.EqualFold(key, "start") {
switch strings.ToLower(key) {
case "start", "_seek":
continue
}
kept = append(kept, part)
+19 -2
View File
@@ -146,8 +146,7 @@ func (t *TranscoderService) EnsureJobFrom(ctx context.Context, mediaID string, s
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 {
if err := resetHLSDir(outDir); err != nil {
return "", err
}
@@ -192,6 +191,24 @@ func sameHLSStart(a, b float64) bool {
return a-b < tol
}
func resetHLSDir(dir string) error {
var lastErr error
for i := 0; i < 6; i++ {
lastErr = os.RemoveAll(dir)
if lastErr == nil {
break
}
time.Sleep(50 * time.Millisecond)
}
if err := os.MkdirAll(dir, 0o750); err != nil {
return err
}
// Best-effort: if RemoveAll kept failing on Windows locks, at least drop the playlist
// so WaitReady cannot treat the stale file as belonging to the new job.
_ = os.Remove(filepath.Join(dir, "index.m3u8"))
return nil
}
// 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)) {
+17 -5
View File
@@ -47,7 +47,10 @@ func buildFFmpegArgsForInput(cfg *config.Config, input transcodeInput, playlist,
settings := ffmpegArgSettingsFromConfig(cfg)
video := ffmpegVideoPlanForSettings(settings)
args := baseFFmpegArgs(video.preInput, settings.realtime)
// Mid-file restarts must not use -re: output -ss would otherwise crawl to the
// seek point at 1x wall-clock before emitting the first HLS segment.
realtime := settings.realtime && input.StartSec <= 0.05
args := baseFFmpegArgs(video.preInput, realtime)
args = appendInputAndVideoArgs(args, input, settings, video)
args = appendOutputHLSArgs(args, settings, segments, playlist)
return args
@@ -126,12 +129,21 @@ func baseFFmpegArgs(preInput string, realtime bool) []string {
func appendInputAndVideoArgs(args []string, input transcodeInput, settings ffmpegArgSettings, video ffmpegVideoPlan) []string {
args = append(args, ffmpegHTTPInputArgs(input)...)
// Input seek (-ss before -i) lets mid-file HLS restarts jump without
// decoding everything before the click position.
ss := ""
if input.StartSec > 0.05 {
args = append(args, "-ss", strconv.FormatFloat(input.StartSec, 'f', 3, 64))
ss = strconv.FormatFloat(input.StartSec, 'f', 3, 64)
}
args = append(args, "-i", input.Source, "-map", "0:v:0?", "-map", "0:a:0?", "-vf", video.filter, "-c:v", video.codec)
// Local files: input -ss (byte/keyframe seek). HTTP/STRM: output -ss after
// -i — several cloud/WMV demuxers ignore input seeks and would otherwise
// restart from t=0. Realtime (-re) is already disabled for StartSec > 0.
if ss != "" && !isHTTPSource(input.Source) {
args = append(args, "-ss", ss)
}
args = append(args, "-i", input.Source)
if ss != "" && isHTTPSource(input.Source) {
args = append(args, "-ss", ss)
}
args = append(args, "-map", "0:v:0?", "-map", "0:a:0?", "-vf", video.filter, "-c:v", video.codec)
if settings.threads > 0 && video.codec == "libx264" {
args = append(args, "-threads", strconv.Itoa(settings.threads))
}
+20 -7
View File
@@ -9,17 +9,30 @@ import (
)
// WaitReady blocks (with a deadline) until the playlist file shows up on
// disk. Returns true on success.
// disk for the *current* job generation. Stale playlists left behind by a
// failed RemoveAll / still-exiting ffmpeg must not unblock a mid-file restart.
func (t *TranscoderService) WaitReady(ctx context.Context, mediaID string, timeout time.Duration) bool {
deadline := time.Now().Add(timeout)
for {
if _, err := os.Stat(t.PlaylistPath(mediaID)); err == nil {
t.mu.Lock()
if j, ok := t.jobs[mediaID]; ok {
j.playlistOK = true
t.mu.Lock()
job, ok := t.jobs[mediaID]
var started time.Time
if ok {
started = job.startedAt
}
t.mu.Unlock()
if ok {
if info, err := os.Stat(t.PlaylistPath(mediaID)); err == nil {
// Allow a small clock skew; reject anything older than this job.
if !info.ModTime().Before(started.Add(-2 * time.Second)) {
t.mu.Lock()
if j, exists := t.jobs[mediaID]; exists {
j.playlistOK = true
}
t.mu.Unlock()
return true
}
}
t.mu.Unlock()
return true
}
if time.Now().After(deadline) || ctx.Err() != nil {
return false
+31 -1
View File
@@ -212,13 +212,43 @@ func TestBuildFFmpegArgsInputSeekBeforeDashI(t *testing.T) {
}
}
if idxSS < 0 || idxI < 0 || idxSS > idxI {
t.Fatalf("expected -ss before -i, args=%v", args)
t.Fatalf("expected local -ss before -i, args=%v", args)
}
if args[idxSS+1] != "125.500" {
t.Fatalf("start = %q", args[idxSS+1])
}
}
func TestBuildFFmpegArgsHTTPSeekAfterDashI(t *testing.T) {
cfg := &config.Config{}
cfg.Transcoder.MaxHeight = 720
cfg.Transcoder.SegmentSeconds = 4
cfg.Transcoder.Realtime = true
args := buildFFmpegArgsForInput(cfg, transcodeInput{
Source: "https://cdn.example.com/a.wmv",
StartSec: 90,
}, "/o/x.m3u8", "/o/seg_%05d.ts")
joined := " " + strings.Join(args, " ") + " "
if strings.Contains(joined, " -re ") {
t.Fatalf("seek restart must disable -re, got: %s", joined)
}
idxSS, idxI := -1, -1
for i, arg := range args {
if arg == "-ss" {
idxSS = i
}
if arg == "-i" && idxI < 0 {
idxI = i
}
}
if idxSS < 0 || idxI < 0 || idxSS < idxI {
t.Fatalf("expected http -ss after -i, args=%v", args)
}
if args[idxSS+1] != "90.000" {
t.Fatalf("start = %q", args[idxSS+1])
}
}
func TestSameHLSStart(t *testing.T) {
if !sameHLSStart(10, 10.2) {
t.Fatal("expected close starts to match")
+5 -2
View File
@@ -148,8 +148,11 @@ export function streamURL(mediaId: string): string {
// hlsURL returns the m3u8 playlist URL fed into hls.js.
// startSec > 0 asks the server to (re)start ffmpeg from that source offset.
export function hlsURL(mediaId: string, startSec = 0): string {
const start = startSec > 0.05 ? `&start=${encodeURIComponent(String(Math.floor(startSec * 1000) / 1000))}` : ''
return `/api/hls/${encodeURIComponent(mediaId)}/index.m3u8?${tokenQuery()}${profileQuery()}${start}`
const start =
startSec > 0.05 ? `&start=${encodeURIComponent(String(Math.round(startSec * 1000) / 1000))}` : ''
// Cache-bust so a seek restart cannot reuse a stale start=0 playlist from disk/browser.
const bust = startSec > 0.05 ? `&_seek=${Date.now()}` : ''
return `/api/hls/${encodeURIComponent(mediaId)}/index.m3u8?${tokenQuery()}${profileQuery()}${start}${bust}`
}
// imageURL converts a remote poster URL into a same-origin proxy URL so it
+14 -3
View File
@@ -575,9 +575,20 @@ export function PlayerPage() {
if (!video) return false
const target = Math.max(0, absoluteSec)
const local = target - hlsStartSec
const available = Number.isFinite(video.duration) ? video.duration : 0
// Stay inside the already-transcoded window of this HLS session.
if (local >= 0 && local <= Math.max(0, available - 0.5)) {
// Live/EVENT HLS during transcoding often reports duration=Infinity.
// Never treat that as "already buffered" or currentTime seeks reset to 0.
const finiteDuration = Number.isFinite(video.duration) ? video.duration : 0
let seekableEnd = 0
if (video.seekable && video.seekable.length > 0) {
try {
seekableEnd = video.seekable.end(video.seekable.length - 1)
} catch {
seekableEnd = 0
}
}
if (!Number.isFinite(seekableEnd)) seekableEnd = 0
const windowEnd = Math.max(finiteDuration, seekableEnd)
if (local >= 0 && windowEnd > 0.5 && local <= windowEnd - 0.5) {
video.currentTime = local
return true
}