@@ -260,6 +260,18 @@ pub struct ClipSummary {
260260 pub relative_time_in : f64 ,
261261 /// Clip duration in seconds.
262262 pub length : f64 ,
263+ /// Whether the measurement pass demuxed this clip's stream file.
264+ ///
265+ /// The one thing that separates "measured as empty" from "never looked
266+ /// at", both of which leave every measured field below at zero: a
267+ /// [`ScanMode::Metadata`] or [`ScanMode::Codecs`] open, a file a
268+ /// `scan_files` selection left out, a file that failed to open, and a file
269+ /// whose partial demux was dropped ([`ScanOptions::keep_partial`] off) all
270+ /// come back `false`, while a 0-byte or header-destroyed file that opens,
271+ /// reads to a clean end of file and registers no stream at all comes back
272+ /// `true` with nothing measured — which is what lets
273+ /// [`BdRom::short_stream_files`] name it.
274+ pub measured : bool ,
263275 /// Payload bytes the demux attributed to this clip.
264276 pub payload_bytes : u64 ,
265277 /// Transport packets the demux attributed to this clip.
@@ -311,7 +323,7 @@ impl ClipSummary {
311323
312324/// Test constructors for [`PlaylistSummary`] and [`ClipSummary`].
313325///
314- /// Both are wide plain-data structs — 11 and 12 public fields — while a test
326+ /// Both are wide plain-data structs — 11 and 13 public fields — while a test
315327/// usually cares about two or three. Each function takes those few and zeroes
316328/// the rest, so a test spells only its own intent and a field added to either
317329/// struct changes one body here instead of every construction site.
@@ -334,6 +346,7 @@ pub mod fixtures {
334346 angle_index : 0 ,
335347 relative_time_in : 0.0 ,
336348 length,
349+ measured : false ,
337350 payload_bytes : 0 ,
338351 packet_count : 0 ,
339352 packet_seconds : 0.0 ,
@@ -1216,15 +1229,19 @@ impl BdRom {
12161229 /// each entry carries).
12171230 ///
12181231 /// Derived from the per-clip summaries on every call, over whatever this
1219- /// [`BdRom`] holds: empty after an open that measured nothing
1232+ /// [`BdRom`] holds, and read through [`ClipSummary::measured`]: empty after
1233+ /// an open that measured nothing
12201234 /// ([`ScanMode::Metadata`]/[`ScanMode::Codecs`]), and silent about the
1221- /// files a `scan_files` selection left out.
1235+ /// files a `scan_files` selection left out. A 0-byte or header-destroyed
1236+ /// file that registers no stream at all IS named — it was measured, and
1237+ /// what it measured is nothing.
12221238 ///
12231239 /// A stream file whose read FAILED partway is named here too when its
12241240 /// partial state was kept ([`ScanOptions::keep_partial`]) — its measured
12251241 /// span is genuinely short. That file is also a [`ScanError`] of the
12261242 /// resilient scan, and this type carries no error list, so a surface
1227- /// showing both pairs them by file name.
1243+ /// showing both pairs them by file name. With the partial state dropped the
1244+ /// file is silent here and the [`ScanError`] is its only record.
12281245 #[ must_use]
12291246 pub fn short_stream_files ( & self ) -> Vec < ShortStreamFile > {
12301247 shortfall:: short_stream_files ( & self . playlists )
@@ -1992,6 +2009,11 @@ fn build_clip_summaries(
19922009 angle_index : clip. angle_index ,
19932010 relative_time_in : clip. relative_time_in ,
19942011 length : clip. length ,
2012+ // Presence in the full-pass map IS "the measurement pass demuxed
2013+ // this file": a file that never ran, failed to open, or had its
2014+ // partial state dropped is not in it, and one that opened and read
2015+ // to a clean end of file is — however little it carried.
2016+ measured : file. is_some ( ) ,
19952017 payload_bytes : clip. payload_bytes ,
19962018 packet_count : clip. packet_count ,
19972019 packet_seconds : clip. packet_seconds ,
@@ -4693,6 +4715,45 @@ mod tests {
46934715 assert ! ( unselected. short_stream_files( ) . is_empty( ) ) ;
46944716 }
46954717
4718+ #[ test]
4719+ fn a_stream_file_that_demuxes_to_nothing_at_all_is_reported_short ( ) {
4720+ // Total destruction leaves every number a never-scanned file leaves:
4721+ // no per-stream tally, no demuxed second, no error (both files open and
4722+ // read to a clean end of file). `ClipSummary::measured` is the only
4723+ // thing that tells the two apart, so this is the case that would go
4724+ // silent if the check read the tally list instead.
4725+ let clip = clpi ( & [ ( 0x1011 , 0x1B , [ 0x62 , 0x30 , 0 , 0 ] ) ] ) ;
4726+ // A 0-byte stub, as a rip that died before writing a byte leaves, and a
4727+ // non-empty file whose header is gone — no PAT, no PMT, so no stream
4728+ // ever registers.
4729+ for stub in [ Vec :: new ( ) , vec ! [ 0xFF ; 4096 ] ] {
4730+ let bytes = stub. len ( ) ;
4731+ let disc = TempDisc :: build (
4732+ & [ ] ,
4733+ & [
4734+ ( "BDMV/PLAYLIST/00000.mpls" , mpls ( "00000" , 0 , 4_500_000 , & [ ] ) ) , // 100 s
4735+ ( "BDMV/CLIPINF/00000.clpi" , clip. clone ( ) ) ,
4736+ ( "BDMV/STREAM/00000.m2ts" , stub) ,
4737+ ] ,
4738+ ) ;
4739+ let scanned = disc. open_scanned ( ) . expect ( "measured scan" ) ;
4740+ let summary = scanned
4741+ . playlists
4742+ . first ( )
4743+ . and_then ( |pl| pl. clips . first ( ) )
4744+ . expect ( "the destroyed file's clip row" ) ;
4745+ assert ! ( summary. measured, "{bytes} bytes: the file was demuxed" ) ;
4746+ assert ! ( summary. streams. is_empty( ) , "{bytes} bytes: nothing registered" ) ;
4747+ let reported = scanned. short_stream_files ( ) ;
4748+ assert_eq ! ( reported. len( ) , 1 , "{bytes} bytes: {reported:?}" ) ;
4749+ assert_eq ! (
4750+ reported. first( ) . expect( "the short file" ) . notice( ) ,
4751+ "00000.M2TS is shorter than declared: \
4752+ measured 0.0 s of 100.0 s (100.0 s missing)"
4753+ ) ;
4754+ }
4755+ }
4756+
46964757 #[ test]
46974758 fn clip_summaries_skip_a_registration_order_entry_without_a_stream ( ) {
46984759 // The two registration fields are public: a caller can desync them, so
@@ -6137,6 +6198,7 @@ mod tests {
61376198 assert_eq ! ( pl. chapters. len( ) , 2 ) ;
61386199 assert_eq ! ( pl. chapters. first( ) . unwrap( ) . avg_rate. to_bits( ) , 0.0_f64 . to_bits( ) ) ;
61396200 let clip = pl. clips . first ( ) . unwrap ( ) ;
6201+ assert ! ( !clip. measured, "a dropped file is not in the full-pass map" ) ;
61406202 assert ! ( clip. streams. is_empty( ) ) ;
61416203 assert_eq ! ( clip. file_seconds. to_bits( ) , 0.0_f64 . to_bits( ) ) ;
61426204 // …while the in-place playlist tallies the demux flushed before dying
@@ -6172,9 +6234,9 @@ mod tests {
61726234 short. first( ) . unwrap( ) . file( )
61736235 ) ;
61746236
6175- // Retention off drops the dead file's demux state , so it carries no
6176- // measured tallies at all — indistinguishable from a file the scan
6177- // never opened, and named by the recorded failure alone.
6237+ // Retention off drops the dead file from the full-pass map , so its clip
6238+ // comes back unmeasured — the same marker a file the scan never opened
6239+ // carries — and the recorded failure alone names it .
61786240 let off = BdRom :: open_resilient (
61796241 & tripping_disc ( m2ts, Some ( DATA_SIZE ) ) ,
61806242 ScanMode :: Full ,
@@ -6184,6 +6246,13 @@ mod tests {
61846246 )
61856247 . expect ( "resilient scan continues" ) ;
61866248 assert_eq ! ( off. errors. len( ) , 1 ) ;
6249+ let dropped = off
6250+ . bdrom
6251+ . playlists
6252+ . first ( )
6253+ . and_then ( |pl| pl. clips . first ( ) )
6254+ . expect ( "the dropped file's clip row" ) ;
6255+ assert ! ( !dropped. measured) ;
61876256 assert ! ( off. bdrom. short_stream_files( ) . is_empty( ) ) ;
61886257 }
61896258
0 commit comments