@@ -237,6 +237,67 @@ fn project_inspect_missing_fails() {
237237 assert ! ( stderr( & output) . to_lowercase( ) . contains( "failed to load" ) ) ;
238238}
239239
240+ #[ test]
241+ fn project_commands_report_duration_for_readable_media ( ) {
242+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
243+ let project = dir. path ( ) . join ( "recording.cap" ) ;
244+ write_test_media ( & project. join ( "content/display.mp4" ) ) ;
245+ write_single_segment_meta ( & project) ;
246+
247+ for command in [ "validate" , "inspect" ] {
248+ let output = run ( & [
249+ "project" ,
250+ command,
251+ project. to_str ( ) . unwrap ( ) ,
252+ "--format" ,
253+ "json" ,
254+ ] ) ;
255+ assert ! (
256+ output. status. success( ) ,
257+ "{command} stderr: {}" ,
258+ stderr( & output)
259+ ) ;
260+ let json = parse_json ( & output) ;
261+ let duration = json[ "durationSeconds" ]
262+ . as_f64 ( )
263+ . unwrap_or_else ( || panic ! ( "{command} did not report durationSeconds: {json}" ) ) ;
264+ assert ! (
265+ ( duration - 1.0 ) . abs( ) < 0.25 ,
266+ "{command} reported unexpected duration {duration}"
267+ ) ;
268+ }
269+ }
270+
271+ #[ test]
272+ fn project_commands_omit_duration_for_unreadable_media ( ) {
273+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
274+ let project = dir. path ( ) . join ( "recording.cap" ) ;
275+ let content = project. join ( "content" ) ;
276+ std:: fs:: create_dir_all ( & content) . unwrap ( ) ;
277+ std:: fs:: write ( content. join ( "display.mp4" ) , b"fake" ) . unwrap ( ) ;
278+ write_single_segment_meta ( & project) ;
279+
280+ for command in [ "validate" , "inspect" ] {
281+ let output = run ( & [
282+ "project" ,
283+ command,
284+ project. to_str ( ) . unwrap ( ) ,
285+ "--format" ,
286+ "json" ,
287+ ] ) ;
288+ assert ! (
289+ output. status. success( ) ,
290+ "{command} stderr: {}" ,
291+ stderr( & output)
292+ ) ;
293+ let json = parse_json ( & output) ;
294+ assert ! (
295+ json. get( "durationSeconds" ) . is_none( ) ,
296+ "{command} reported a duration for unreadable media: {json}"
297+ ) ;
298+ }
299+ }
300+
240301#[ test]
241302fn project_validate_missing_meta_reports_invalid ( ) {
242303 let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
@@ -1228,6 +1289,59 @@ fn auth_login_defaults_to_the_creator_profile() {
12281289 assert ! ( stdout( & output) . contains( "[default: creator]" ) ) ;
12291290}
12301291
1292+ fn write_test_media ( path : & Path ) {
1293+ use ffmpeg:: format:: Pixel ;
1294+ ffmpeg:: init ( ) . unwrap ( ) ;
1295+ std:: fs:: create_dir_all ( path. parent ( ) . unwrap ( ) ) . unwrap ( ) ;
1296+ let mut output = ffmpeg:: format:: output ( path) . unwrap ( ) ;
1297+ let codec = ffmpeg:: encoder:: find ( ffmpeg:: codec:: Id :: MPEG4 ) . unwrap ( ) ;
1298+ let mut encoder = ffmpeg:: codec:: Context :: new_with_codec ( codec)
1299+ . encoder ( )
1300+ . video ( )
1301+ . unwrap ( ) ;
1302+ encoder. set_width ( 32 ) ;
1303+ encoder. set_height ( 32 ) ;
1304+ encoder. set_format ( Pixel :: YUV420P ) ;
1305+ encoder. set_time_base ( ( 1 , 30 ) ) ;
1306+ encoder. set_frame_rate ( Some ( ( 30 , 1 ) ) ) ;
1307+ encoder. set_flags ( ffmpeg:: codec:: Flags :: GLOBAL_HEADER ) ;
1308+ let mut encoder = encoder. open_as ( codec) . unwrap ( ) ;
1309+ output. add_stream ( codec) . unwrap ( ) . set_parameters ( & encoder) ;
1310+ output. write_header ( ) . unwrap ( ) ;
1311+ let output_time_base = output. stream ( 0 ) . unwrap ( ) . time_base ( ) ;
1312+ {
1313+ let mut drain = |encoder : & mut ffmpeg:: encoder:: video:: Encoder , finished : bool | loop {
1314+ let mut packet = ffmpeg:: Packet :: empty ( ) ;
1315+ match encoder. receive_packet ( & mut packet) {
1316+ Ok ( ( ) ) => {
1317+ packet. set_stream ( 0 ) ;
1318+ packet. rescale_ts ( ( 1 , 30 ) , output_time_base) ;
1319+ packet. write_interleaved ( & mut output) . unwrap ( ) ;
1320+ }
1321+ Err ( ffmpeg:: Error :: Other { errno } )
1322+ if !finished && errno == ffmpeg:: ffi:: EAGAIN =>
1323+ {
1324+ break ;
1325+ }
1326+ Err ( ffmpeg:: Error :: Eof ) if finished => break ,
1327+ Err ( error) => panic ! ( "test media encoding failed: {error}" ) ,
1328+ }
1329+ } ;
1330+ for index in 0 ..30 {
1331+ let mut frame = ffmpeg:: frame:: Video :: new ( Pixel :: YUV420P , 32 , 32 ) ;
1332+ frame. data_mut ( 0 ) . fill ( 32 + index as u8 ) ;
1333+ frame. data_mut ( 1 ) . fill ( 128 ) ;
1334+ frame. data_mut ( 2 ) . fill ( 128 ) ;
1335+ frame. set_pts ( Some ( index) ) ;
1336+ encoder. send_frame ( & frame) . unwrap ( ) ;
1337+ drain ( & mut encoder, false ) ;
1338+ }
1339+ encoder. send_eof ( ) . unwrap ( ) ;
1340+ drain ( & mut encoder, true ) ;
1341+ }
1342+ output. write_trailer ( ) . unwrap ( ) ;
1343+ }
1344+
12311345fn write_single_segment_meta ( project : & Path ) {
12321346 std:: fs:: create_dir_all ( project) . unwrap ( ) ;
12331347 let meta = serde_json:: json!( {
0 commit comments