You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Adding a single video file with an unsupported chroma subsampling (in my case H.264 High 4:4:4 Predictive, yuv444p) to Saved GIFs causes the entire GIF panel to permanently hang and pin the CPU at ~100% (device heats up) from the moment the panel is opened. The condition never recovers — not by closing the panel, not by navigating away, not by waiting — only force-quitting the app stops it. Removing the file from Saved GIFs fully resolves the issue.
Environment
Device: iPhone 15 Pro Max
iOS: 26.5.2 (23F84)
Telegram: 12.9.2 (34474)
Attached: crash/hang report (.ips, RUNNINGBOARD SIGKILL, 0xdeadfa1d) captured while reproducing
Attached: the exact offending video file
Root cause (traced in source)
submodules/FFMpegBinding/Sources/FFMpegAVFrame.m — -[FFMpegAVFrame pixelFormat] only recognizes AV_PIX_FMT_YUV420P / YUVJ420P / YUVA420P. Any other pixel format (4:2:2, 4:4:4, 10-bit, etc.) falls into default: return FFMpegAVFramePixelFormatUnsupported.
submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift — isPlanarYUV420Safe(_:) correctly rejects .Unsupported frames, so convertVideoFrame(...) returns nil for every single frame of an unsupported-format file. This is working as intended defensively (per the comment referencing a prior YUV422P fix) — the frame is just never converted.
FFMpegFileReader.readFrame() — since decoder.decode() never returns a frame, the read loop correctly drains all packets and returns .endOfStream once the file is exhausted. This part behaves correctly.
The actual bug — submodules/TelegramUI/Components/BatchVideoRendering/Sources/BatchVideoRenderingContext.swift, ReadingContext.advance():
switch reader.readFrame(){caselet.frame(frame):returncreateSampleBuffer(...)case.error:self.isFailed =truebreak outer
case.endOfStream:self.reader =nil // <-- BUG: does not set isFailed, does not break
case.waitingForMoreData:self.isFailed =truebreak outer
}
.error and .waitingForMoreData correctly set isFailed = true and exit the outer while true loop. .endOfStream does neither — it just discards the reader and lets the outer loop immediately create a brand-new FFMpegFileReader, re-open the file, re-decode every packet from scratch, fail every frame conversion again, hit .endOfStream again, and repeat — forever, with no exit condition, entirely inside one synchronous call to advance().
Because advance() is called from BatchVideoRenderingContext.sharedQueue (a single serial queue shared by every visible GIF/sticker target), this infinite loop:
Pins one CPU core at 100% indefinitely (device heats up)
Permanently blocks the shared queue, so no other visible tile's frames are ever processed again — the entire panel freezes, not just the offending tile
Cannot be recovered from at the Swift level (shouldBeAnimating = false on panel close has no effect, since the offending synchronous call on the shared queue never returns) — only killing the process frees it
Any file whose video stream uses a pixel format outside YUV420P/YUVJ420P/YUVA420P (4:2:2, 4:4:4, 10-bit variants, etc.) should reproduce this deterministically, regardless of resolution or bitrate — my repro file is only 400x246 @ 509kbps, so this is not a resource/size issue, it's a logic bug.
Steps to reproduce
Add the attached video file to Saved GIFs (e.g. forward to Saved Messages, save as GIF).
Open the GIF panel in the chat attachment tray.
Observe: CPU pins near 100%, device begins heating within seconds.
Close the panel / leave the chat / background the app — the condition does not clear.
Force-quit the app — this is the only way to stop it.
Remove the file from Saved GIFs — panel behaves normally again.
Suggested fix
In ReadingContext.advance(), treat .endOfStream the same defensively as .error/.waitingForMoreData when zero frames have ever been successfully produced by the reader — set isFailed = true and break instead of silently recreating the reader. A minimal fix:
case .endOfStream:if framesProducedByThisReader ==0{self.isFailed =truebreak outer
}self.reader =nil
Suggested follow-up improvements (secondary, not blocking the crash fix)
Per-target decode watchdog / timeout: no single file should be able to block the shared serial decode queue indefinitely. Consider a max wall-clock budget per advance() call, or moving decode for each target off the single shared queue onto a bounded concurrent pool so one bad file can't starve every other visible tile.
Decoded-frame caching for short loops: GifVideoLayer/BatchVideoRenderingContext currently re-decodes every frame from disk on every loop of a Saved GIF (ReadingContext is recreated from scratch each time .endOfStream is hit even in the working case). For short clips (Saved GIFs are typically a few seconds), caching decoded frames after the first loop would avoid continuous CPU cost for content that never changes, instead of paying full software-decode cost on every repeat.
A working reference already exists in this codebase: submodules/AnimatedStickerNode/Sources/VideoStickerFrameSource.swift (used for video/Premium stickers everywhere in chats/channels) handles this correctly — takeFrame() is called once per animation tick (not in an internal unbounded loop), and it already caches decoded frames via cache?.storeUncompressedRgbFrame/readUncompressedYuvaFrame so subsequent loops read from cache instead of re-decoding. BatchVideoRenderingContext could adopt the same pattern instead of introducing a new one.
Crash/hang telemetry for this class of failure: since this SIGKILL is a plain RunningBoard "unresponsive" kill (not a Swift/ObjC exception), it likely doesn't get automatically triaged as a Telegram-specific bug by any internal crash-grouping — consider detecting "zero frames produced, high loop count" as a distinct signal worth reporting client-side.
Attachments
Offending video file (H.264, High 4:4:4 Predictive, yuv444p, 400x246, 15.12s, 378 frames)
.ips hang/crash report from the device
Workaround
Re-encoding the file to H.264 yuv420p (standard 4:2:0 chroma) before adding it to Saved GIFs avoids the crash entirely.
1482267764-364e4719925fe8cc082f5a4e3adadf59.mp4
Telegram-2026-07-21-222521.txt
Summary
Adding a single video file with an unsupported chroma subsampling (in my case H.264 High 4:4:4 Predictive,
yuv444p) to Saved GIFs causes the entire GIF panel to permanently hang and pin the CPU at ~100% (device heats up) from the moment the panel is opened. The condition never recovers — not by closing the panel, not by navigating away, not by waiting — only force-quitting the app stops it. Removing the file from Saved GIFs fully resolves the issue.Environment
.ips, RUNNINGBOARD SIGKILL, 0xdeadfa1d) captured while reproducingRoot cause (traced in source)
submodules/FFMpegBinding/Sources/FFMpegAVFrame.m—-[FFMpegAVFrame pixelFormat]only recognizesAV_PIX_FMT_YUV420P/YUVJ420P/YUVA420P. Any other pixel format (4:2:2, 4:4:4, 10-bit, etc.) falls intodefault: return FFMpegAVFramePixelFormatUnsupported.submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift—isPlanarYUV420Safe(_:)correctly rejects.Unsupportedframes, soconvertVideoFrame(...)returnsnilfor every single frame of an unsupported-format file. This is working as intended defensively (per the comment referencing a prior YUV422P fix) — the frame is just never converted.FFMpegFileReader.readFrame()— sincedecoder.decode()never returns a frame, the read loop correctly drains all packets and returns.endOfStreamonce the file is exhausted. This part behaves correctly.The actual bug —
submodules/TelegramUI/Components/BatchVideoRendering/Sources/BatchVideoRenderingContext.swift,ReadingContext.advance():.errorand.waitingForMoreDatacorrectly setisFailed = trueand exit the outerwhile trueloop..endOfStreamdoes neither — it just discards the reader and lets theouterloop immediately create a brand-newFFMpegFileReader, re-open the file, re-decode every packet from scratch, fail every frame conversion again, hit.endOfStreamagain, and repeat — forever, with no exit condition, entirely inside one synchronous call toadvance().Because
advance()is called fromBatchVideoRenderingContext.sharedQueue(a single serial queue shared by every visible GIF/sticker target), this infinite loop:shouldBeAnimating = falseon panel close has no effect, since the offending synchronous call on the shared queue never returns) — only killing the process frees itAny file whose video stream uses a pixel format outside
YUV420P/YUVJ420P/YUVA420P(4:2:2, 4:4:4, 10-bit variants, etc.) should reproduce this deterministically, regardless of resolution or bitrate — my repro file is only 400x246 @ 509kbps, so this is not a resource/size issue, it's a logic bug.Steps to reproduce
Suggested fix
In
ReadingContext.advance(), treat.endOfStreamthe same defensively as.error/.waitingForMoreDatawhen zero frames have ever been successfully produced by the reader — setisFailed = trueand break instead of silently recreating the reader. A minimal fix:Suggested follow-up improvements (secondary, not blocking the crash fix)
advance()call, or moving decode for each target off the single shared queue onto a bounded concurrent pool so one bad file can't starve every other visible tile.GifVideoLayer/BatchVideoRenderingContextcurrently re-decodes every frame from disk on every loop of a Saved GIF (ReadingContextis recreated from scratch each time.endOfStreamis hit even in the working case). For short clips (Saved GIFs are typically a few seconds), caching decoded frames after the first loop would avoid continuous CPU cost for content that never changes, instead of paying full software-decode cost on every repeat.submodules/AnimatedStickerNode/Sources/VideoStickerFrameSource.swift(used for video/Premium stickers everywhere in chats/channels) handles this correctly —takeFrame()is called once per animation tick (not in an internal unbounded loop), and it already caches decoded frames viacache?.storeUncompressedRgbFrame/readUncompressedYuvaFrameso subsequent loops read from cache instead of re-decoding.BatchVideoRenderingContextcould adopt the same pattern instead of introducing a new one.Attachments
.ipshang/crash report from the deviceWorkaround
Re-encoding the file to H.264
yuv420p(standard 4:2:0 chroma) before adding it to Saved GIFs avoids the crash entirely.