A disc rip is joined by the wrong ffmpeg mechanism #43

Closed
opened 2026-08-03 20:14:19 +02:00 by Claude · 1 comment
Collaborator

A DVD rip is handed to ffmpeg as -i concat:"a|b" where upstream writes a
concat config file and passes -f concat -safe 0 -i <file>.

Those are two different mechanisms. concat: is a protocol that joins byte
streams; -f concat is a demuxer reading a list of files with durations.
EncodingHelper.GetInputArgument takes the second road for Dvd and BluRay,
building <cache>/concat/<sourceId>.concat through GenerateConcatConfig,
which probes each file for its duration first.

So the argument string this server builds is not upstream's on that branch, and
neither the response harness nor the ffmpeg corpus can see it: the corpus
generator refuses a DVD case outright — DeclaredEncoder throws "undeclared"
for a folder — so there is nothing to diff against.

Porting it wants two things this does not have yet: a probe per playable file
for its duration, and a written file under the cache directory, which is adapter
work rather than argument building.

Wanted first: a rip to play. Nothing in either library here is one, so whether
the protocol form plays a DVD correctly is itself unmeasured.

A DVD rip is handed to ffmpeg as `-i concat:"a|b"` where upstream writes a concat config file and passes `-f concat -safe 0 -i <file>`. Those are two different mechanisms. `concat:` is a protocol that joins byte streams; `-f concat` is a demuxer reading a list of files with durations. `EncodingHelper.GetInputArgument` takes the second road for `Dvd` and `BluRay`, building `<cache>/concat/<sourceId>.concat` through `GenerateConcatConfig`, which probes each file for its duration first. So the argument string this server builds is not upstream's on that branch, and neither the response harness nor the ffmpeg corpus can see it: the corpus generator refuses a DVD case outright — `DeclaredEncoder` throws "undeclared" for a folder — so there is nothing to diff against. Porting it wants two things this does not have yet: a probe per playable file for its duration, and a written file under the cache directory, which is adapter work rather than argument building. Wanted first: a rip to play. Nothing in either library here is one, so whether the protocol form plays a DVD correctly is itself unmeasured.
Author
Collaborator

A rip's files now reach ffmpeg through the concat demuxer, as upstream sends
them: a list written to <cache>/concat/<sourceId>.concat, one stanza per file
naming where it is and how long it plays, passed as -f concat -safe 0 -i.

The protocol form this replaces joined the files as bytes. The demuxer re-bases
timestamps per file from the durations in the list, which the protocol cannot
do — and the selection rule groups by the part number a name ends with, so two
different title sets can land in one group and their timestamps restart at the
boundary.

The review turned up two things worth naming.

The duration was resting on an unverified assumption. It is
TimeSpan.TotalSeconds upstream, and .NET Framework scaled the ticks by a
ten-millionth where modern .NET divides them — the two differ in the last bit
for most values. Every duration in the tests happened to be one where both
agree, so nothing could have caught it either way. Settled by running the real
thing under mcr.microsoft.com/dotnet/sdk:10.0: TimeSpan.FromTicks(12345678901) .TotalSeconds is 1234.5678901, the division. That value is now the test, and
it fails against a reader that scales instead.

The list was written while describing a source, which is every playlist and
every segment request. Upstream writes it only where an encoder is started. A
rip with one damaged part answered 500 from master.m3u8 where upstream
answers 200 and fails at the transcode, and a cold playlist request blocked on
one ffprobe per VOB. Now written at the two places a transcode starts, and put
in place by a rename so two requests cannot hand ffmpeg a file cut short.

Also fixed: an encoder built with no cache configured named the list at the root
of the filesystem.

Not fixed, and true of upstream too: the list is written once and kept, so a
re-rip that changes a part's length keeps playing against the old durations.
Upstream ages its whole cache directory out on a schedule; this server has no
such task, so there it is stale until the file is removed by hand.

A Blu-ray still does not play. It takes this same branch upstream, but the list
is filled from the disc's own playlist, which nothing here reads — so the
argument names a file that is never written. Broken before this change and
broken after, now recorded in the test that says so.

Neither the harness nor the corpus can see any of this: the corpus generator
refuses a folder source, and no rip exists in either library. Whether ffmpeg
actually plays a rip from one of these lists is untested — the assertions are on
the argument and the bytes, not on a picture.

A rip's files now reach ffmpeg through the concat demuxer, as upstream sends them: a list written to `<cache>/concat/<sourceId>.concat`, one stanza per file naming where it is and how long it plays, passed as `-f concat -safe 0 -i`. The protocol form this replaces joined the files as bytes. The demuxer re-bases timestamps per file from the durations in the list, which the protocol cannot do — and the selection rule groups by the part number a name ends with, so two different title sets can land in one group and their timestamps restart at the boundary. The review turned up two things worth naming. **The duration was resting on an unverified assumption.** It is `TimeSpan.TotalSeconds` upstream, and .NET Framework scaled the ticks by a ten-millionth where modern .NET divides them — the two differ in the last bit for most values. Every duration in the tests happened to be one where both agree, so nothing could have caught it either way. Settled by running the real thing under `mcr.microsoft.com/dotnet/sdk:10.0`: `TimeSpan.FromTicks(12345678901) .TotalSeconds` is `1234.5678901`, the division. That value is now the test, and it fails against a reader that scales instead. **The list was written while describing a source**, which is every playlist and every segment request. Upstream writes it only where an encoder is started. A rip with one damaged part answered 500 from `master.m3u8` where upstream answers 200 and fails at the transcode, and a cold playlist request blocked on one ffprobe per VOB. Now written at the two places a transcode starts, and put in place by a rename so two requests cannot hand ffmpeg a file cut short. Also fixed: an encoder built with no cache configured named the list at the root of the filesystem. Not fixed, and true of upstream too: the list is written once and kept, so a re-rip that changes a part's length keeps playing against the old durations. Upstream ages its whole cache directory out on a schedule; this server has no such task, so there it is stale until the file is removed by hand. A Blu-ray still does not play. It takes this same branch upstream, but the list is filled from the disc's own playlist, which nothing here reads — so the argument names a file that is never written. Broken before this change and broken after, now recorded in the test that says so. Neither the harness nor the corpus can see any of this: the corpus generator refuses a folder source, and no rip exists in either library. Whether ffmpeg actually plays a rip from one of these lists is untested — the assertions are on the argument and the bytes, not on a picture.
Sign in to join this conversation.
No description provided.