Embedded Disc Metadata Format¶
decode-orc can write a LaserDisc's VBI metadata — picture numbers, programme time codes, chapter markers, stop codes, lead-in/lead-out markers and the programme status word — inside the exported Matroska file, as an attachment. A player emulator then needs exactly one file to play a side: no sidecar, no second thing to keep alongside the video.
This page is the format reference. It documents what the document contains,
how to read it, and the rules a consumer must follow. If you only want to
switch the feature on, see the embed_disc_metadata parameter in the
video sink documentation.
Why it exists¶
Almost all seeking a real LaserDisc player performs is expressed in terms of picture number or programme time code, and players expose that over a serial interface. An emulator driving decoded video needs the same information to behave like a player. The single most important thing the document provides is therefore the map between file frame position and disc address.
Finding the document¶
| Property | Value |
|---|---|
| Attachment filename | orc-disc-metadata.yaml |
| Attachment MIME type | application/vnd.decode-orc.disc-metadata+yaml |
| Container | Matroska (.mkv) only |
Both are a contract: locate the document by either, and never guess. Neither is ever versioned — they are how the document is found; the version inside says whether you can read it.
Matroska only, because MP4 and MOV reject attachment streams outright. FFV1 lives in Matroska anyway, so the restriction costs nothing in practice.
The metadata is not in the video stream
FFV1 has no user-data field, so nothing can ride inside the codec bitstream. The document lives at container level, which is also why it can be read without decoding a single frame.
Reading it with libavformat¶
The attachment arrives whole in extradata. No packet is demuxed and no seek
is performed:
avformat_open_input(&fmt, path, NULL, NULL);
avformat_find_stream_info(fmt, NULL);
for (unsigned i = 0; i < fmt->nb_streams; i++) {
AVStream *st = fmt->streams[i];
if (st->codecpar->codec_type != AVMEDIA_TYPE_ATTACHMENT) continue;
AVDictionaryEntry *fn = av_dict_get(st->metadata, "filename", NULL, 0);
if (!fn || strcmp(fn->value, "orc-disc-metadata.yaml")) continue;
parse_disc_metadata(st->codecpar->extradata,
st->codecpar->extradata_size);
break;
}
Load it once at file open and the picture-number map is available for the rest of the session.
Reading it from the command line¶
Consumers not using FFmpeg can read the same bytes from the Matroska
Attachments element with any EBML reader.
Versioning¶
Every document opens with the same header:
formatis a constant. It identifies the document type independently of the filename and MIME type, so the document survives being extracted, renamed, or carried in some future container.format_versionisMAJOR.MINOR, and is always a quoted string. Unquoted, YAML reads it as a float and1.10silently becomes1.1.
Compatibility rules¶
| Change | Bump | What a consumer must do |
|---|---|---|
| New optional key or block; a new value in a field documented as extensible | MINOR | Read the file, ignoring what it does not recognise |
Key removed or renamed; a field's type, units or meaning changed; a rule such as the unnumbered arithmetic altered |
MAJOR | Refuse the file and report the version |
The reader's contract, which every consumer should implement identically:
if format != "orc-disc-metadata" -> not our document; ignore it
if MAJOR != 1 -> refuse, report the version
if MINOR > known minor -> read it, ignore unknown keys
Refuse an unknown MAJOR — do not guess
An unknown MINOR means keys this reader has not heard of, which is harmless. An unknown MAJOR means a rule the reader relies on may have changed underneath it, and continuing would produce a seek map that is confidently wrong. A player that lands on the wrong frame is worse than one that reports it cannot read the metadata.
Keys reserved but not yet emitted (fm40, pulldown,
address_map.source: fm40) do not change meaning within MAJOR 1. Their
absence means "not decoded", never "not applicable".
Container tags¶
The disc-level summary is mirrored into Matroska tags, so a file is
self-describing to ffprobe and MediaInfo without extracting anything:
ORC_DISC_METADATA orc-disc-metadata.yaml
ORC_DISC_METADATA_VERSION 1.0
ORC_DISC_FORMAT CAV
ORC_DISC_SIDE 1
ORC_DISC_SIZE_INCHES 12
ORC_VIDEO_SYSTEM PAL
ORC_FIRST_PICTURE 1
ORC_LAST_PICTURE 54000
ORC_SOUND_MODE stereo
ORC_CX on
Tags whose value was never recovered are omitted, not written as
unknown. ORC_DISC_METADATA_VERSION always matches the document's
format_version, so a tool can decide whether it can read the attachment
before extracting it — but the document remains the authority.
Document structure¶
# decode-orc disc metadata
%YAML 1.2
---
format: orc-disc-metadata
format_version: "1.0"
generator:
application: decode-orc
version: "1.4.0"
video:
system: PAL # PAL | NTSC | PAL_M
frame_rate: 25 # nominal: the rate CLV timecode counts at
frame_rate_exact: [25, 1] # [30000, 1001] for NTSC
field_order: tff # tff | bff
frame_count: 54000 # frames present in this file
fields_per_frame: 2
source:
first_frame: 0 # source frame id of file frame 0 (provenance only)
last_frame: 53999
disc:
format: CAV # CAV | CLV
side: 1
size_inches: 12
cx_enabled: true
teletext: false
digital_video: false
sound_mode: stereo
fm_multiplex: false
programme_dump: false
user_code: "8DC00"
amendment2:
present: true
copy_permitted: false
video_standard: true
sound_mode: stereo
confidence:
fields_total: 108000
fields_with_status: 106880
fields_parity_valid: 106834
agreement: 0.9998
address_map:
kind: cav_picture
source: biphase
runs:
- {file_frame: 0, count: 54000, picture: 1}
unnumbered:
count: 10800
encoding: bitmap
bitmap: "8420..."
undecoded:
count: 12
encoding: ranges
ranges: [[20114, 3], [41022, 9]]
unmapped:
count: 24
encoding: ranges
ranges: [[0, 24]]
events:
lead_in: [[0, 24]]
lead_out: [[53976, 24]]
stop_codes: [12480, 33001]
chapters:
- {file_frame: 0, chapter: 1}
- {file_frame: 9000, chapter: 2}
File frames are always 0-based and contiguous, counting the frames actually
present in this file. source.first_frame records where they came from and is
provenance only — never use it to offset a lookup.
disc¶
Every value here is voted across each field that carried a status word, not
taken from the first field that had one. A single mis-decoded field must not be
able to flip the disc side. confidence.agreement is the fraction of status
fields whose whole word matched the vote — a strict measure, where one
dissenting bit counts the field as disagreeing.
Values never recovered are omitted rather than guessed.
events¶
lead_in/lead_out—[file_frame, count]ranges. A frame in a run is never listed here (see Lead frames that carry a number).stop_codes— file frames carrying a picture stop code.chapters— only chapter transitions, not every frame.
The address map¶
This is the part an emulator seeks with.
Runs¶
A run asserts an arithmetic relation over a span of consecutive file frames:
That is: the address advances once per file frame, except across frames
listed in unnumbered, which advance it by nothing.
kind selects the address space and the run's key:
kind |
Run key | Meaning |
|---|---|---|
cav_picture |
picture: 1 |
CAV picture number of the run's first frame |
clv_timecode |
time: "00:00:00.00" |
CLV programme time code of the run's first frame |
none |
— | No address information was recovered at all |
For clv_timecode, each subsequent frame advances one picture, rolling over at
the video system's nominal frame rate — 25 for PAL, 30 for NTSC and PAL-M.
CLV timecode is a disc address, not elapsed time
NTSC's 30 is nominal: the disc counts 30 pictures per timecode second while running at 30000/1001 fps. Never convert a CLV timecode to wall-clock time.
source says which channel the map came from. It is currently always
biphase; fm40 is reserved for the NTSC 40-bit channel (see
Not yet decoded).
A run breaks at an address discontinuity and at a change of address kind. Runs are not broken by frames that merely lack a number.
Frames with no picture number¶
On NTSC, film-sourced CAV material is mastered so that pulldown frames — video frames built from fields of two different photographic pictures — carry no picture number. That stops a player pausing or stepping onto a frame that is not a real picture. On a 2:3 cadence roughly one frame in five is affected, so the file-frame index and the disc address are not in one-to-one correspondence.
The picture number does not advance across such a frame. A frame numbered P, then a pulldown frame, then P + 1. The number sequence stays dense — no value is skipped — so a picture-number range remains contiguous.
There is a second reason a frame can lack a number, and it behaves in exactly the opposite way:
| Set | On the disc | Effect on the count |
|---|---|---|
unnumbered |
no number was ever recorded (pulldown) | does not advance |
undecoded |
a number exists; decode-orc did not recover it | does advance |
Keeping them separate is what lets one arithmetic rule cover both a
fully-numbered video disc and a film-sourced one. It also means the ratio of
undecoded to unnumbered is a direct measure of how well the VBI read.
How the two are told apart
decode-orc does not guess. For a gap of k frames between numbers P and
Q: Q − P == k + 1 means every hole frame advanced the count
(undecoded); Q − P == 1 means none did (unnumbered). Anything between
is a mix, resolved only when the per-frame evidence accounts for it exactly
— a frame whose VBI did not decode at all is a decode failure, and a clean
frame carrying a chapter code is positively a pulldown frame, since both
IEC standards place chapter numbers in the fields "which do not have an
insertion of picture numbers" (§10.1.5). So is a frame mastered as film's
pulldown frames are: line 16's status code in both fields, lines 17 and
18 decoded and blank. Damage that takes a number leaves those lines
undecoded, or decodes picture over them as a word that is not blank.
That settles the common film case of a pulldown frame beside a frame
whose number was lost. Otherwise the run simply breaks:
an ambiguous gap is never silently resolved.
What a consumer must do. An unnumbered frame has no address; an
undecoded one has an address the file cannot state. Neither may be given a
neighbouring frame's number in a map lookup — a real player holds and
redisplays the last number it saw, which is display behaviour, not an address.
Seeking is unaffected either way: every address the file states resolves to
exactly one file frame.
Set encoding: ranges or bitmap¶
unnumbered, undecoded and unmapped all use the same shape, in whichever
of two encodings is smaller. A reader must accept both.
unnumbered:
count: 10800
encoding: ranges # a list of [file_frame, count] pairs
ranges: [[3, 1], [8, 1], [13, 1]]
unnumbered:
count: 10800
encoding: bitmap # one bit per file frame, LSB-first, 1 = member
bitmap: "8420108421..."
The bitmap is 6.75 KB for a 54,000-frame side regardless of cadence, and
assumes nothing about the pulldown pattern being regular — it is not, across
edits. Ranges win when few frames are affected, which is the usual shape of
undecoded.
Decoding a bitmap:
frames = {i * 8 + bit
for i, byte in enumerate(bytes.fromhex(bitmap))
for bit in range(8)
if byte >> bit & 1}
count is always present, so a consumer that only cares how much of the side
is affected never has to decode either form.
Worked example¶
A short PAL CAV export: two lead-in frames, then pictures 1–16 across 18 frames, with two frames the disc never numbered.
address_map:
kind: cav_picture
source: biphase
runs:
- {file_frame: 2, count: 18, picture: 1}
unnumbered:
count: 2
encoding: bitmap
bitmap: "802000" # bits 7 and 13 -> file frames 7 and 13
unmapped:
count: 2
encoding: bitmap
bitmap: "030000" # bits 0 and 1 -> the lead-in frames
Applying the rule:
| File frame | Calculation | Address |
|---|---|---|
| 0, 1 | in unmapped |
none — lead-in |
| 2 | 1 + (2−2) − 0 |
picture 1 |
| 6 | 1 + (6−2) − 0 |
picture 5 |
| 7 | in unnumbered |
none — not a picture |
| 8 | 1 + (8−2) − 1 |
picture 6 |
| 19 | 1 + (19−2) − 2 |
picture 16 |
Note frame 8: the pulldown frame at 7 held the count, so picture 6 follows picture 5 with no value skipped.
In Python:
def address(map_, f):
for run in map_["runs"]:
if not run["file_frame"] <= f < run["file_frame"] + run["count"]:
continue
if f in unnumbered or f in undecoded:
return None # no address a consumer may rely on
held = len([u for u in unnumbered if run["file_frame"] <= u < f])
return run["picture"] + (f - run["file_frame"]) - held
return None # unmapped
unmapped¶
Frames covered by no run at all — typically lead-in frames before the first numbered frame, which have nothing preceding them to anchor a run. They are never interpolated: a gap is reported as a gap, because an emulator that seeks to a fabricated picture number lands somewhere wrong and silently.
Lead frames that carry a number¶
A lead code on one field does not stop the other field carrying a programme number. The first frame of a CLV programme can carry the lead-in code on its first field and the first time code on its second, and a stack can repeat such a frame.
The field that carries a picture number or CLV time code is the first field
of that picture (IEC 60856/60857 §10.1.3, §10.1.10). A frame whose other
field reads lead-in therefore holds the first field of the programme's first
picture, and the address is its own. It keeps its number when the programme
continues out of it, the next frame being one address on. Such a frame is in
a run and not listed under lead_in. The programme's first chapter starts
at it too. Its chapter-bearing field holds the lead-in code, so the chapter
number for its picture is read from the next frame. A lead-in frame that
repeats an address, with the next frame showing the same number again, or
that nothing after it continues, is unmapped and listed under lead_in.
A frame with a lead-out code keeps its number only when the programme
continues into it, the previous frame being one address back. Such a frame
is in a run and not listed under lead_out. Otherwise it is unmapped and
listed under lead_out. Either way, no address is claimed by both a lead
frame and a programme frame.
Raw VBI¶
At full detail an extra block carries the biphase words verbatim, so anything
the schema does not model stays recoverable from the file itself:
raw_vbi:
encoding: hex24-per-field
lines: [16, 17, 18]
# One line per file frame, six words:
# f1_l16 f1_l17 f1_l18 f2_l16 f2_l17 f2_l18
# "------" means the line was not decoded.
data: |
8dc000 88ffff 88ffff 8dc000 88ffff 88ffff
8dc000 f00001 f00001 8dc000 f00001 f00001
That is 42 bytes per frame — roughly 2.3 MB for a full 54,000-frame side, against tens of gigabytes of FFV1. It is off by default because most consumers only need the map.
PAL and NTSC¶
LaserVision is specified by two standards, one per video system:
- IEC 60856-1986 — LaserVision PAL, 50 Hz/625 lines
- IEC 60857-1986 — LaserVision NTSC, 60 Hz/525 lines
They are not interchangeable:
| PAL (IEC 60856) | NTSC (IEC 60857) | |
|---|---|---|
| Biphase picture/chapter/time code | lines 17, 18 / 330, 331 | lines 17, 18 / 280, 281 |
| Biphase CLV picture number | line 16 / 329 | line 16 / 279 |
| Biphase picture stop code | lines 16, 17 / 329, 330 | lines 16, 17 / 279, 280 |
| Max CAV picture number (biphase) | 99 999 | 79 999 |
| CLV picture-within-second | X4 = 0–2, X5 = 0–9 (0–24 used at 25 fps) | X4 = 0–2, X5 = 0–9 (0–29 used at 30 fps) |
| 40-bit FM coded channel | absent | lines 10, 273 (§10.2) |
| First-field white flag | absent | line 11 / 274 (§10.2.4) |
Both standards put the biphase codes on the same lines of each field, so no system-specific handling is needed for what decode-orc decodes today.
Known limitation: PAL picture numbers above 79 999
The biphase decoder masks the picture number to the NTSC limit of 79 999. A PAL disc numbered at or above 80 000 will decode incorrectly. Where that produces an address discontinuity, the map reports a run break rather than silently repairing it.
Not yet decoded¶
IEC 60857 §10.2 defines a 40-bit FM coded channel on lines 10 and 273 with
no PAL counterpart. It is a better address source than the biphase codes: on
CAV it is always present and runs to 99 999 rather than 79 999 (§10.2.3), and
on CLV its programme time carries a character naming the disc region outright —
A lead-in, B end-of-lead-in, D picture, C lead-out (§10.2.5) — where
the biphase codes leave the programme-area boundary to be inferred.
§10.2.4 defines the first-field white flag on line 11/274, positioned so it always marks the first field of the next photographic picture. That is the authoritative pulldown marker, better than inferring cadence from missing picture numbers.
decode-orc decodes neither. The schema reserves fm40 and pulldown blocks
plus address_map.source: fm40 so that adding them later is additive, and
their absence today means "not decoded".
Constraints worth knowing¶
The document is built before the first frame is encoded. Matroska writes attachments and tags into the file header, so the whole map must exist up front. Enabling the option therefore adds a pre-scan pass over the export range — the same cost chapter embedding already pays, and the two share one pass when both are on.
The metadata describes the frames actually written. VBI is decoded from the sink's own input, after any upstream frame removal or padding, so the file frame indices in the document always match the frames in the file.