mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 03:06:38 +08:00
bug处理
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user