diff --git a/ffplayout-engine/src/input/playlist.rs b/ffplayout-engine/src/input/playlist.rs index a66af059..a87fe01e 100644 --- a/ffplayout-engine/src/input/playlist.rs +++ b/ffplayout-engine/src/input/playlist.rs @@ -11,9 +11,11 @@ use serde_json::json; use simplelog::*; use ffplayout_lib::utils::{ - controller::PlayerControl, gen_dummy, get_delta, is_close, is_remote, - json_serializer::read_json, loop_filler, loop_image, modified_time, seek_and_length, - time_in_seconds, Media, MediaProbe, PlayoutConfig, PlayoutStatus, IMAGE_FORMAT, + controller::PlayerControl, + gen_dummy, get_delta, is_close, is_remote, + json_serializer::{read_json, set_defaults}, + loop_filler, loop_image, modified_time, seek_and_length, time_in_seconds, JsonPlaylist, Media, + MediaProbe, PlayoutConfig, PlayoutStatus, IMAGE_FORMAT, }; /// Struct for current playlist. @@ -24,9 +26,7 @@ pub struct CurrentProgram { config: PlayoutConfig, start_sec: f64, end_sec: f64, - json_mod: Option, - json_path: Option, - json_date: String, + json_playlist: JsonPlaylist, player_control: PlayerControl, current_node: Media, is_terminated: Arc, @@ -47,9 +47,10 @@ impl CurrentProgram { config: config.clone(), start_sec: config.playlist.start_sec.unwrap(), end_sec: config.playlist.length_sec.unwrap(), - json_mod: None, - json_path: None, - json_date: String::new(), + json_playlist: JsonPlaylist::new( + "1970-01-01".to_string(), + config.playlist.start_sec.unwrap(), + ), player_control: player_control.clone(), current_node: Media::new(0, "", false), is_terminated, @@ -65,9 +66,9 @@ impl CurrentProgram { let mut get_current = false; let mut reload = false; - if let Some(path) = self.json_path.clone() { + if let Some(path) = self.json_playlist.path.clone() { if (Path::new(&path).is_file() || is_remote(&path)) - && self.json_mod != modified_time(&path) + && self.json_playlist.modified != modified_time(&path) { info!("Reload playlist {path}"); self.playout_stat.list_init.store(true, Ordering::SeqCst); @@ -79,26 +80,24 @@ impl CurrentProgram { } if get_current { - let json = read_json( - &self.config, + self.json_playlist = read_json( + &mut self.config, &self.player_control, - self.json_path.clone(), + self.json_playlist.path.clone(), self.is_terminated.clone(), seek, false, ); if !reload { - if let Some(file) = &json.current_file { + if let Some(file) = &self.json_playlist.path { info!("Read playlist: {file}"); } } - self.json_path = json.current_file; - self.json_mod = json.modified; - *self.player_control.current_list.lock().unwrap() = json.program; + *self.player_control.current_list.lock().unwrap() = self.json_playlist.program.clone(); - if self.json_path.is_none() { + if self.json_playlist.path.is_none() { trace!("missing playlist"); self.current_node = Media::new(0, "", false); @@ -137,15 +136,16 @@ impl CurrentProgram { trace!("next_start: {next_start}, end_sec: {}", self.end_sec); // Check if we over the target length or we are close to it, if so we load the next playlist. - if next_start >= self.end_sec - || is_close(total_delta, 0.0, 2.0) - || is_close(total_delta, self.end_sec, 2.0) + if !self.config.playlist.infinit + && (next_start >= self.end_sec + || is_close(total_delta, 0.0, 2.0) + || is_close(total_delta, self.end_sec, 2.0)) { trace!("get next day"); next = true; - let json = read_json( - &self.config, + self.json_playlist = read_json( + &mut self.config, &self.player_control, None, self.is_terminated.clone(), @@ -153,18 +153,14 @@ impl CurrentProgram { true, ); - if let Some(file) = &json.current_file { + if let Some(file) = &self.json_playlist.path { info!("Read next playlist: {file}"); } self.playout_stat.list_init.store(false, Ordering::SeqCst); - self.set_status(json.date.clone()); + self.set_status(self.json_playlist.date.clone()); - self.json_path = json.current_file.clone(); - self.json_mod = json.modified; - self.json_date = json.date; - - *self.player_control.current_list.lock().unwrap() = json.program; + *self.player_control.current_list.lock().unwrap() = self.json_playlist.program.clone(); self.player_control.current_index.store(0, Ordering::SeqCst); } else { self.load_or_update_playlist(seek) @@ -212,7 +208,7 @@ impl CurrentProgram { let mut time_sec = time_in_seconds(); if time_sec < self.start_sec { - time_sec += self.config.playlist.length_sec.unwrap() + time_sec += 86400.0 // self.config.playlist.length_sec.unwrap(); } time_sec @@ -231,6 +227,15 @@ impl CurrentProgram { time_sec += *shift; } + drop(shift); + + if self.config.playlist.infinit + && self.json_playlist.length.unwrap() < 86400.0 + && time_sec > self.json_playlist.length.unwrap() + self.start_sec + { + self.recalculate_begin(true) + } + for (i, item) in self .player_control .current_list @@ -266,14 +271,14 @@ impl CurrentProgram { // de-instance node to preserve original values in list let mut node_clone = nodes[index].clone(); + // Important! When no manual drop is happen here, lock is still active in handle_list_init + drop(nodes); + trace!("Clip from init: {}", node_clone.source); node_clone.seek += time_sec - (node_clone.begin.unwrap() - *self.playout_stat.time_shift.lock().unwrap()); - // Important! When no manual drop is happen here, lock is still active in handle_list_init - drop(nodes); - self.current_node = handle_list_init( &self.config, node_clone, @@ -297,7 +302,6 @@ impl CurrentProgram { fn fill_end(&mut self, total_delta: f64) { // Fill end from playlist - let index = self.player_control.current_index.load(Ordering::SeqCst); let mut media = Media::new(index, "", false); media.begin = Some(time_in_seconds()); @@ -327,6 +331,20 @@ impl CurrentProgram { .current_index .fetch_add(1, Ordering::SeqCst); } + + fn recalculate_begin(&mut self, extend: bool) { + debug!("Infinit playlist reaches end, recalculate clip begins."); + + let mut time_sec = time_in_seconds(); + + if extend { + time_sec = self.start_sec + self.json_playlist.length.unwrap(); + } + + self.json_playlist.start_sec = Some(time_sec); + set_defaults(&mut self.json_playlist); + *self.player_control.current_list.lock().unwrap() = self.json_playlist.program.clone(); + } } /// Build the playlist iterator @@ -334,7 +352,7 @@ impl Iterator for CurrentProgram { type Item = Media; fn next(&mut self) -> Option { - self.last_json_path = self.json_path.clone(); + self.last_json_path = self.json_playlist.path.clone(); self.last_node_ad = self.current_node.last_ad; self.check_for_playlist(self.playout_stat.list_init.load(Ordering::SeqCst)); @@ -342,7 +360,7 @@ impl Iterator for CurrentProgram { trace!("Init playlist, from next iterator"); let mut init_clip_is_filler = false; - if self.json_path.is_some() { + if self.json_playlist.path.is_some() { init_clip_is_filler = self.init_clip(); } @@ -420,7 +438,7 @@ impl Iterator for CurrentProgram { let (_, total_delta) = get_delta(&self.config, &self.start_sec); if !self.config.playlist.infinit - && self.last_json_path == self.json_path + && self.last_json_path == self.json_playlist.path && total_delta.abs() > 1.0 { // Playlist is to early finish, @@ -438,6 +456,10 @@ impl Iterator for CurrentProgram { drop(c_list); + if self.config.playlist.infinit { + self.recalculate_begin(false) + } + self.player_control.current_index.store(0, Ordering::SeqCst); self.current_node = gen_source( &self.config, @@ -502,6 +524,7 @@ fn timed_source( if (total_delta > node.out - node.seek && !last) || node.index.unwrap() < 2 || !config.playlist.length.contains(':') + || config.playlist.infinit { // when we are in the 24 hour range, get the clip new_node.process = Some(true); @@ -742,7 +765,7 @@ fn handle_list_init( debug!("Playlist init"); let (_, total_delta) = get_delta(config, &node.begin.unwrap()); - if node.out - node.seek > total_delta { + if !config.playlist.infinit && node.out - node.seek > total_delta { node.out = total_delta + node.seek; } diff --git a/ffplayout-engine/src/output/mod.rs b/ffplayout-engine/src/output/mod.rs index fd1cbb36..6fe25df3 100644 --- a/ffplayout-engine/src/output/mod.rs +++ b/ffplayout-engine/src/output/mod.rs @@ -107,8 +107,18 @@ pub fn player( continue; } + let c_index = if cfg!(debug_assertions) { + format!( + " ({}/{})", + node.index.unwrap() + 1, + play_control.current_list.lock().unwrap().len() + ) + } else { + String::new() + }; + info!( - "Play for {}: {} {}", + "Play for {}{c_index}: {} {}", sec_to_time(node.out - node.seek), node.source, node.audio diff --git a/lib/src/utils/generator.rs b/lib/src/utils/generator.rs index a0b8449e..7beb60c5 100644 --- a/lib/src/utils/generator.rs +++ b/lib/src/utils/generator.rs @@ -272,8 +272,9 @@ pub fn generate_playlist( let mut playlist = JsonPlaylist { channel: channel.clone(), date, - current_file: None, + path: None, start_sec: None, + length: None, modified: None, program: vec![], }; diff --git a/lib/src/utils/import.rs b/lib/src/utils/import.rs index ce2c6ce5..a266ba99 100644 --- a/lib/src/utils/import.rs +++ b/lib/src/utils/import.rs @@ -19,8 +19,9 @@ pub fn import_file( let mut playlist = JsonPlaylist { channel: channel_name.unwrap_or_else(|| "Channel 1".to_string()), date: date.to_string(), - current_file: None, + path: None, start_sec: None, + length: None, modified: None, program: vec![], }; diff --git a/lib/src/utils/json_serializer.rs b/lib/src/utils/json_serializer.rs index 920e523a..a486a7d3 100644 --- a/lib/src/utils/json_serializer.rs +++ b/lib/src/utils/json_serializer.rs @@ -9,8 +9,8 @@ use std::{ use simplelog::*; use crate::utils::{ - controller::ProcessUnit::*, get_date, is_remote, modified_time, time_from_header, - validate_playlist, Media, PlayerControl, PlayoutConfig, DUMMY_LEN, + get_date, is_remote, modified_time, time_from_header, validate_playlist, Media, PlayerControl, + PlayoutConfig, DUMMY_LEN, }; /// This is our main playlist object, it holds all necessary information for the current day. @@ -24,7 +24,10 @@ pub struct JsonPlaylist { pub start_sec: Option, #[serde(skip_serializing, skip_deserializing)] - pub current_file: Option, + pub length: Option, + + #[serde(skip_serializing, skip_deserializing)] + pub path: Option, #[serde(skip_serializing, skip_deserializing)] pub modified: Option, @@ -33,7 +36,7 @@ pub struct JsonPlaylist { } impl JsonPlaylist { - fn new(date: String, start: f64) -> Self { + pub fn new(date: String, start: f64) -> Self { let mut media = Media::new(0, "", false); media.begin = Some(start); media.duration = DUMMY_LEN; @@ -42,7 +45,8 @@ impl JsonPlaylist { channel: "Channel 1".into(), date, start_sec: Some(start), - current_file: None, + length: Some(86400.0), + path: None, modified: None, program: vec![media], } @@ -61,13 +65,9 @@ fn default_channel() -> String { "Channel 1".to_string() } -fn set_defaults( - mut playlist: JsonPlaylist, - current_file: String, - mut start_sec: f64, -) -> JsonPlaylist { - playlist.current_file = Some(current_file); - playlist.start_sec = Some(start_sec); +pub fn set_defaults(playlist: &mut JsonPlaylist) { + let mut start_sec = playlist.start_sec.unwrap(); + let mut length = 0.0; // Add extra values to every media clip for (i, item) in playlist.program.iter_mut().enumerate() { @@ -78,69 +78,18 @@ fn set_defaults( item.process = Some(true); item.filter = None; - start_sec += item.out - item.seek; + let dur = item.out - item.seek; + start_sec += dur; + length += dur; } - playlist -} - -fn loop_playlist( - config: &PlayoutConfig, - current_file: String, - mut playlist: JsonPlaylist, -) -> JsonPlaylist { - let start_sec = config.playlist.start_sec.unwrap(); - let mut begin = start_sec; - let length = config.playlist.length_sec.unwrap(); - let mut program_list = vec![]; - let mut index = 0; - - playlist.current_file = Some(current_file); - playlist.start_sec = Some(start_sec); - - 'program_looper: loop { - for item in playlist.program.iter() { - let media = Media { - index: Some(index), - begin: Some(begin), - seek: item.seek, - out: item.out, - duration: item.duration, - duration_audio: item.duration_audio, - category: item.category.clone(), - source: item.source.clone(), - audio: item.audio.clone(), - cmd: item.cmd.clone(), - probe: item.probe.clone(), - probe_audio: item.probe_audio.clone(), - process: Some(true), - unit: Decoder, - last_ad: false, - next_ad: false, - filter: None, - custom_filter: String::new(), - }; - - if begin < start_sec + length { - program_list.push(media); - } else { - break 'program_looper; - } - - begin += item.out - item.seek; - index += 1; - } - } - - playlist.program = program_list; - - playlist + playlist.length = Some(length) } /// Read json playlist file, fills JsonPlaylist struct and set some extra values, /// which we need to process. pub fn read_json( - config: &PlayoutConfig, + config: &mut PlayoutConfig, player_control: &PlayerControl, path: Option, is_terminated: Arc, @@ -179,6 +128,8 @@ pub fn read_json( if let Ok(body) = resp.text() { let mut playlist: JsonPlaylist = serde_json::from_str(&body).expect("Could't read remote json playlist."); + playlist.path = Some(current_file); + playlist.start_sec = Some(start_sec); if let Some(time) = time_from_header(&headers) { playlist.modified = Some(time.to_string()); @@ -197,10 +148,9 @@ pub fn read_json( }); } - match config.playlist.infinit { - true => return loop_playlist(config, current_file, playlist), - false => return set_defaults(playlist, current_file, start_sec), - } + set_defaults(&mut playlist); + + return playlist; } } } @@ -225,6 +175,8 @@ pub fn read_json( playlist = JsonPlaylist::new(date, start_sec) } + playlist.path = Some(current_file); + playlist.start_sec = Some(start_sec); playlist.modified = modified; let list_clone = playlist.clone(); @@ -235,10 +187,9 @@ pub fn read_json( }); } - match config.playlist.infinit { - true => return loop_playlist(config, current_file, playlist), - false => return set_defaults(playlist, current_file, start_sec), - } + set_defaults(&mut playlist); + + return playlist; } error!("Playlist {current_file} not exist!"); diff --git a/lib/src/utils/json_validate.rs b/lib/src/utils/json_validate.rs index 38c38913..627148bb 100644 --- a/lib/src/utils/json_validate.rs +++ b/lib/src/utils/json_validate.rs @@ -236,5 +236,16 @@ pub fn validate_playlist( ); } - debug!("Validation done, in {:.3?} ...", timer.elapsed(),); + if config.general.validate { + info!( + "[Validation] Playlist length: {}", + sec_to_time(begin - config.playlist.start_sec.unwrap()) + ); + } + + debug!( + "Validation done, in {:.3?}, playlist length: {} ...", + timer.elapsed(), + sec_to_time(begin - config.playlist.start_sec.unwrap()) + ); } diff --git a/lib/src/utils/mod.rs b/lib/src/utils/mod.rs index 44652595..857fda1b 100644 --- a/lib/src/utils/mod.rs +++ b/lib/src/utils/mod.rs @@ -7,7 +7,6 @@ use std::{ path::{Path, PathBuf}, process::{exit, ChildStderr, Command, Stdio}, sync::{Arc, Mutex}, - time::{self, UNIX_EPOCH}, }; #[cfg(not(windows))] @@ -427,11 +426,12 @@ pub fn time_to_sec(time_str: &str) -> f64 { /// Convert floating number (seconds) to a formatted time string. pub fn sec_to_time(sec: f64) -> String { - let d = UNIX_EPOCH + time::Duration::from_millis((sec * 1000.0) as u64); - // Create DateTime from SystemTime - let date_time = DateTime::::from(d); - - date_time.format("%H:%M:%S%.3f").to_string() + format!( + "{:0>2}:{:0>2}:{:06.3}", + (sec / 60.0 / 60.0) as i32, + (sec / 60.0 % 60.0) as i32, + (sec % 60.0), + ) } /// get file extension @@ -463,22 +463,27 @@ pub fn sum_durations(clip_list: &Vec) -> f64 { pub fn get_delta(config: &PlayoutConfig, begin: &f64) -> (f64, f64) { let mut current_time = time_in_seconds(); let start = config.playlist.start_sec.unwrap(); - let length = time_to_sec(&config.playlist.length); + let length = config.playlist.length_sec.unwrap_or(86400.0); let mut target_length = 86400.0; if length > 0.0 && length != target_length { target_length = length } + if begin == &start && start == 0.0 && 86400.0 - current_time < 4.0 { - current_time -= target_length + current_time -= 86400.0 } else if start >= current_time && begin != &start { - current_time += target_length + current_time += 86400.0 } let mut current_delta = begin - current_time; - if is_close(current_delta, 86400.0, config.general.stop_threshold) { - current_delta -= 86400.0 + if is_close( + current_delta.abs(), + 86400.0, + config.general.stop_threshold + 2.0, + ) { + current_delta = current_delta.abs() - 86400.0 } let total_delta = if current_time < start { diff --git a/tests/assets/playlists/2024/03/2024-03-19.json b/tests/assets/playlists/2024/03/2024-03-19.json new file mode 100644 index 00000000..2952577d --- /dev/null +++ b/tests/assets/playlists/2024/03/2024-03-19.json @@ -0,0 +1,288 @@ +{ + "channel": "Test 1", + "date": "2024-02-01", + "program": [ + { + "in": 0, + "out": 10.0, + "duration": 10.0, + "source": "tests/assets/media_sorted/DarkGray_00-00-10.mp4" + }, + { + "in": 0, + "out": 30.0, + "duration": 30.0, + "source": "tests/assets/media_sorted/Olive_00-00-30.mp4" + }, + { + "in": 0, + "out": 15.0, + "duration": 15.0, + "source": "tests/assets/media_sorted/Indigo_00-00-15.mp4" + }, + { + "in": 0, + "out": 25.0, + "duration": 25.0, + "source": "tests/assets/media_sorted/DarkOrchid_00-00-25.mp4" + }, + { + "in": 0, + "out": 45.0, + "duration": 45.0, + "source": "tests/assets/media_sorted/Orange_00-00-45.mp4" + }, + { + "in": 0, + "out": 20.0, + "duration": 20.0, + "source": "tests/assets/media_sorted/LightGoldenRodYellow_00-00-20.mp4" + }, + { + "in": 0, + "out": 30.0, + "duration": 30.0, + "source": "tests/assets/media_sorted/Cyan_00-00-30.mp4" + }, + { + "in": 0, + "out": 50.0, + "duration": 50.0, + "source": "tests/assets/media_sorted/Cornsilk_00-00-50.mp4" + }, + { + "in": 0, + "out": 15.0, + "duration": 15.0, + "source": "tests/assets/media_sorted/LightSeaGreen_00-00-15.mp4" + }, + { + "in": 0, + "out": 30.0, + "duration": 30.0, + "source": "tests/assets/media_sorted/Yellow_00-00-30.mp4" + }, + { + "in": 0, + "out": 30.0, + "duration": 30.0, + "source": "tests/assets/media_sorted/Aqua_00-00-30.mp4" + }, + { + "in": 0, + "out": 1500.0, + "duration": 1500.0, + "source": "tests/assets/media_sorted/MediumSeaGreen_00-25-00.mp4" + }, + { + "in": 0, + "out": 1800.0, + "duration": 1800.0, + "source": "tests/assets/media_sorted/MediumOrchid_00-30-00.mp4" + }, + { + "in": 0, + "out": 3600.0, + "duration": 3600.0, + "source": "tests/assets/media_sorted/IndianRed_01-00-00.mp4" + }, + { + "in": 0, + "out": 3600.0, + "duration": 3600.0, + "source": "tests/assets/media_sorted/ForestGreen_01-00-00.mp4" + }, + { + "in": 0, + "out": 3600.0, + "duration": 3600.0, + "source": "tests/assets/media_sorted/Plum_01-00-00.mp4" + }, + { + "in": 0, + "out": 3600.0, + "duration": 3600.0, + "source": "tests/assets/media_sorted/IndianRed_01-00-00.mp4" + }, + { + "in": 0, + "out": 3600.0, + "duration": 3600.0, + "source": "tests/assets/media_sorted/ForestGreen_01-00-00.mp4" + }, + { + "in": 0, + "out": 3600.0, + "duration": 3600.0, + "source": "tests/assets/media_sorted/Plum_01-00-00.mp4" + }, + { + "in": 0, + "out": 3600.0, + "duration": 3600.0, + "source": "tests/assets/media_sorted/IndianRed_01-00-00.mp4" + }, + { + "in": 0, + "out": 3600.0, + "duration": 3600.0, + "source": "tests/assets/media_sorted/ForestGreen_01-00-00.mp4" + }, + { + "in": 0, + "out": 3600.0, + "duration": 3600.0, + "source": "tests/assets/media_sorted/Plum_01-00-00.mp4" + }, + { + "in": 0, + "out": 3600.0, + "duration": 3600.0, + "source": "tests/assets/media_sorted/IndianRed_01-00-00.mp4" + }, + { + "in": 0, + "out": 3600.0, + "duration": 3600.0, + "source": "tests/assets/media_sorted/ForestGreen_01-00-00.mp4" + }, + { + "in": 0, + "out": 3600.0, + "duration": 3600.0, + "source": "tests/assets/media_sorted/Plum_01-00-00.mp4" + }, + { + "in": 0, + "out": 3600.0, + "duration": 3600.0, + "source": "tests/assets/media_sorted/IndianRed_01-00-00.mp4" + }, + { + "in": 0, + "out": 3600.0, + "duration": 3600.0, + "source": "tests/assets/media_sorted/ForestGreen_01-00-00.mp4" + }, + { + "in": 0, + "out": 3600.0, + "duration": 3600.0, + "source": "tests/assets/media_sorted/Plum_01-00-00.mp4" + }, + { + "in": 0, + "out": 3600.0, + "duration": 3600.0, + "source": "tests/assets/media_sorted/IndianRed_01-00-00.mp4" + }, + { + "in": 0, + "out": 3600.0, + "duration": 3600.0, + "source": "tests/assets/media_sorted/ForestGreen_01-00-00.mp4" + }, + { + "in": 0, + "out": 3600.0, + "duration": 3600.0, + "source": "tests/assets/media_sorted/Plum_01-00-00.mp4" + }, + { + "in": 0, + "out": 3600.0, + "duration": 3600.0, + "source": "tests/assets/media_sorted/IndianRed_01-00-00.mp4" + }, + { + "in": 0, + "out": 3600.0, + "duration": 3600.0, + "source": "tests/assets/media_sorted/ForestGreen_01-00-00.mp4" + }, + { + "in": 0, + "out": 3600.0, + "duration": 3600.0, + "source": "tests/assets/media_sorted/Plum_01-00-00.mp4" + }, + { + "in": 0, + "out": 3600.0, + "duration": 3600.0, + "source": "tests/assets/media_sorted/IndianRed_01-00-00.mp4" + }, + { + "in": 0, + "out": 1800.0, + "duration": 1800.0, + "source": "tests/assets/media_sorted/MediumOrchid_00-30-00.mp4" + }, + { + "in": 0, + "out": 1500.0, + "duration": 1500.0, + "source": "tests/assets/media_sorted/MediumSeaGreen_00-25-00.mp4" + }, + { + "in": 0, + "out": 10.0, + "duration": 10.0, + "source": "tests/assets/media_sorted/DarkGray_00-00-10.mp4" + }, + { + "in": 0, + "out": 30.0, + "duration": 30.0, + "source": "tests/assets/media_sorted/Olive_00-00-30.mp4" + }, + { + "in": 0, + "out": 15.0, + "duration": 15.0, + "source": "tests/assets/media_sorted/Indigo_00-00-15.mp4" + }, + { + "in": 0, + "out": 25.0, + "duration": 25.0, + "source": "tests/assets/media_sorted/DarkOrchid_00-00-25.mp4" + }, + { + "in": 0, + "out": 45.0, + "duration": 45.0, + "source": "tests/assets/media_sorted/Orange_00-00-45.mp4" + }, + { + "in": 0, + "out": 20.0, + "duration": 20.0, + "source": "tests/assets/media_sorted/LightGoldenRodYellow_00-00-20.mp4" + }, + { + "in": 0, + "out": 30.0, + "duration": 30.0, + "source": "tests/assets/media_sorted/Cyan_00-00-30.mp4" + }, + { + "in": 0, + "out": 50.0, + "duration": 50.0, + "source": "tests/assets/media_sorted/Cornsilk_00-00-50.mp4" + }, + { + "in": 0, + "out": 15.0, + "duration": 15.0, + "source": "tests/assets/media_sorted/LightSeaGreen_00-00-15.mp4" + }, + { + "in": 0, + "out": 30.0, + "duration": 30.0, + "source": "tests/assets/media_sorted/Yellow_00-00-30.mp4" + } + ] +}