mux: json:// emits the complete title model (no dropped fields)
The json:// sink previously emitted only codec + language + pid per stream. Fill it out to the full DiscTitle: video carries resolution (+ pixel dims, interlaced), frame rate (+ fraction), HDR, colour space, display aspect, and measured CICP; audio carries channels (+ count), sample rate (+ Hz), and editorial purpose; subtitles carry the qualifier. Add the clip list and chapter names. This is everything the scan resolved, so json:// is a lossless machine-readable view of a title rather than a summary.
This commit is contained in:
+140
-18
@@ -93,41 +93,123 @@ impl Stream for ChaptersSink {
|
|||||||
|
|
||||||
// ── json:// ──────────────────────────────────────────────────────────────────
|
// ── json:// ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Serialization id for an audio stream's editorial purpose.
|
||||||
|
fn purpose_id(p: crate::labels::LabelPurpose) -> &'static str {
|
||||||
|
use crate::labels::LabelPurpose::*;
|
||||||
|
match p {
|
||||||
|
Normal => "normal",
|
||||||
|
Commentary => "commentary",
|
||||||
|
Descriptive => "descriptive",
|
||||||
|
Score => "score",
|
||||||
|
Ime => "ime",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serialization id for a subtitle stream's qualifier.
|
||||||
|
fn qualifier_id(q: crate::labels::LabelQualifier) -> &'static str {
|
||||||
|
use crate::labels::LabelQualifier::*;
|
||||||
|
match q {
|
||||||
|
None => "none",
|
||||||
|
Sdh => "sdh",
|
||||||
|
DescriptiveService => "descriptive_service",
|
||||||
|
Forced => "forced",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One stream as JSON — every field the scan resolved, nothing dropped. This is
|
||||||
|
/// the complete per-stream model (`disc::Stream`), not a summary: consumers get
|
||||||
|
/// resolution/HDR/aspect for video, channels/sample-rate/purpose for audio, and
|
||||||
|
/// the qualifier for subtitles, all in machine-readable form.
|
||||||
fn stream_json(s: &DiscStream) -> serde_json::Value {
|
fn stream_json(s: &DiscStream) -> serde_json::Value {
|
||||||
use super::demux_sink::codec_label;
|
use super::demux_sink::codec_label;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
match s {
|
match s {
|
||||||
DiscStream::Video(v) => json!({
|
DiscStream::Video(v) => {
|
||||||
"kind": "video",
|
let (w, h) = v.resolution.pixels();
|
||||||
"codec": codec_label(v.codec),
|
let (fps_num, fps_den) = v.frame_rate.as_fraction();
|
||||||
"pid": v.pid,
|
let mut o = json!({
|
||||||
}),
|
"kind": "video",
|
||||||
DiscStream::Audio(a) => json!({
|
"codec": codec_label(v.codec),
|
||||||
"kind": "audio",
|
"pid": v.pid,
|
||||||
"codec": codec_label(a.codec),
|
"resolution": v.resolution.to_string(),
|
||||||
"language": a.language,
|
"width": w,
|
||||||
"pid": a.pid,
|
"height": h,
|
||||||
}),
|
"interlaced": v.resolution.is_interlaced(),
|
||||||
|
"frame_rate": v.frame_rate.to_string(),
|
||||||
|
"frame_rate_num": fps_num,
|
||||||
|
"frame_rate_den": fps_den,
|
||||||
|
"hdr": v.hdr.id(),
|
||||||
|
"color_space": v.color_space.id(),
|
||||||
|
"secondary": v.secondary,
|
||||||
|
"mvc_dependent": v.is_mvc_dependent(),
|
||||||
|
});
|
||||||
|
if let Some((num, den)) = v.display_aspect {
|
||||||
|
o["display_aspect"] = json!(format!("{num}:{den}"));
|
||||||
|
}
|
||||||
|
if let Some(c) = v.measured_cicp {
|
||||||
|
o["measured_cicp"] = json!({
|
||||||
|
"matrix": c.matrix,
|
||||||
|
"transfer": c.transfer,
|
||||||
|
"primaries": c.primaries,
|
||||||
|
"range": c.range,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if !v.label.is_empty() {
|
||||||
|
o["label"] = json!(v.label);
|
||||||
|
}
|
||||||
|
o
|
||||||
|
}
|
||||||
|
DiscStream::Audio(a) => {
|
||||||
|
let mut o = json!({
|
||||||
|
"kind": "audio",
|
||||||
|
"codec": codec_label(a.codec),
|
||||||
|
"pid": a.pid,
|
||||||
|
"language": a.language,
|
||||||
|
"channels": a.channels.to_string(),
|
||||||
|
"channel_count": a.channels.count(),
|
||||||
|
"sample_rate": a.sample_rate.to_string(),
|
||||||
|
"sample_rate_hz": a.sample_rate.hz(),
|
||||||
|
"secondary": a.secondary,
|
||||||
|
"purpose": purpose_id(a.purpose),
|
||||||
|
});
|
||||||
|
if !a.label.is_empty() {
|
||||||
|
o["label"] = json!(a.label);
|
||||||
|
}
|
||||||
|
o
|
||||||
|
}
|
||||||
DiscStream::Subtitle(t) => json!({
|
DiscStream::Subtitle(t) => json!({
|
||||||
"kind": "subtitle",
|
"kind": "subtitle",
|
||||||
"codec": codec_label(t.codec),
|
"codec": codec_label(t.codec),
|
||||||
|
"pid": t.pid,
|
||||||
"language": t.language,
|
"language": t.language,
|
||||||
"forced": t.forced,
|
"forced": t.forced,
|
||||||
"pid": t.pid,
|
"qualifier": qualifier_id(t.qualifier),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The `json://` document for one title: identity, duration/size, its streams,
|
/// The `json://` document for one title: identity, duration/size, its clips,
|
||||||
/// and its chapter points. A stable, machine-readable view of one title.
|
/// its complete stream models, and its chapter points. A stable, machine-
|
||||||
|
/// readable view of one title — the same information the scan resolved, no loss.
|
||||||
pub(crate) fn title_json(title: &DiscTitle) -> serde_json::Value {
|
pub(crate) fn title_json(title: &DiscTitle) -> serde_json::Value {
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
let streams: Vec<_> = title.streams.iter().map(stream_json).collect();
|
let streams: Vec<_> = title.streams.iter().map(stream_json).collect();
|
||||||
|
let clips: Vec<_> = title
|
||||||
|
.clips
|
||||||
|
.iter()
|
||||||
|
.map(|c| {
|
||||||
|
json!({
|
||||||
|
"clip_id": c.clip_id,
|
||||||
|
"duration_secs": c.duration_secs,
|
||||||
|
"source_packets": c.source_packets,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
let chapters: Vec<_> = title
|
let chapters: Vec<_> = title
|
||||||
.chapters
|
.chapters
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, c)| json!({ "n": i + 1, "start_secs": c.time_secs }))
|
.map(|(i, c)| json!({ "n": i + 1, "start_secs": c.time_secs, "name": c.name }))
|
||||||
.collect();
|
.collect();
|
||||||
json!({
|
json!({
|
||||||
"playlist": title.playlist,
|
"playlist": title.playlist,
|
||||||
@@ -135,6 +217,7 @@ pub(crate) fn title_json(title: &DiscTitle) -> serde_json::Value {
|
|||||||
"duration_secs": title.duration_secs,
|
"duration_secs": title.duration_secs,
|
||||||
"size_bytes": title.size_bytes,
|
"size_bytes": title.size_bytes,
|
||||||
"format": format!("{:?}", title.content_format),
|
"format": format!("{:?}", title.content_format),
|
||||||
|
"clips": clips,
|
||||||
"streams": streams,
|
"streams": streams,
|
||||||
"chapters": chapters,
|
"chapters": chapters,
|
||||||
})
|
})
|
||||||
@@ -226,10 +309,49 @@ mod tests {
|
|||||||
})];
|
})];
|
||||||
let v = title_json(&t);
|
let v = title_json(&t);
|
||||||
assert_eq!(v["playlist"], "MAIN");
|
assert_eq!(v["playlist"], "MAIN");
|
||||||
assert_eq!(v["streams"][0]["kind"], "audio");
|
let a = &v["streams"][0];
|
||||||
assert_eq!(v["streams"][0]["codec"], "TrueHD");
|
assert_eq!(a["kind"], "audio");
|
||||||
assert_eq!(v["streams"][0]["language"], "eng");
|
assert_eq!(a["codec"], "TrueHD");
|
||||||
|
assert_eq!(a["language"], "eng");
|
||||||
|
// Completeness: audio carries channels + sample rate + purpose, not just codec.
|
||||||
|
assert_eq!(a["channels"], "stereo");
|
||||||
|
assert_eq!(a["channel_count"], 2);
|
||||||
|
assert_eq!(a["sample_rate"], "48kHz");
|
||||||
|
assert_eq!(a["sample_rate_hz"], 48000.0);
|
||||||
|
assert_eq!(a["purpose"], "normal");
|
||||||
assert_eq!(v["chapters"][1]["n"], 2);
|
assert_eq!(v["chapters"][1]["n"], 2);
|
||||||
assert_eq!(v["chapters"][1]["start_secs"], 62.5);
|
assert_eq!(v["chapters"][1]["start_secs"], 62.5);
|
||||||
|
assert_eq!(v["chapters"][1]["name"], "2");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn video_json_carries_resolution_and_hdr() {
|
||||||
|
use crate::disc::Codec;
|
||||||
|
use crate::disc::{
|
||||||
|
ColorSpace, DiscTitle, FrameRate, HdrFormat, Resolution, Stream as DiscStream,
|
||||||
|
VideoStream,
|
||||||
|
};
|
||||||
|
let mut t = DiscTitle::empty();
|
||||||
|
t.streams = vec![DiscStream::Video(VideoStream {
|
||||||
|
pid: 0x1011,
|
||||||
|
codec: Codec::Hevc,
|
||||||
|
resolution: Resolution::R2160p,
|
||||||
|
frame_rate: FrameRate::F23_976,
|
||||||
|
hdr: HdrFormat::Hdr10,
|
||||||
|
color_space: ColorSpace::Bt2020,
|
||||||
|
display_aspect: None,
|
||||||
|
secondary: false,
|
||||||
|
label: String::new(),
|
||||||
|
measured_cicp: None,
|
||||||
|
})];
|
||||||
|
let vid = &title_json(&t)["streams"][0];
|
||||||
|
assert_eq!(vid["kind"], "video");
|
||||||
|
assert_eq!(vid["resolution"], "2160p");
|
||||||
|
assert_eq!(vid["width"], 3840);
|
||||||
|
assert_eq!(vid["height"], 2160);
|
||||||
|
assert_eq!(vid["frame_rate"], "23.976");
|
||||||
|
assert_eq!(vid["frame_rate_num"], 24000);
|
||||||
|
assert_eq!(vid["hdr"], "hdr10");
|
||||||
|
assert_eq!(vid["color_space"], "bt2020");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user