mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-06 05:16: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 {
|
if i := strings.IndexByte(part, '='); i >= 0 {
|
||||||
key = part[:i]
|
key = part[:i]
|
||||||
}
|
}
|
||||||
if strings.EqualFold(key, "start") {
|
switch strings.ToLower(key) {
|
||||||
|
case "start", "_seek":
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
kept = append(kept, part)
|
kept = append(kept, part)
|
||||||
|
|||||||
@@ -146,8 +146,7 @@ func (t *TranscoderService) EnsureJobFrom(ctx context.Context, mediaID string, s
|
|||||||
|
|
||||||
outDir := t.HLSDir(mediaID)
|
outDir := t.HLSDir(mediaID)
|
||||||
// Wipe prior segments so a mid-file restart cannot serve stale early chunks.
|
// Wipe prior segments so a mid-file restart cannot serve stale early chunks.
|
||||||
_ = os.RemoveAll(outDir)
|
if err := resetHLSDir(outDir); err != nil {
|
||||||
if err := os.MkdirAll(outDir, 0o750); err != nil {
|
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,6 +191,24 @@ func sameHLSStart(a, b float64) bool {
|
|||||||
return a-b < tol
|
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
|
// SetStrmPlayTargetResolver wires STRM URL resolution so ffmpeg can transcode
|
||||||
// remote .strm media (HTTP 直链 or local source path) after direct play fails.
|
// 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)) {
|
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)
|
settings := ffmpegArgSettingsFromConfig(cfg)
|
||||||
video := ffmpegVideoPlanForSettings(settings)
|
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 = appendInputAndVideoArgs(args, input, settings, video)
|
||||||
args = appendOutputHLSArgs(args, settings, segments, playlist)
|
args = appendOutputHLSArgs(args, settings, segments, playlist)
|
||||||
return args
|
return args
|
||||||
@@ -126,12 +129,21 @@ func baseFFmpegArgs(preInput string, realtime bool) []string {
|
|||||||
|
|
||||||
func appendInputAndVideoArgs(args []string, input transcodeInput, settings ffmpegArgSettings, video ffmpegVideoPlan) []string {
|
func appendInputAndVideoArgs(args []string, input transcodeInput, settings ffmpegArgSettings, video ffmpegVideoPlan) []string {
|
||||||
args = append(args, ffmpegHTTPInputArgs(input)...)
|
args = append(args, ffmpegHTTPInputArgs(input)...)
|
||||||
// Input seek (-ss before -i) lets mid-file HLS restarts jump without
|
ss := ""
|
||||||
// decoding everything before the click position.
|
|
||||||
if input.StartSec > 0.05 {
|
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" {
|
if settings.threads > 0 && video.codec == "libx264" {
|
||||||
args = append(args, "-threads", strconv.Itoa(settings.threads))
|
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
|
// 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 {
|
func (t *TranscoderService) WaitReady(ctx context.Context, mediaID string, timeout time.Duration) bool {
|
||||||
deadline := time.Now().Add(timeout)
|
deadline := time.Now().Add(timeout)
|
||||||
for {
|
for {
|
||||||
if _, err := os.Stat(t.PlaylistPath(mediaID)); err == nil {
|
t.mu.Lock()
|
||||||
t.mu.Lock()
|
job, ok := t.jobs[mediaID]
|
||||||
if j, ok := t.jobs[mediaID]; ok {
|
var started time.Time
|
||||||
j.playlistOK = true
|
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 {
|
if time.Now().After(deadline) || ctx.Err() != nil {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -212,13 +212,43 @@ func TestBuildFFmpegArgsInputSeekBeforeDashI(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if idxSS < 0 || idxI < 0 || idxSS > idxI {
|
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" {
|
if args[idxSS+1] != "125.500" {
|
||||||
t.Fatalf("start = %q", args[idxSS+1])
|
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) {
|
func TestSameHLSStart(t *testing.T) {
|
||||||
if !sameHLSStart(10, 10.2) {
|
if !sameHLSStart(10, 10.2) {
|
||||||
t.Fatal("expected close starts to match")
|
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.
|
// hlsURL returns the m3u8 playlist URL fed into hls.js.
|
||||||
// startSec > 0 asks the server to (re)start ffmpeg from that source offset.
|
// startSec > 0 asks the server to (re)start ffmpeg from that source offset.
|
||||||
export function hlsURL(mediaId: string, startSec = 0): string {
|
export function hlsURL(mediaId: string, startSec = 0): string {
|
||||||
const start = startSec > 0.05 ? `&start=${encodeURIComponent(String(Math.floor(startSec * 1000) / 1000))}` : ''
|
const start =
|
||||||
return `/api/hls/${encodeURIComponent(mediaId)}/index.m3u8?${tokenQuery()}${profileQuery()}${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
|
// 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
|
if (!video) return false
|
||||||
const target = Math.max(0, absoluteSec)
|
const target = Math.max(0, absoluteSec)
|
||||||
const local = target - hlsStartSec
|
const local = target - hlsStartSec
|
||||||
const available = Number.isFinite(video.duration) ? video.duration : 0
|
// Live/EVENT HLS during transcoding often reports duration=Infinity.
|
||||||
// Stay inside the already-transcoded window of this HLS session.
|
// Never treat that as "already buffered" or currentTime seeks reset to 0.
|
||||||
if (local >= 0 && local <= Math.max(0, available - 0.5)) {
|
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
|
video.currentTime = local
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user