← Documentation ・ ← SmartCut ・ 日本語
tests/run_tests.sh — decodes the output, matches it against the source's frame
hashes, and checks the frame count, the alignment, and how many frames are
bit-exact.
| Case | Lossless copy ratio |
|---|---|
| H.264 single interval | 180/222 (81.1 %) |
| H.264 multiple intervals | 300/336 (89.3 %) |
| H.264 middle removed (through to the end) | 540/540 (100 %) |
| H.264 exactly on access points | 180/180 (100 %) |
| H.264 interval shorter than a GOP | 0/21 — falls back to a full re-encode |
| HEVC | 300/342 (87.7 %) |
| H.264 29.97 fps | 300/342 (87.7 %) |
| H.264 open GOP (referenced leading pictures) | 0/342 — rejected as a start point (correct behaviour) |
| MPEG-2 TS open GOP | 328/342 (95.9 %) |
| MPEG-2 TS multiple intervals | 296/299 (99.0 %) |
| MPEG-2 TS through to the end | 447/449 (99.6 %) |
| Matroska output | 180/222 (81.1 %) |
All 13 cases agree on both frame count and alignment.
Synthetic fixtures hide certain problems, so validation also runs against actual
broadcast recordings. tests/verify_real.py <src> <out> <ranges> checks frame
count, alignment, bit-exact ratio, timeline, interlacing and A/V length
difference in one go.
| Material | Result |
|---|---|
| Terrestrial NHK E-Tele (MPEG-2 1440x1080i, true 29.97) | 899/899, 98.2 % lossless, timeline matches, interlacing preserved, A/V 2.0 ms |
| BS11 (MPEG-2 1920x1080i) | 899/899, 98.2 % lossless, same as above |
| AT-X (MPEG-2 1440x1080, 2:3 pulldown) | 719/719, 99.9 % lossless, 2:3 pattern preserved, A/V 2.0 ms |
| H.264 720p from YouTube (29.24 fps) | 878 frames, A/V 2.6 ms |
| VP9 + Opus (webm→mp4) | Passes if the plan is copy-only |
Everything a real TS throws at you turned up:
- A
start_timeof 29288 seconds (PCR is based on wall-clock time) - Every access point an open GOP (776 out of 776; the leading pictures are droppable)
- Missing frames (28 of them in a 668 MB recording, from dropouts)
- Multiple coexisting streams — ARIB subtitles, data broadcasting and so on
The same count the video is judged by, applied to the audio: walk the output's ADTS frame by frame and compare each with the recording's own bytes. Nippon TV recording, two intervals, four boundaries:
| Mode | Container | Frames verbatim | Re-encoded | ADTS |
|---|---|---|---|---|
copy |
TS | 5606/5606 (100%) | 0 | all MPEG-2 LC |
smart |
TS | 5602/5606 (99.929%) | 4 | all MPEG-2 LC |
smart |
MP4 | 5602/5606 (payloads) | 4 | — |
reencode |
TS | 0/5607 | 5607 | all MPEG-2 LC |
smart --aac mpeg4 |
TS | 5602/5606 | 4 | all MPEG-2 (see below) |
reencode --aac mpeg4 |
TS | 0/5607 | 5607 | all MPEG-4 |
smart (a commercial block cut out, all four boundaries in silence) |
TS | 1392/1392 (100%) | 0 | all MPEG-2 LC |
As long as frames are being copied, asking for an AAC the recording does not
carry cannot be honoured -- honouring it would make a stream that is two kinds
of AAC at once -- so the request is refused with a note and the recording's own
version followed. A whole-track re-encode copies nothing, so there it is
honoured (the reencode --aac mpeg4 row).
The smart row with the commercial block cut out is what an ordinary cut looks
like, and there the output is byte-identical to copy.
The last row is what checks how --aac behaves: only the frames this tool
writes change, and the copied frames keep their own headers.
An MP4 keeps the payloads and throws the ADTS framing away, so payloads are what can be compared there. Same payload, same sound.
Only the re-encoded partial GOPs came out with interlaced_frame=0, so the
combing disagreed with the copied parts. The cause was not passing
AV_CODEC_FLAG_INTERLACED_DCT / INTERLACED_ME and the field order to the
encoder. On broadcast material this is fatal, because picture quality changes at
every cut point. Fixed.
Most anime carries 24 fps film material in a 29.97 stream via
repeat_first_field. The decoded pictures therefore arrive with alternating
intervals of 0.0334 s and 0.0500 s — per picture, the stream is not CFR.
Measured across the library, 13 of 40 files (32 %) were pulldown:
| Station | Pulldown ratio |
|---|---|
| AT-X | 8/14 (57 %) |
| Disney Channel | 2/2 (100 %) |
| BS Animax | 3/10 (30 %) |
| BS11 / BS-TBS / BS Nittele / NHK / Kids / Tele-Asa ch2 | 0 % |
Too common to ignore, so it is handled. The key is to drop the unit of the
output timeline from the frame to the field. Pulldown alternates two fields and
three fields, so on a field grid it is expressible in integers. The output time
base is 1/(2 × fps_numerator) (1/60000 for 29.97), which makes one field
exactly fps_denominator ticks.
As a side effect, the way DTS is built had to change too. Summing each
picture's duration in decode order overtakes presentation once field durations
vary, and the muxer rejects it with pts < dts. It was replaced with the correct
construction, deriving DTS from the position in display order.
Interval boundaries used to be snapped to an ideal grid, round(t*fps)/fps. That
disagreed with the real stream's frame phase (0.010 s off on the test material)
and dropped one frame at the interval edges.
Removing the snapping fixed it. Segment durations are now measured from the pictures actually written rather than from the planner's arithmetic, so there is nothing left to align to a grid. Each segment is placed relative to its own first picture, and the next segment starts after the length the previous segment actually occupied. That follows both the phase and the pulldown automatically.
These only surfaced on real material:
- The ground truth was being sliced by frame number. Real recordings drop
frames, so
frame number = time × fpsdoes not hold, and the comparison target was 27 frames off — which looked like "0 % match". Fixed to work from PTS. - The time origin differed. Cuts are relative to the presentation timeline
(the format's
start_time, the position a player shows as 00:00), but ffmpeg's decoded output is relative to the video stream'sstart_time. In broadcast recordings audio starts first, so the two differ by 0.346 s. Correction added.
- The first frame is 13 ms early. A raw ES carries no timestamps at all
(every packet is
N/A), so ffmpeg synthesises them from-rand the POC. In doing so, the firsthas_b_framespackets come out with no PTS. Only the MP4 muxer tolerates that (-avoid_negative_ts make_zero); Matroska and MPEG-TS reject it, which is why MKV output is remuxed via MP4. From the second frame on the spacing is perfectly uniform. This does not happen in the libav implementation, which assigns PTS/DTS per packet itself. - An H.264/HEVC open GOP whose leading pictures are reference pictures cannot be used as a copy start point (#3). This is inherent: avoiding it would mean keeping the leading pictures in the bitstream and hiding them with an edit list, which the ES-concatenation approach cannot express. It is a non-issue on material with regular IDRs, which covers most broadcast H.264.
- The leading-picture reference test samples one place in the file and applies
the result to the whole thing (assuming the encoder does not change its mind
partway). The libav implementation does not need this, since
nal_ref_idccan be read directly while demuxing. - H.264 / HEVC pulldown (the SEI
pic_struct) is not detected. Japanese broadcast H.264 is CFR, so no actual harm has been observed, but unlike MPEG-2 it is not detected. - The Python reference implementation still snaps to the ideal grid and therefore
still has the phase problem (
mpeg2 ts multiintests/run_tests.shis an xfail). It still serves as a test oracle, but the Rust implementation is ahead of it. - Supported codecs are H.264 / HEVC / MPEG-2 / MPEG-4 Part 2. VP9 and AV1 have no ES-concatenation form and would need a different design.
- One video track only, and in the Python reference implementation one audio
track only. The Rust engine reads every sound track the recording carries and
writes them all, and carries the ARIB caption stream across when writing a
.ts(see the Rust core and broadcast workflow compatibility); other subtitle formats, PGS among them, are not handled. - The audio boundary is still rounded to a whole frame in every mode but
reencode. Whatsmartremoves is the audio from the far side of a cut being left in the seam; it does not change an interval being a whole number of frames long. As long as no container can put two frames at one instant, a seam gains up to 10.7 ms of silence or loses up to 10.7 ms of sound. - The frames written here carry no ADTS CRC (
protection_absent = 1). It is a per-frame field, so they sit legally among frames that have one, but they are not byte-for-byte the same shape as the recording's. smartgoes as far as AAC and no further. A replacement frame has to cover the same samples the recording's frame did, and whether it can comes down to the encoder's delay being a whole number of frames. AAC's is 1024, exactly one frame, so its packets land on the same grid. AC-3's is 256, which puts every packet it makes 256 samples off that grid; MP2's encoder will not take planar float at all. Both are caught before the run, said out loud and copied instead of quietly doing nothing -- so on such material the default behaves exactly ascopydoes.--aacreaches only the frames this tool writes. Copied frames keep their own ADTS headers, so material cannot be converted from one to the other: the payload may use tools the other version does not have, and flipping the bit alone would be a lie. A request that disagrees with the recording is refused with a note rather than producing a mixed stream -- except under a whole-track re-encode, where nothing is copied and it is honoured.