Historical recipe — not current setup guidance. Measurements and implementation links below belong to their original workload and revision. Deployment labels and defaults may be obsolete. Use the reviewed performance recipes; browse historical evidence.
AVPlumber · Mixer optimization cookbook
Looped clips keep counting across the wrap
Implementation links are pinned to develop at c20464a5 (2026-10-09). The mixer has changed since; this page was checked against develop at 8fa15c92 on 2026-10-13. Measurements retain their original workload and scope. Watch the current 192-input L4 demo.
Current scope. Continuous loop timestamps remain useful with native-rate input pacing. References below to force_fps describe the historical normalized chain; current mixer file inputs feed realtime directly to the downstream mixer graph.
A looped file used to restart at its first timestamp on every pass, and each wrap cost the source a frame. With loop_continuous_ts, input_rec shifts each new pass by the length of the one before, so timestamps keep rising and a wrap looks like any other frame.
realtime resyncs and the source loses a frame. Green: pass 2 carries the file's timestamps + 2 s and pass 3 + 4 s, so the line never steps back. In pass 1 the two lines are the same.What went wrong at the wrap
File sources from a mixer config loop by default (--input URLs loop with --loop-inputs), and each generated source loops its own clip, 2 s long unless the recipe says otherwise (see Real 1080p inputs). At the end of the file input_rec seeks back to the start, and the next packets carry the file's first timestamps again.
realtime(set_pts) releases each frame when its timestamp is due. The first frame of a new pass looked one loop length overdue, so realtime resynced and released it at once, one frame period early. force_fps then got two frames for one output tick and dropped one: a stutter at every wrap, on every looped file source.
A mixer run with 2 s generated clips, measured when the change was made, showed it: force_fps estimated the input at 60.5 fps, dropped about 0.9 frames per second per source, and every wrap logged three warnings. These numbers come from that run, not from the smoke test below or the 110-source show.
How the shift works
- Measure each pass. For every packet,
input_rectracks the earliest timestamp and the latest timestamp + duration of the video stream, or of all streams in a file without video. It reads the PTS (the DTS when there is no PTS) and keeps the minimum and maximum over the whole pass, not the last packet's value, so B-frame reordering does not shorten the span. A packet without a duration counts as one frame at the stream's average frame rate, when the file declares one. - Add the span at each forward wrap. Both loop points, end of file and
stop_ts, go throughloopForward(), which adds the finished pass's span to a runningloop_offset_and then seeks tostart_ts. A 2 s clip at 25 fps runs from PTS 0 to 1.96 s, and its last frame lasts 0.04 s: span 2 s. Pass 2 is shifted by 2 s, pass 3 by 4 s. - Shift every stream by the same offset. Just before a packet leaves
input_rec, its PTS and DTS getloop_offset_. Audio gets the video's offset, so audio and video stay in sync. - Control stays on file timestamps.
stop_tsand seek-at entries compare the file's own timestamps, taken before the shift, and seek targets still resolve in the file. Only forward loops add to the offset; a reverse-playback loop adds nothing.
Where the mixer turns it on
- NVDEC file sources. Both mixer entry points,
--inputURLs and sources from a config, callbuild_input(…, continuous_loop=True).input_recgetsloop_continuous_tsonly when the source also loops. - Raw v210 files, and raw NV12/P010 files on the
pinnedpath._raw_file_packetssetsloop_continuous_tsto the source'sloop. Rawvideo decode and the patched FFmpeg upload filter preserve the packet timeline, so the shift reachesrealtime. - Raw NV12/P010 files on the
directpath, which the setup selects.raw_video_file_cudadoes not useinput_rec; it takes the same parameter,loop_continuous_ts, set to the source'sloop, and counts pictures on across passes. - Unchanged. The default raw NV12/P010 chain (
hwupload) already counts frames withsetptsbefore pacing, so its timestamps were already continuous. The playlist and replay demos seek, so they keep the default. Browser sources anddirectraw sources do not useinput_rec.
At a glance
- What
input_recgainsloop_continuous_ts(default false). Each forward loop adds the previous pass's span to a running offset, and every packet's PTS and DTS carry it. The mixer turns it on for its free-running file loops: NVDEC sources, v210 files and raw NV12/P010 files (throughinput_recon thepinnedpath, through the same parameter ofraw_video_file_cudaondirect). - Why
A wrap sent the timestamps back one loop length.
realtimeresynced and released the next pass's first frame a frame early, so each looped source lost a frame and stuttered at every wrap: in a mixer run with 2 s clips, about 0.9 drops/s per source and three log warnings per wrap.In normalized helper chains it also helps
force_fps.center_phase, which picks its phase on the first frame and again only after a discontinuity it detects. A resync restartsrealtime's release times at a new phase, which could leave a source off-center after a wrap. With nothing moving at a wrap, the chosen phase holds across passes. - Latency0 frames added to program and to command-to-screen latency: no queue, buffer, wait or node. A wrap no longer releases a frame early.
- Side effectsTimestamps of a looped input keep rising instead of repeating the file's range. Only the nodes between
input_recandrealtime(demux, the decoder or raw upload node, any decoded filter) see shifted timestamps;realtime(set_pts)restamps every frame onto the host clock, so nothing after it changes. A chain that seeks would aim at file timestamps while its packets carry shifted ones, which is why seeking chains keep the default. - Cost0 nodes, 0 queues, 0 VRAM, no new thread or lock. Per packet, a few timestamp comparisons and additions. Code: +54/−3 lines in
input_rec.cppand a 141-line smoke test. - RiskLow. The span follows the video stream, by design: a file whose audio runs a frame longer or shorter than its video still gets the video's offset, so its audio overlaps or leaves a gap at the wrap by that difference. The mixer's file sources route only video. Not for chains that seek.
- Switch it offPass
continuous_loop=Falseat the relevantbuild_inputcalls inapplication.pyorcli.py, and setloop_continuous_tsto false in_raw_file_packetsfor the v210 andpinnedchains and in thedirectbranch ofbuild_raw420_input. On a hand-built graph, leaveloop_continuous_tsat its default, false. No runtime switch.
Where in the code
src/nodes/input_rec.cpp: the flag,loop_offset_and the current pass's start and end.src/nodes/input_rec.cpp:loopForward()adds the pass span to the offset, then seeks tostart_ts.src/nodes/input_rec.cpp:continueLoopTimeline()measures the pass on the video stream and shifts PTS and DTS.src/nodes/input_rec.cppand887-890: end of file andstop_tsboth loop throughloopForward().src/nodes/input_rec.cpp: the file PTS is kept for seek-at entries (source), and the shift runs just before the packet is sent.src/nodes/input_rec.cpp: theloop_continuous_tsparameter.pyplumber/mixer/inputs.py:build_inputsetsloop_continuous_tstoloop and continuous_loop.pyplumber/mixer/inputs.py:_raw_file_packets, the head of the v210 and pinned raw chains.pyplumber/mixer/inputs.pyandsrc/nodes/hwaccel/raw_video_file_cuda.cpp(pinned todevelopat 8fa15c92): thedirectbranch ofbuild_raw420_inputand the node's ownloop_continuous_ts.pyplumber/mixer/inputs.py: the default raw chain counts frames withsetptsinstead.pyplumber/mixer/application.pyand729-731: the mixer's twobuild_inputcalls passcontinuous_loop=True.pyplumber/mixer/config.py: config sources loop unlessloopis false;demos/mixer/prepare_demo.py: generated clips last 2 s by default.tests/smoke_input_loop.py: the default loop repeats its timestamps; continuous mp4 (B-frames + AAC) and rawvideo step one frame period across wraps; paced throughrealtime(set_pts), the default loop shows early steps and the continuous one stays within 4 ms of the period.
Markdown reference: NODES.md · input_rec documents loop_continuous_ts.
Source links are pinned to the reviewed revision.