diff --git a/Cargo.toml b/Cargo.toml index 5a76fae..ad50c77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,12 +22,13 @@ tokio = { version = "1.47", default-features = false, features = [ [features] tokio = ["dep:tokio"] +utils = [] [dev-dependencies] roxmltree = "0.20.0" tokio-test = "0.4.4" tokio = { version = "1.47", features = ["full"] } -mkv-element = { path = ".", features = ["tokio"] } +mkv-element = { path = ".", features = ["tokio", "utils"] } [build-dependencies] roxmltree = "0.20.0" diff --git a/README.md b/README.md index 864e6e9..03dc1c6 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,25 @@ assert_eq!(ebml, ebml_read_4); # }) ``` +## Features + +This crate provides the following optional features: + +- **`tokio`**: Enables asynchronous I/O support using Tokio. This adds `async_read_from()`, `async_read_element()`, and `async_write_to()` methods that work with types implementing `tokio::io::AsyncRead` and `tokio::io::AsyncWrite`. + +- **`utils`**: Enables utility modules for working with Matroska files, such as the `view` module. The `view` module provides `MatroskaView` and `SegmentView` structs for efficiently parsing MKV file metadata without loading cluster data into memory. + +To enable these features, add them to your `Cargo.toml`: + +```toml +[dependencies] +mkv-element = { version = "0.2", features = ["tokio", "utils"] } +``` + +## Important notes +1. if you need to work with actual MKV files, don't read a whole segment into memory at once, read only the parts you need instead. Real world MKV files can be very large.) +``` + ## Quick Note 1. if you need to work with actual MKV files, don't read a whole segment into memory at once, read only the parts you need instead. Real world MKV files can be very large. 2. According to the Matroska specifications, segments and clusters can have an "unknown" size (all size bytes set to 1). In that case, the segment/cluster extends to the end of the file or until the next segment/cluster. This needs to handle by the user. Trying to read such elements with this library will result in an [`ElementBodySizeUnknown`](crate::Error::ElementBodySizeUnknown) error. diff --git a/src/io.rs b/src/io.rs index d02f149..6bacf25 100644 --- a/src/io.rs +++ b/src/io.rs @@ -61,6 +61,19 @@ pub mod blocking_impl { Ok(()) } } + + /// Write an element to a writer provided the header. + pub trait WriteElement: Sized + Element { + /// Write an element to a writer. + fn write_element(&self, header: &Header, w: &mut W) -> crate::Result<()> { + header.write_to(w)?; + let mut buf = vec![]; + self.encode_body(&mut buf)?; + w.write_all(&buf)?; + Ok(()) + } + } + impl WriteElement for T {} } /// tokio non-blocking I/O implementations, supporting async reading and writing. #[cfg(feature = "tokio")] @@ -119,6 +132,24 @@ pub mod tokio_impl { } } + /// Write an element to a writer provided the header asynchronously. + pub trait AsyncWriteElement: Sized + Element { + /// Write an element to a writer asynchronously. + fn async_write_element( + &self, + header: &Header, + w: &mut W, + ) -> impl std::future::Future> { + async { + header.async_write_to(w).await?; + let mut buf = vec![]; + self.encode_body(&mut buf)?; + Ok(w.write_all(&buf).await?) + } + } + } + impl AsyncWriteElement for T {} + impl Header { /// Read the body of the element from a reader into memory. pub(crate) async fn read_body_tokio( diff --git a/src/lib.rs b/src/lib.rs index 63a355f..0d09685 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,10 @@ mod supplement; // Supplementary elements in Matroska. Void elements, CRC-32, et // following modules are public pub mod io; +#[cfg(feature = "utils")] +#[cfg_attr(docsrs, doc(cfg(feature = "utils")))] +pub mod view; + // Re-export common types pub use crate::frame::*; pub use crate::lacer::*; diff --git a/src/view.rs b/src/view.rs new file mode 100644 index 0000000..6ad1c99 --- /dev/null +++ b/src/view.rs @@ -0,0 +1,347 @@ +//! A View of a Matroska file, parsing w/o loading clusters into memory. + +use std::mem::take; + +use crate::element::Element; +use crate::master::*; + +/// View of a Matroska file, parsing the EBML and Segment headers, but not loading Clusters. +#[derive(Debug, Clone, PartialEq)] +pub struct MatroskaView { + /// The EBML header. + pub ebml: Ebml, + /// The Segment views, as there can be multiple segments in a Matroska file. + pub segments: Vec, +} + +impl MatroskaView { + /// Create a new MatroskaView by parsing the EBML header and all Segment headers, + /// but skipping Cluster data to avoid loading it into memory. + pub fn new(reader: &mut R) -> crate::Result + where + R: std::io::Read + std::io::Seek, + { + use crate::io::blocking_impl::*; + + // Read the EBML header + let ebml = Ebml::read_from(reader)?; + + // Parse all segments in the file + let segments = SegmentView::new(reader)?; + + // At least one segment is required + if segments.is_empty() { + return Err(crate::Error::MissingElement(Segment::ID)); + } + + Ok(MatroskaView { ebml, segments }) + } + + /// Create a new MatroskaView by parsing the EBML header and all Segment headers, + /// but skipping Cluster data to avoid loading it into memory. + #[cfg(feature = "tokio")] + #[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] + pub async fn new_async(reader: &mut R) -> crate::Result + where + R: tokio::io::AsyncRead + tokio::io::AsyncSeek + Unpin, + { + use crate::io::tokio_impl::*; + + // Read the EBML header + let ebml = Ebml::async_read_from(reader).await?; + + // Parse all segments in the file + let segments = SegmentView::new_async(reader).await?; + + Ok(MatroskaView { ebml, segments }) + } +} + +/// View of a Segment, parsing the Segment header, but not loading Clusters. +#[derive(Debug, Clone, PartialEq)] +pub struct SegmentView { + /// Contains seeking information of Top-Level Elements; see data-layout. + pub seek_head: Vec, + /// Contains general information about the Segment. + pub info: Info, + /// A Top-Level Element of information with many tracks described. + pub tracks: Option, + /// A Top-Level Element to speed seeking access. All entries are local to the Segment. This Element **SHOULD** be set when the Segment is not transmitted as a live stream (see #livestreaming). + pub cues: Option, + /// Contain attached files. + pub attachments: Option, + /// A system to define basic menus and partition data. For more detailed information, look at the Chapters explanation in chapters. + pub chapters: Option, + /// Element containing metadata describing Tracks, Editions, Chapters, Attachments, or the Segment as a whole. A list of valid tags can be found in [Matroska tagging RFC](https://www.matroska.org/technical/tagging.html). + pub tags: Vec, + /// The position of the Segment data (after the Segment header). + pub segment_data_position: u64, + /// The position of the first Cluster in the Segment. 0 if no Cluster found. + pub first_cluster_position: u64, +} + +impl SegmentView { + /// Create a new SegmentView by parsing the Segment header and metadata elements, + /// but skipping Cluster data to avoid loading it into memory. + pub fn new(reader: &mut R) -> crate::Result> + where + R: std::io::Read + std::io::Seek, + { + let mut out = vec![]; + + use crate::io::blocking_impl::*; + use std::io::SeekFrom; + + // Read the Segment header + let segment_header = crate::base::Header::read_from(reader)?; + if segment_header.id != Segment::ID { + return Err(crate::Error::MissingElement(Segment::ID)); + } + + let mut segment_data_position = reader.stream_position()?; + + let mut seek_head = Vec::new(); + let mut info = None; + let mut tracks = None; + let mut cues = None; + let mut attachments = None; + let mut chapters = None; + let mut tags = Vec::new(); + let mut first_cluster_position = 0; + + // Parse segment elements + loop { + use crate::base::Header; + + let current_position = reader.stream_position()?; + let Ok(header) = Header::read_from(reader) else { + break; + }; + if header.id == Cluster::ID && first_cluster_position == 0 { + first_cluster_position = current_position; + } + + // Check if we've reached the end of the segment + match header.id { + SeekHead::ID => seek_head.push(SeekHead::read_element(&header, reader)?), + Info::ID => info = Some(Info::read_element(&header, reader)?), + Tracks::ID => tracks = Some(Tracks::read_element(&header, reader)?), + Cues::ID => cues = Some(Cues::read_element(&header, reader)?), + Attachments::ID => attachments = Some(Attachments::read_element(&header, reader)?), + Chapters::ID => chapters = Some(Chapters::read_element(&header, reader)?), + Tags::ID => tags.push(Tags::read_element(&header, reader)?), + Cluster::ID => { + // try to skip, or else break + use crate::base::VInt64; + let mut seeks: Vec<(VInt64, u64)> = seek_head + .iter() + .flat_map(|sh| { + sh.seek.iter().flat_map(|s| { + let mut id = &s.seek_id[..]; + let a = VInt64::read_from(&mut id); + match a { + Ok(v) => Some((v, *s.seek_position + segment_data_position)), + Err(e) => { + log::warn!("Failed to read seek_id as VInt: {e}, skip..."); + None + } + } + }) + }) + .collect(); + + seeks.sort_by(|a, b| a.1.cmp(&b.1)); + + // find position larger than first_cluster_position + if let Some(pos) = seeks.iter().find(|(_, pos)| *pos > first_cluster_position) { + reader.seek(SeekFrom::Start(pos.1))?; + continue; + } + + if segment_header.size.is_unknown { + break; + } else { + let eos = segment_data_position + *segment_header.size; + reader.seek(SeekFrom::Start(eos))?; + continue; + } + } + Segment::ID => { + out.push(SegmentView { + seek_head: take(&mut seek_head), + // Info is required in a valid Matroska file + info: info.take().ok_or(crate::Error::MissingElement(Info::ID))?, + tracks: tracks.take(), + cues: cues.take(), + attachments: attachments.take(), + chapters: chapters.take(), + tags: take(&mut tags), + first_cluster_position: take(&mut first_cluster_position), + segment_data_position: take(&mut segment_data_position), + }); + segment_data_position = reader.stream_position()?; + } + _ => { + use log::warn; + use std::io::Read; + // Skip unknown elements, here we read and discard the data for efficiency + std::io::copy(&mut reader.take(*header.size), &mut std::io::sink())?; + warn!("Skipped unknown element with ID: {}", header.id); + } + } + } + + // Info is required in a valid Matroska file + let info = info.ok_or(crate::Error::MissingElement(Info::ID))?; + + out.push(SegmentView { + seek_head, + info, + tracks, + cues, + attachments, + chapters, + tags, + first_cluster_position, + segment_data_position, + }); + Ok(out) + } + + /// Create a new SegmentView by parsing the Segment header and metadata elements, + /// but skipping Cluster data to avoid loading it into memory. + #[cfg(feature = "tokio")] + #[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] + pub async fn new_async(reader: &mut R) -> crate::Result> + where + R: tokio::io::AsyncRead + tokio::io::AsyncSeek + Unpin, + { + let mut out = vec![]; + + use crate::io::tokio_impl::*; + use tokio::io::AsyncSeekExt; + + // Read the Segment header + let segment_header = crate::base::Header::async_read_from(reader).await?; + if segment_header.id != Segment::ID { + return Err(crate::Error::MissingElement(Segment::ID)); + } + + let mut segment_data_position = reader.stream_position().await?; + + let mut seek_head = Vec::new(); + let mut info = None; + let mut tracks = None; + let mut cues = None; + let mut attachments = None; + let mut chapters = None; + let mut tags = Vec::new(); + let mut first_cluster_position = 0; + + // Parse segment elements + loop { + use crate::base::Header; + + let current_position = reader.stream_position().await?; + let Ok(header) = Header::async_read_from(reader).await else { + break; + }; + if header.id == Cluster::ID && first_cluster_position == 0 { + first_cluster_position = current_position; + } + + // Check if we've reached the end of the segment + match header.id { + SeekHead::ID => { + seek_head.push(SeekHead::async_read_element(&header, reader).await?) + } + Info::ID => info = Some(Info::async_read_element(&header, reader).await?), + Tracks::ID => tracks = Some(Tracks::async_read_element(&header, reader).await?), + Cues::ID => cues = Some(Cues::async_read_element(&header, reader).await?), + Attachments::ID => { + attachments = Some(Attachments::async_read_element(&header, reader).await?) + } + Chapters::ID => { + chapters = Some(Chapters::async_read_element(&header, reader).await?) + } + Tags::ID => tags.push(Tags::async_read_element(&header, reader).await?), + Cluster::ID => { + // try to skip, or else break + use crate::base::VInt64; + let mut seeks: Vec<(VInt64, u64)> = seek_head + .iter() + .flat_map(|sh| { + sh.seek.iter().flat_map(|s| { + use crate::io::blocking_impl::ReadFrom; + + let mut id = &s.seek_id[..]; + let a = VInt64::read_from(&mut id); + match a { + Ok(v) => Some((v, *s.seek_position + segment_data_position)), + Err(e) => { + log::warn!("Failed to read seek_id as VInt: {e}, skip..."); + None + } + } + }) + }) + .collect(); + + seeks.sort_by(|a, b| a.1.cmp(&b.1)); + + // find position larger than first_cluster_position + if let Some(pos) = seeks.iter().find(|(_, pos)| *pos > first_cluster_position) { + reader.seek(std::io::SeekFrom::Start(pos.1)).await?; + continue; + } + + if segment_header.size.is_unknown { + break; + } else { + let eos = segment_data_position + *segment_header.size; + reader.seek(std::io::SeekFrom::Start(eos)).await?; + continue; + } + } + Segment::ID => { + out.push(SegmentView { + seek_head: take(&mut seek_head), + // Info is required in a valid Matroska file + info: info.take().ok_or(crate::Error::MissingElement(Info::ID))?, + tracks: tracks.take(), + cues: cues.take(), + attachments: attachments.take(), + chapters: chapters.take(), + tags: take(&mut tags), + first_cluster_position: take(&mut first_cluster_position), + segment_data_position: take(&mut segment_data_position), + }); + segment_data_position = reader.stream_position().await?; + } + _ => { + use log::warn; + use tokio::io::AsyncReadExt; + // Skip unknown elements, here we read and discard the data for efficiency + tokio::io::copy(&mut reader.take(*header.size), &mut tokio::io::sink()).await?; + warn!("Skipped unknown element with ID: {}", header.id); + } + } + } + + // Info is required in a valid Matroska file + let info = info.ok_or(crate::Error::MissingElement(Info::ID))?; + + out.push(SegmentView { + seek_head, + info, + tracks, + cues, + attachments, + chapters, + tags, + first_cluster_position, + segment_data_position, + }); + Ok(out) + } +} diff --git a/tests/view_integration.rs b/tests/view_integration.rs new file mode 100644 index 0000000..39e4b1e --- /dev/null +++ b/tests/view_integration.rs @@ -0,0 +1,337 @@ +#![cfg(feature = "utils")] + +use mkv_element::io::blocking_impl::{WriteElement, WriteTo}; +use mkv_element::prelude::*; +use mkv_element::view::MatroskaView; +use std::io::Cursor; + +/// Helper function to create a standard EBML header for Matroska +fn ebml() -> Ebml { + Ebml { + crc32: None, + ebml_version: Some(EbmlVersion(1)), + ebml_read_version: Some(EbmlReadVersion(1)), + ebml_max_id_length: EbmlMaxIdLength(4), + ebml_max_size_length: EbmlMaxSizeLength(8), + doc_type: Some(DocType("matroska".to_string())), + doc_type_version: Some(DocTypeVersion(4)), + doc_type_read_version: Some(DocTypeReadVersion(2)), + void: None, + } +} + +/// Helper function to create the first test segment with basic info +fn segment1() -> Segment { + let info = Info { + timestamp_scale: TimestampScale(1000000), // 1ms per tick (default) + muxing_app: MuxingApp("mkv-element".to_string()), + writing_app: WritingApp("integration-test".to_string()), + title: Some(Title("Test Segment 1".to_string())), + duration: Some(Duration(30000.0)), // 30 seconds + ..Default::default() + }; + + // Create a simple video track + let video_track = TrackEntry { + track_number: TrackNumber(1), + track_uid: TrackUid(1234567890), + track_type: TrackType(1), // Video + codec_id: CodecId("V_VP9".to_string()), + name: Some(Name("Video Track".to_string())), + codec_name: Some(CodecName("VP9".to_string())), + video: Some(Video { + pixel_width: PixelWidth(1920), + pixel_height: PixelHeight(1080), + ..Default::default() + }), + ..Default::default() + }; + + let tracks = Tracks { + track_entry: vec![video_track], + ..Default::default() + }; + + // Create a simple cluster with dummy data + let cluster = Cluster { + timestamp: Timestamp(0), + blocks: vec![], // Empty for simplicity + ..Default::default() + }; + + Segment { + crc32: None, + void: None, + seek_head: vec![], + info, + cluster: vec![cluster], + tracks: Some(tracks), + cues: None, + attachments: None, + chapters: None, + tags: vec![], + } +} + +fn segment_without_clusters() -> Segment { + let info = Info { + timestamp_scale: TimestampScale(1000000), + muxing_app: MuxingApp("mkv-element".to_string()), + writing_app: WritingApp("integration-test".to_string()), + title: Some(Title("No Clusters Segment".to_string())), + ..Default::default() + }; + + // Create a simple audio track + let audio_track = TrackEntry { + track_number: TrackNumber(1), + track_uid: TrackUid(9876543210), + track_type: TrackType(2), // Audio + codec_id: CodecId("A_OPUS".to_string()), + name: Some(Name("Audio Track".to_string())), + codec_name: Some(CodecName("Opus".to_string())), + audio: Some(Audio { + sampling_frequency: SamplingFrequency(48000.0), + channels: Channels(2), + ..Default::default() + }), + ..Default::default() + }; + + let tracks = Tracks { + track_entry: vec![audio_track], + ..Default::default() + }; + + Segment { + crc32: None, + void: None, + seek_head: vec![], + info, + cluster: vec![], // No clusters + tracks: Some(tracks), + cues: None, + attachments: None, + chapters: None, + tags: vec![], + } +} + +#[test] +fn test_basic_matroska_view() { + // Create a Matroska file with EBML header and a basic segment + let ebml_header = ebml(); + let segment = segment1(); + + // Serialize to bytes + let mut buffer = Vec::new(); + ebml_header.write_to(&mut buffer).unwrap(); + segment.write_to(&mut buffer).unwrap(); + + let mut cursor = Cursor::new(&buffer); + let view = MatroskaView::new(&mut cursor).unwrap(); + assert_eq!(view.ebml.doc_type.as_deref(), Some("matroska")); + assert_eq!(view.segments.len(), 1); + let segment_view = &view.segments[0]; + assert_eq!(segment_view.info.title.as_deref(), Some("Test Segment 1")); + assert_eq!(segment_view.tracks.as_ref().unwrap().track_entry.len(), 1); + assert_ne!(segment_view.first_cluster_position, 0); +} + +#[test] +fn test_segment_without_clusters() { + // Create a Matroska file with EBML header and a segment without clusters + let ebml_header = ebml(); + let segment = segment_without_clusters(); + + // Serialize to bytes + let mut buffer = Vec::new(); + ebml_header.write_to(&mut buffer).unwrap(); + segment.write_to(&mut buffer).unwrap(); + + let mut cursor = Cursor::new(&buffer); + let view = MatroskaView::new(&mut cursor).unwrap(); + assert_eq!(view.ebml.doc_type.as_deref(), Some("matroska")); + assert_eq!(view.segments.len(), 1); + let segment_view = &view.segments[0]; + assert_eq!( + segment_view.info.title.as_deref(), + Some("No Clusters Segment") + ); + assert_eq!(segment_view.tracks.as_ref().unwrap().track_entry.len(), 1); + assert_eq!(segment_view.first_cluster_position, 0); // No clusters present +} + +#[test] +fn test_multiple_segments() { + // Create a Matroska file with EBML header and multiple segments + let ebml_header = ebml(); + let segment1 = segment1(); + let segment2 = segment1.clone(); + let segment3 = segment1.clone(); + let segment4 = segment_without_clusters(); + + // Serialize to bytes + let mut buffer = Vec::new(); + ebml_header.write_to(&mut buffer).unwrap(); + segment1.write_to(&mut buffer).unwrap(); + segment2.write_to(&mut buffer).unwrap(); + segment3.write_to(&mut buffer).unwrap(); + segment4.write_to(&mut buffer).unwrap(); + + let mut cursor = Cursor::new(&buffer); + let view = MatroskaView::new(&mut cursor).unwrap(); + assert_eq!(view.ebml.doc_type.as_deref(), Some("matroska")); + assert_eq!(view.segments.len(), 4, "should have 4 segments"); + + for (i, segment_view) in view.segments.iter().enumerate() { + if i < 3 { + assert_eq!(segment_view.info.title.as_deref(), Some("Test Segment 1")); + assert_eq!(segment_view.tracks.as_ref().unwrap().track_entry.len(), 1); + assert_ne!(segment_view.first_cluster_position, 0); + } else { + assert_eq!( + segment_view.info.title.as_deref(), + Some("No Clusters Segment") + ); + assert_eq!(segment_view.tracks.as_ref().unwrap().track_entry.len(), 1); + assert_eq!(segment_view.first_cluster_position, 0); // No clusters present + } + } +} + +#[test] +fn test_unsize_segment() { + let ebml_header = ebml(); + + let segment_header = Header { + id: Segment::ID, + size: VInt64::new_unknown(), + }; + let segment = segment1(); + let mut buffer = Vec::new(); + ebml_header.write_to(&mut buffer).unwrap(); + segment.write_element(&segment_header, &mut buffer).unwrap(); + let mut cursor = Cursor::new(&buffer); + let view = MatroskaView::new(&mut cursor).unwrap(); + assert_eq!(view.ebml.doc_type.as_deref(), Some("matroska")); + assert_eq!(view.segments.len(), 1); + let segment_view = &view.segments[0]; + assert_eq!(segment_view.info.title.as_deref(), Some("Test Segment 1")); + assert_eq!(segment_view.tracks.as_ref().unwrap().track_entry.len(), 1); + assert_ne!(segment_view.first_cluster_position, 0); +} + +#[cfg(feature = "tokio")] +mod async_tests { + use super::*; + use mkv_element::io::tokio_impl::{AsyncWriteElement, AsyncWriteTo}; + + #[tokio::test] + async fn test_basic_matroska_view_async() { + // Create a Matroska file with EBML header and a basic segment + let ebml_header = ebml(); + let segment = segment1(); + + // Serialize to bytes + let mut buffer = Vec::new(); + ebml_header.async_write_to(&mut buffer).await.unwrap(); + segment.async_write_to(&mut buffer).await.unwrap(); + + let mut cursor = Cursor::new(&buffer); + let view = MatroskaView::new_async(&mut cursor).await.unwrap(); + assert_eq!(view.ebml.doc_type.as_deref(), Some("matroska")); + assert_eq!(view.segments.len(), 1); + let segment_view = &view.segments[0]; + assert_eq!(segment_view.info.title.as_deref(), Some("Test Segment 1")); + assert_eq!(segment_view.tracks.as_ref().unwrap().track_entry.len(), 1); + assert_ne!(segment_view.first_cluster_position, 0); + } + + #[tokio::test] + async fn test_segment_without_clusters_async() { + // Create a Matroska file with EBML header and a segment without clusters + let ebml_header = ebml(); + let segment = segment_without_clusters(); + + // Serialize to bytes + let mut buffer = Vec::new(); + ebml_header.async_write_to(&mut buffer).await.unwrap(); + segment.async_write_to(&mut buffer).await.unwrap(); + + let mut cursor = Cursor::new(&buffer); + let view = MatroskaView::new_async(&mut cursor).await.unwrap(); + assert_eq!(view.ebml.doc_type.as_deref(), Some("matroska")); + assert_eq!(view.segments.len(), 1); + let segment_view = &view.segments[0]; + assert_eq!( + segment_view.info.title.as_deref(), + Some("No Clusters Segment") + ); + assert_eq!(segment_view.tracks.as_ref().unwrap().track_entry.len(), 1); + assert_eq!(segment_view.first_cluster_position, 0); // No clusters present + } + + #[tokio::test] + async fn test_multiple_segments_async() { + // Create a Matroska file with EBML header and multiple segments + let ebml_header = ebml(); + let segment1 = segment1(); + let segment2 = segment1.clone(); + let segment3 = segment1.clone(); + let segment4 = segment_without_clusters(); + + // Serialize to bytes + let mut buffer = Vec::new(); + ebml_header.async_write_to(&mut buffer).await.unwrap(); + segment1.async_write_to(&mut buffer).await.unwrap(); + segment2.async_write_to(&mut buffer).await.unwrap(); + segment3.async_write_to(&mut buffer).await.unwrap(); + segment4.async_write_to(&mut buffer).await.unwrap(); + + let mut cursor = Cursor::new(&buffer); + let view = MatroskaView::new_async(&mut cursor).await.unwrap(); + assert_eq!(view.ebml.doc_type.as_deref(), Some("matroska")); + assert_eq!(view.segments.len(), 4, "should have 4 segments"); + + for (i, segment_view) in view.segments.iter().enumerate() { + if i < 3 { + assert_eq!(segment_view.info.title.as_deref(), Some("Test Segment 1")); + assert_eq!(segment_view.tracks.as_ref().unwrap().track_entry.len(), 1); + assert_ne!(segment_view.first_cluster_position, 0); + } else { + assert_eq!( + segment_view.info.title.as_deref(), + Some("No Clusters Segment") + ); + assert_eq!(segment_view.tracks.as_ref().unwrap().track_entry.len(), 1); + assert_eq!(segment_view.first_cluster_position, 0); // No clusters present + } + } + } + + #[tokio::test] + async fn test_unsize_segment_async() { + let ebml_header = ebml(); + + let segment_header = Header { + id: Segment::ID, + size: VInt64::new_unknown(), + }; + let segment = segment1(); + let mut buffer = Vec::new(); + ebml_header.async_write_to(&mut buffer).await.unwrap(); + segment + .async_write_element(&segment_header, &mut buffer) + .await + .unwrap(); + let mut cursor = Cursor::new(&buffer); + let view = MatroskaView::new_async(&mut cursor).await.unwrap(); + assert_eq!(view.ebml.doc_type.as_deref(), Some("matroska")); + assert_eq!(view.segments.len(), 1); + let segment_view = &view.segments[0]; + assert_eq!(segment_view.info.title.as_deref(), Some("Test Segment 1")); + assert_eq!(segment_view.tracks.as_ref().unwrap().track_entry.len(), 1); + assert_ne!(segment_view.first_cluster_position, 0); + } +}