Fix mobile black-screen-in-fullscreen: normalize negative DTS on remux

The faststart fix (f8842f8) handled the moov-atom position but missed a
second issue in the same merged video+audio stream: the video track's first
packet has a slightly negative DTS (from B-frame reordering across the
merge), confirmed via ffprobe on the actual failing video (-0.083322s).
Reported symptom after the faststart fix was already live: preview thumbnail
fine, black screen on opening fullscreen on Telegram's mobile app, but
picture visible in PiP mode - consistent with the hardware-accelerated
fullscreen decoder path choking on it while PiP's path (apparently software)
tolerates it. Desktop played it fine regardless.

Adds -avoid_negative_ts make_zero to the same lossless -c copy remux pass
(no extra cost - it's the same command). Verified on the actual video:
first packet DTS goes from -0.083322 to -0.000322 (down to B-frame rounding
noise, not a real negative start anymore).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-09 19:49:34 +05:00
parent 5549b7d49b
commit e875475f88
+13 -7
View File
@@ -172,18 +172,24 @@ class MediaProcessor:
async def _apply_faststart(self, path: Path) -> Path: async def _apply_faststart(self, path: Path) -> Path:
"""Moves the moov atom to the front with a fast, lossless remux (-c copy - no """Moves the moov atom to the front with a fast, lossless remux (-c copy - no
re-encode, just repositions the index). Needed because muxing a separate re-encode, just repositions the index and timestamps). Needed because muxing a
video+audio stream (see _select_format's "+bestaudio" case) leaves the moov atom separate video+audio stream (see _select_format's "+bestaudio" case) leaves the
at the end by default: confirmed on a real post where the result played audio moov atom at the end by default, AND leaves the video track starting at a
with a black video, because streaming players (notably Telegram's mobile clients) slightly negative DTS (its first frame is a B-frame reordered before the
can't render video without seeking to the index first. Falls back to the original keyframe's PTS) - confirmed on a real post where the result played audio with a
file - rather than dropping the video - if ffmpeg fails for any reason.""" black video on Telegram's mobile app in fullscreen (but rendered fine in PiP,
which apparently tolerates it - a strong hint the hardware decoder path is what
chokes on it) while desktop played it fine either way. -avoid_negative_ts
make_zero fixes the second half; without it, moov-only faststart wasn't enough.
Falls back to the original file - rather than dropping the video - if ffmpeg
fails for any reason."""
fixed_path = path.with_name(f"{path.stem}_fs.mp4") fixed_path = path.with_name(f"{path.stem}_fs.mp4")
await register_active_path(fixed_path) await register_active_path(fixed_path)
proc = None proc = None
try: try:
proc = await asyncio.create_subprocess_exec( proc = await asyncio.create_subprocess_exec(
"ffmpeg", "-y", "-i", str(path), "-c", "copy", "-movflags", "+faststart", "ffmpeg", "-y", "-i", str(path), "-c", "copy",
"-avoid_negative_ts", "make_zero", "-movflags", "+faststart",
str(fixed_path), str(fixed_path),
stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL, stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL,
) )