Fix mobile black-screen: declared video dimensions didn't match the file

ProcessedMedia.width/height for downloaded videos were always VK's original
source dimensions (e.g. 3840x2160 for a 4K upload), passed straight through
to Telegram's sendVideo width/height params - even though the actual file
sent is capped to video_max_height (e.g. 720p) by _select_format. A client
declaring 4K but receiving a 720p stream is a known cause of black-screen
playback on strict mobile decoders, which can allocate the render surface
from the declared size before the real stream is probed - matches the
reported symptom exactly (fine on desktop and in PiP, black in Telegram
mobile's fullscreen decoder path).

Neither the faststart remux nor the negative-DTS fix addressed this, since
both were about the file's own internal structure, not the metadata
describing it externally.

Adds _probe_video_dims (ffprobe on the actual downloaded/remuxed file) and
uses its real width/height/duration instead of VK's pre-download values.
Verified on the actual failing video: now reports the real 1280x720 instead
of VK's original 3840x2160.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-09 19:56:24 +05:00
parent e875475f88
commit 56f3cfaac2
+36 -3
View File
@@ -207,6 +207,38 @@ class MediaProcessor:
await unregister_active_path(path)
return fixed_path
async def _probe_video_dims(self, path: Path) -> tuple[Optional[int], Optional[int], Optional[float]]:
"""Reads the ACTUAL encoded width/height/duration of a downloaded file via
ffprobe. Telegram was declared VK's original video dimensions (e.g. a 4K
source) while the file we actually send is capped to video_max_height (e.g.
720p) - that mismatch between declared and real dimensions is a known cause of
black-screen playback on strict mobile decoders, since some clients allocate
the render surface from the declared size before the real stream is probed."""
try:
proc = await asyncio.create_subprocess_exec(
"ffprobe", "-v", "error", "-select_streams", "v:0",
"-show_entries", "stream=width,height:format=duration",
"-of", "csv=p=0",
str(path),
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL,
)
out, _ = await proc.communicate()
except OSError:
return None, None, None
lines = [l for l in out.decode().strip().splitlines() if l]
width = height = None
duration = None
for line in lines:
parts = line.split(",")
if len(parts) == 2 and parts[0].isdigit() and parts[1].isdigit():
width, height = int(parts[0]), int(parts[1])
elif len(parts) == 1:
try:
duration = float(parts[0])
except ValueError:
pass
return width, height, duration
async def _download_video_ytdlp_attempt(
self, url: str, prefix: str = "video_"
) -> tuple[Optional[Path], Optional[str], bool]:
@@ -373,6 +405,7 @@ class MediaProcessor:
p, err, perm = await self.download_video_ytdlp(url, prefix=f"v_{att_id}_")
if p:
real_width, real_height, real_duration = await self._probe_video_dims(p)
results.append(
ProcessedMedia(
media_type="video",
@@ -380,9 +413,9 @@ class MediaProcessor:
original_url=url,
attachment_id=att_id,
size_bytes=p.stat().st_size,
duration_sec=item.duration_sec,
width=item.width,
height=item.height,
duration_sec=int(real_duration) if real_duration else item.duration_sec,
width=real_width or item.width,
height=real_height or item.height,
)
)
else: