feat: add animated GIF export to record-video - #84
Conversation
record-video gains --format mp4|gif (also inferred from a .gif --output extension; explicit --format wins with a stderr note on mismatch) on all three surfaces: the top-level cross-platform verb, ios record-video, and android record-video. GIF is a post-step rather than another capture path: none of the capture pipelines (idb's in-process recorder on iOS, adb screenrecord passthrough on Android, the screenshot/screencap fallbacks) expose decoded frames in-process, and the signal-to-finalise path is latency-critical. The recording lands in an intermediate MP4 first; on stop, GIFTranscoder (SimUseVideo) decodes it with AVAssetReader and encodes a looping GIF via CGImageDestination. Two decoding passes bound peak memory to one frame: pass 1 collects display-timeline timestamps (CGImageDestination needs the image count at creation; passthrough PTS carry B-frame reorder offsets and un-applied edit lists, so a non-decoding read can't drive the plan), pass 2 appends only the sampled frames. Sampling walks a 1/fps target clock, which also normalizes Android's variable-frame-rate capture; per-frame delays follow the source timestamp gaps (2 cs floor). Format/fps/scale resolution lives in SimUseVideo (ResolvedRecordingOptions) so the surfaces cannot drift: GIF defaults are fps 10 / scale 0.5 (mp4 keeps 30 / 1.0) because full-rate full-scale GIFs are enormous; both remain overridable. A failed transcode preserves the intermediate MP4 and reports its path, so footage is never lost. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: wonseob <wonseob@linecorp.com>
|
The current sampling plan can stretch GIF playback at the documented 50 fps cap. For a 60 fps source with Could we make frame selection and centisecond quantization share a cumulative timing model, so the 20 ms floor does not lose timing debt? Re-selecting frames or carrying the rounding error into subsequent delays would both avoid the stretch. Please also add 60 fps and 120 fps source cases with |
Review follow-up (lycorp-jp#84): with a 60/120 fps source, the target-clock admission let frames closer than the 2 cs GIF floor through, and each gap was then clamped up independently — accumulating ~6.5%/8% playback stretch over a 10 s recording despite the 50 fps cap. Two-part fix, per review: selection now refuses a frame closer than minimumDelay to the previous kept frame (a frame that would need clamping is never chosen), and delays quantize the cumulative timeline to centiseconds so per-frame rounding carries forward instead of accumulating. New parametrized tests pin 60 and 120 fps sources at --fps 60 to centisecond-level total-duration tolerance, and the existing cap test's tolerance is tightened from 0.1 to 0.02. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: wonseob <wonseob@linecorp.com>
|
Confirmed — a code bug, not a doc issue: the admission tolerance let sub-2cs gaps through, and independent clamping accumulated exactly the stretch you measured. Fixed in 35848ce with your cumulative timing model:
Added the requested 60/120 fps source tests (10 s at |
|
@kws0210 Thank you! LGTM and this is really useful! |
Tracking: LINECLIENT-1689 (LY internal)
What
record-videogains--format mp4|gifon all three surfaces (top-level,ios record-video,android record-video).The format is also inferred from a
.gif--outputextension; an explicit--formatwins, with a stderr note on mismatch.sim-use record-video --output demo.gif # inferred; 10 fps / 0.5 scale GIF defaults sim-use record-video --format gif --fps 15 --scale 0.4Why
Animated GIFs auto-play inline in PRs / issues / chat, which makes them the most convenient evidence format for short agent-driven verification flows — today producing one requires an out-of-band ffmpeg conversion.
Design
GIF is a post-stop transcode, not a fourth capture path: none of the capture pipelines (idb's in-process recorder on iOS,
adb screenrecordpassthrough on Android, the screenshot/screencap fallbacks) expose decoded frames in-process, and the signal-to-finalise path is latency-critical. Capture lands in an intermediate MP4; on stop,GIFTranscoder(inSimUseVideo) decodes it withAVAssetReaderand encodes a looping GIF viaCGImageDestination.RecordingOutputPlanowns resolve → warn → prepare → stale-intermediate cleanup → transcode for every surface, so the flag contract can't drift between platforms (same posture asVideoRecordingOptions).Base
Started from
50bc5a0and rebased ontoda63f16(post-v0.12.0), relocating the code into theSimUseVideomodule introduced by #79 — which is also what made theandroid record-videosurface come along for free (as discussed on Slack).Testing
make testfully green (834 unit tests), including 16 newGIFTranscoderTests— sampling-plan properties, format/option resolution, output-plan lifecycle, and a real MP4→GIF round-trip synthesized throughH264StreamRecorder(no simulator needed).Demo
Each recorded against LINE Dev on an iPhone 17 simulator with a single
record-video --output <name>.gifinvocation (touch points enabled in the app):(83 frames, 905 KB)
(68 frames, 626 KB)
🤖 Generated with Claude Code