From af18492217643c3c9885b64f292a7332d061a065 Mon Sep 17 00:00:00 2001 From: b01o Date: Sat, 4 Oct 2025 21:15:52 +0800 Subject: [PATCH 1/6] Adds Matroska view functionality Adds a `view` module with `MatroskaView` and `SegmentView` structs. --- src/lib.rs | 1 + src/view.rs | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 src/view.rs diff --git a/src/lib.rs b/src/lib.rs index 63a355f..2610150 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,7 @@ mod supplement; // Supplementary elements in Matroska. Void elements, CRC-32, et // following modules are public pub mod io; +pub mod view; // Re-export common types pub use crate::frame::*; diff --git a/src/view.rs b/src/view.rs new file mode 100644 index 0000000..16b03d6 --- /dev/null +++ b/src/view.rs @@ -0,0 +1,16 @@ +//! A View of a Matroska file, parsing w/o loading clusters into memory. + +use crate::master::*; + +/// View of a Matroska file, parsing the EBML and Segment headers, but not loading Clusters. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MatroskaView { + /// The EBML header. + pub ebml: Ebml, + /// The Segment views, as there can be multiple segments in a Matroska file. + pub segment: Vec, +} + +/// View of a Segment, parsing the Segment header, but not loading Clusters. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SegmentView; From 0a6555c82ae4f11e7051ee8a95996f1aef70b335 Mon Sep 17 00:00:00 2001 From: b01o Date: Sat, 4 Oct 2025 21:28:38 +0800 Subject: [PATCH 2/6] Adds `MatroskaView` for efficient parsing Introduces `MatroskaView` and `SegmentView` structs to enable parsing Matroska files without loading Cluster data into memory, improving performance. The implementation includes both synchronous and asynchronous (tokio) versions for flexible integration. --- src/view.rs | 288 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 285 insertions(+), 3 deletions(-) diff --git a/src/view.rs b/src/view.rs index 16b03d6..acbbd63 100644 --- a/src/view.rs +++ b/src/view.rs @@ -1,9 +1,10 @@ //! A View of a Matroska file, parsing w/o loading clusters into memory. +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, Eq)] +#[derive(Debug, Clone, PartialEq)] pub struct MatroskaView { /// The EBML header. pub ebml: Ebml, @@ -11,6 +12,287 @@ pub struct MatroskaView { pub segment: 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)?; + + let mut segments = Vec::new(); + + // Parse all segments in the file + while let Ok(segment) = SegmentView::new(reader) { + segments.push(segment); + } + + // At least one segment is required + if segments.is_empty() { + return Err(crate::Error::MissingElement(Segment::ID)); + } + + Ok(MatroskaView { + ebml, + segment: 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?; + + let mut segments = Vec::new(); + + // Parse all segments in the file + while let Ok(segment) = SegmentView::new_async(reader).await { + segments.push(segment); + } + + // At least one segment is required + if segments.is_empty() { + return Err(crate::Error::MissingElement(Segment::ID)); + } + + Ok(MatroskaView { + ebml, + segment: segments, + }) + } +} + /// View of a Segment, parsing the Segment header, but not loading Clusters. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SegmentView; +#[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 first Cluster in the Segment. + pub first_cluster_position: u64, + /// The position of the Segment data (after the Segment header). + pub segment_data_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, + { + use crate::io::blocking_impl::*; + + // 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 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 = None; + + // Parse segment elements + loop { + let current_position = reader.stream_position()?; + + // Check if we've reached the end of the segment + if let Ok(header) = crate::base::Header::read_from(reader) { + match header.id { + SeekHead::ID => { + let element = SeekHead::read_element(&header, reader)?; + seek_head.push(element); + } + Info::ID => { + let element = Info::read_element(&header, reader)?; + info = Some(element); + } + Tracks::ID => { + let element = Tracks::read_element(&header, reader)?; + tracks = Some(element); + } + Cues::ID => { + let element = Cues::read_element(&header, reader)?; + cues = Some(element); + } + Attachments::ID => { + let element = Attachments::read_element(&header, reader)?; + attachments = Some(element); + } + Chapters::ID => { + let element = Chapters::read_element(&header, reader)?; + chapters = Some(element); + } + Tags::ID => { + let element = Tags::read_element(&header, reader)?; + tags.push(element); + } + Cluster::ID => { + // Found the first cluster, record its position and stop parsing + if first_cluster_position.is_none() { + first_cluster_position = Some(current_position); + } + break; + } + _ => { + 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); + } + } + } else { + // End of stream or error reading header + break; + } + } + + // Info is required in a valid Matroska file + let info = info.ok_or(crate::Error::MissingElement(Info::ID))?; + + Ok(SegmentView { + seek_head, + info, + tracks, + cues, + attachments, + chapters, + tags, + first_cluster_position: first_cluster_position.unwrap_or(0), + segment_data_position, + }) + } + + /// 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, + { + 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 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 = None; + + // Parse segment elements + loop { + let current_position = reader.stream_position().await?; + + // Check if we've reached the end of the segment + if let Ok(header) = crate::base::Header::async_read_from(reader).await { + match header.id { + SeekHead::ID => { + let element = SeekHead::async_read_element(&header, reader).await?; + seek_head.push(element); + } + Info::ID => { + let element = Info::async_read_element(&header, reader).await?; + info = Some(element); + } + Tracks::ID => { + let element = Tracks::async_read_element(&header, reader).await?; + tracks = Some(element); + } + Cues::ID => { + let element = Cues::async_read_element(&header, reader).await?; + cues = Some(element); + } + Attachments::ID => { + let element = Attachments::async_read_element(&header, reader).await?; + attachments = Some(element); + } + Chapters::ID => { + let element = Chapters::async_read_element(&header, reader).await?; + chapters = Some(element); + } + Tags::ID => { + let element = Tags::async_read_element(&header, reader).await?; + tags.push(element); + } + Cluster::ID => { + // Found the first cluster, record its position and stop parsing + if first_cluster_position.is_none() { + first_cluster_position = Some(current_position); + } + break; + } + _ => { + 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); + } + } + } else { + // End of stream or error reading header + break; + } + } + + // Info is required in a valid Matroska file + let info = info.ok_or(crate::Error::MissingElement(Info::ID))?; + + Ok(SegmentView { + seek_head, + info, + tracks, + cues, + attachments, + chapters, + tags, + first_cluster_position: first_cluster_position.unwrap_or(0), + segment_data_position, + }) + } +} From 5cd4546ae9fc3a9d813290498fcca62f5be355f0 Mon Sep 17 00:00:00 2001 From: b01o Date: Mon, 6 Oct 2025 13:18:30 +0800 Subject: [PATCH 3/6] Enables parsing of multiple segments Refactors segment parsing to handle multiple segments within a single Matroska file. Now returns a `Vec` from the `SegmentView::new` and `SegmentView::new_async` functions. Also implements logic to find next segment using SeekHead information to skip to the next segment when a Cluster is encountered. --- src/view.rs | 305 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 191 insertions(+), 114 deletions(-) diff --git a/src/view.rs b/src/view.rs index acbbd63..e3acdd7 100644 --- a/src/view.rs +++ b/src/view.rs @@ -24,12 +24,8 @@ impl MatroskaView { // Read the EBML header let ebml = Ebml::read_from(reader)?; - let mut segments = Vec::new(); - // Parse all segments in the file - while let Ok(segment) = SegmentView::new(reader) { - segments.push(segment); - } + let segments = SegmentView::new(reader)?; // At least one segment is required if segments.is_empty() { @@ -55,17 +51,8 @@ impl MatroskaView { // Read the EBML header let ebml = Ebml::async_read_from(reader).await?; - let mut segments = Vec::new(); - // Parse all segments in the file - while let Ok(segment) = SegmentView::new_async(reader).await { - segments.push(segment); - } - - // At least one segment is required - if segments.is_empty() { - return Err(crate::Error::MissingElement(Segment::ID)); - } + let segments = SegmentView::new_async(reader).await?; Ok(MatroskaView { ebml, @@ -100,11 +87,14 @@ pub struct SegmentView { 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 + 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)?; @@ -112,7 +102,7 @@ impl SegmentView { return Err(crate::Error::MissingElement(Segment::ID)); } - let segment_data_position = reader.stream_position()?; + let mut segment_data_position = reader.stream_position()?; let mut seek_head = Vec::new(); let mut info = None; @@ -125,64 +115,101 @@ impl SegmentView { // 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.is_none() { + first_cluster_position = Some(current_position); + } // Check if we've reached the end of the segment - if let Ok(header) = crate::base::Header::read_from(reader) { - match header.id { - SeekHead::ID => { - let element = SeekHead::read_element(&header, reader)?; - seek_head.push(element); - } - Info::ID => { - let element = Info::read_element(&header, reader)?; - info = Some(element); - } - Tracks::ID => { - let element = Tracks::read_element(&header, reader)?; - tracks = Some(element); - } - Cues::ID => { - let element = Cues::read_element(&header, reader)?; - cues = Some(element); - } - Attachments::ID => { - let element = Attachments::read_element(&header, reader)?; - attachments = Some(element); - } - Chapters::ID => { - let element = Chapters::read_element(&header, reader)?; - chapters = Some(element); - } - Tags::ID => { - let element = Tags::read_element(&header, reader)?; - tags.push(element); - } - Cluster::ID => { - // Found the first cluster, record its position and stop parsing - if first_cluster_position.is_none() { - first_cluster_position = Some(current_position); - } + 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; + if seek_head.is_empty() { break; } - _ => { - 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); + + 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.unwrap()) + { + reader.seek(SeekFrom::Start(pos.1))?; + continue; + } else { + break; } } - } else { - // End of stream or error reading header - break; + Segment::ID => { + out.push(SegmentView { + seek_head, + // Info is required in a valid Matroska file + info: info.ok_or(crate::Error::MissingElement(Info::ID))?, + tracks, + cues, + attachments, + chapters, + tags, + first_cluster_position: first_cluster_position + .ok_or(crate::Error::MissingElement(Cluster::ID))?, + segment_data_position, + }); + + segment_data_position = reader.stream_position()?; + + seek_head = Vec::new(); + info = None; + tracks = None; + cues = None; + attachments = None; + chapters = None; + tags = Vec::new(); + first_cluster_position = None; + } + _ => { + 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))?; - Ok(SegmentView { + out.push(SegmentView { seek_head, info, tracks, @@ -190,19 +217,23 @@ impl SegmentView { attachments, chapters, tags, - first_cluster_position: first_cluster_position.unwrap_or(0), + first_cluster_position: first_cluster_position + .ok_or(crate::Error::MissingElement(Cluster::ID))?, 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 + 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; @@ -212,7 +243,7 @@ impl SegmentView { return Err(crate::Error::MissingElement(Segment::ID)); } - let segment_data_position = reader.stream_position().await?; + let mut segment_data_position = reader.stream_position().await?; let mut seek_head = Vec::new(); let mut info = None; @@ -225,65 +256,109 @@ impl SegmentView { // 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.is_none() { + first_cluster_position = Some(current_position); + } // Check if we've reached the end of the segment - if let Ok(header) = crate::base::Header::async_read_from(reader).await { - match header.id { - SeekHead::ID => { - let element = SeekHead::async_read_element(&header, reader).await?; - seek_head.push(element); - } - Info::ID => { - let element = Info::async_read_element(&header, reader).await?; - info = Some(element); - } - Tracks::ID => { - let element = Tracks::async_read_element(&header, reader).await?; - tracks = Some(element); - } - Cues::ID => { - let element = Cues::async_read_element(&header, reader).await?; - cues = Some(element); - } - Attachments::ID => { - let element = Attachments::async_read_element(&header, reader).await?; - attachments = Some(element); - } - Chapters::ID => { - let element = Chapters::async_read_element(&header, reader).await?; - chapters = Some(element); - } - Tags::ID => { - let element = Tags::async_read_element(&header, reader).await?; - tags.push(element); - } - Cluster::ID => { - // Found the first cluster, record its position and stop parsing - if first_cluster_position.is_none() { - first_cluster_position = Some(current_position); - } + 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; + if seek_head.is_empty() { break; } - _ => { - 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); + + 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.unwrap()) + { + reader.seek(std::io::SeekFrom::Start(pos.1)).await?; + continue; + } else { + break; } } - } else { - // End of stream or error reading header - break; + Segment::ID => { + out.push(SegmentView { + seek_head, + // Info is required in a valid Matroska file + info: info.ok_or(crate::Error::MissingElement(Info::ID))?, + tracks, + cues, + attachments, + chapters, + tags, + first_cluster_position: first_cluster_position + .ok_or(crate::Error::MissingElement(Cluster::ID))?, + segment_data_position, + }); + + segment_data_position = reader.stream_position().await?; + + seek_head = Vec::new(); + info = None; + tracks = None; + cues = None; + attachments = None; + chapters = None; + tags = Vec::new(); + first_cluster_position = None; + } + _ => { + 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))?; - Ok(SegmentView { + out.push(SegmentView { seek_head, info, tracks, @@ -291,8 +366,10 @@ impl SegmentView { attachments, chapters, tags, - first_cluster_position: first_cluster_position.unwrap_or(0), + first_cluster_position: first_cluster_position + .ok_or(crate::Error::MissingElement(Cluster::ID))?, segment_data_position, - }) + }); + Ok(out) } } From b55988bc1f411a8ff2de155cad434300cbcf72b8 Mon Sep 17 00:00:00 2001 From: b01o Date: Thu, 9 Oct 2025 10:30:51 +0800 Subject: [PATCH 4/6] Adds Matroska view integration tests Adds integration tests for the Matroska view functionality, including tests for basic segment parsing, segments without clusters, multiple segments, and un-sized segments. This ensures that the view parsing logic correctly handles various Matroska file structures. Also introduces `WriteElement` trait for writing structs from the `element` module. --- src/io.rs | 31 ++++++ src/view.rs | 95 +++++++--------- tests/view_integration.rs | 221 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 289 insertions(+), 58 deletions(-) create mode 100644 tests/view_integration.rs 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/view.rs b/src/view.rs index e3acdd7..2d69b71 100644 --- a/src/view.rs +++ b/src/view.rs @@ -1,5 +1,7 @@ //! A View of a Matroska file, parsing w/o loading clusters into memory. +use std::mem::take; + use crate::element::Element; use crate::master::*; @@ -9,7 +11,7 @@ pub struct MatroskaView { /// The EBML header. pub ebml: Ebml, /// The Segment views, as there can be multiple segments in a Matroska file. - pub segment: Vec, + pub segments: Vec, } impl MatroskaView { @@ -32,10 +34,7 @@ impl MatroskaView { return Err(crate::Error::MissingElement(Segment::ID)); } - Ok(MatroskaView { - ebml, - segment: segments, - }) + Ok(MatroskaView { ebml, segments }) } /// Create a new MatroskaView by parsing the EBML header and all Segment headers, @@ -54,10 +53,7 @@ impl MatroskaView { // Parse all segments in the file let segments = SegmentView::new_async(reader).await?; - Ok(MatroskaView { - ebml, - segment: segments, - }) + Ok(MatroskaView { ebml, segments }) } } @@ -78,10 +74,10 @@ pub struct SegmentView { 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 first Cluster in the Segment. - pub first_cluster_position: u64, /// 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 { @@ -111,7 +107,7 @@ impl SegmentView { let mut attachments = None; let mut chapters = None; let mut tags = Vec::new(); - let mut first_cluster_position = None; + let mut first_cluster_position = 0; // Parse segment elements loop { @@ -121,8 +117,8 @@ impl SegmentView { let Ok(header) = Header::read_from(reader) else { break; }; - if header.id == Cluster::ID && first_cluster_position.is_none() { - first_cluster_position = Some(current_position); + if header.id == Cluster::ID && first_cluster_position == 0 { + first_cluster_position = current_position; } // Check if we've reached the end of the segment @@ -136,12 +132,7 @@ impl SegmentView { Tags::ID => tags.push(Tags::read_element(&header, reader)?), Cluster::ID => { // try to skip, or else break - use crate::base::VInt64; - if seek_head.is_empty() { - break; - } - let mut seeks: Vec<(VInt64, u64)> = seek_head .iter() .flat_map(|sh| { @@ -158,43 +149,37 @@ impl SegmentView { }) }) .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.unwrap()) - { + if let Some(pos) = seeks.iter().find(|(_, pos)| *pos > first_cluster_position) { reader.seek(SeekFrom::Start(pos.1))?; continue; - } else { + } + + 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, + seek_head: take(&mut seek_head), // Info is required in a valid Matroska file - info: info.ok_or(crate::Error::MissingElement(Info::ID))?, - tracks, - cues, - attachments, - chapters, - tags, - first_cluster_position: first_cluster_position - .ok_or(crate::Error::MissingElement(Cluster::ID))?, - segment_data_position, + 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()?; - - seek_head = Vec::new(); - info = None; - tracks = None; - cues = None; - attachments = None; - chapters = None; - tags = Vec::new(); - first_cluster_position = None; } _ => { use log::warn; @@ -217,8 +202,7 @@ impl SegmentView { attachments, chapters, tags, - first_cluster_position: first_cluster_position - .ok_or(crate::Error::MissingElement(Cluster::ID))?, + first_cluster_position, segment_data_position, }); Ok(out) @@ -252,7 +236,7 @@ impl SegmentView { let mut attachments = None; let mut chapters = None; let mut tags = Vec::new(); - let mut first_cluster_position = None; + let mut first_cluster_position = 0; // Parse segment elements loop { @@ -262,8 +246,8 @@ impl SegmentView { let Ok(header) = Header::async_read_from(reader).await else { break; }; - if header.id == Cluster::ID && first_cluster_position.is_none() { - first_cluster_position = Some(current_position); + if header.id == Cluster::ID && first_cluster_position == 0 { + first_cluster_position = current_position; } // Check if we've reached the end of the segment @@ -309,10 +293,7 @@ impl SegmentView { .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.unwrap()) - { + if let Some(pos) = seeks.iter().find(|(_, pos)| *pos > first_cluster_position) { reader.seek(std::io::SeekFrom::Start(pos.1)).await?; continue; } else { @@ -329,8 +310,7 @@ impl SegmentView { attachments, chapters, tags, - first_cluster_position: first_cluster_position - .ok_or(crate::Error::MissingElement(Cluster::ID))?, + first_cluster_position, segment_data_position, }); @@ -343,7 +323,7 @@ impl SegmentView { attachments = None; chapters = None; tags = Vec::new(); - first_cluster_position = None; + first_cluster_position = 0; } _ => { use log::warn; @@ -366,8 +346,7 @@ impl SegmentView { attachments, chapters, tags, - first_cluster_position: first_cluster_position - .ok_or(crate::Error::MissingElement(Cluster::ID))?, + 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..c418d4f --- /dev/null +++ b/tests/view_integration.rs @@ -0,0 +1,221 @@ +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); +} From 8dbfa3365bc155763e3786302f431fe48468ca49 Mon Sep 17 00:00:00 2001 From: b01o Date: Thu, 9 Oct 2025 10:37:04 +0800 Subject: [PATCH 5/6] Improves segment view parsing and adds async tests Refactors the segment view parsing logic to correctly handle edge cases, such as segments without clusters and segments with unknown sizes. Specifically, it skips to the end of the segment if a cluster is not found and the segment size is known. Adds comprehensive asynchronous tests to validate the functionality of MatroskaView, including handling multiple segments, segments without clusters, and segments with unknown sizes. --- src/view.rs | 43 ++++++-------- tests/view_integration.rs | 114 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 25 deletions(-) diff --git a/src/view.rs b/src/view.rs index 2d69b71..6ad1c99 100644 --- a/src/view.rs +++ b/src/view.rs @@ -267,12 +267,7 @@ impl SegmentView { Tags::ID => tags.push(Tags::async_read_element(&header, reader).await?), Cluster::ID => { // try to skip, or else break - use crate::base::VInt64; - if seek_head.is_empty() { - break; - } - let mut seeks: Vec<(VInt64, u64)> = seek_head .iter() .flat_map(|sh| { @@ -291,39 +286,37 @@ impl SegmentView { }) }) .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; - } else { + } + + 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, + seek_head: take(&mut seek_head), // Info is required in a valid Matroska file - info: info.ok_or(crate::Error::MissingElement(Info::ID))?, - tracks, - cues, - attachments, - chapters, - tags, - first_cluster_position, - segment_data_position, + 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?; - - seek_head = Vec::new(); - info = None; - tracks = None; - cues = None; - attachments = None; - chapters = None; - tags = Vec::new(); - first_cluster_position = 0; } _ => { use log::warn; diff --git a/tests/view_integration.rs b/tests/view_integration.rs index c418d4f..e73f367 100644 --- a/tests/view_integration.rs +++ b/tests/view_integration.rs @@ -219,3 +219,117 @@ fn test_unsize_segment() { 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); + } +} From ce2b0ac024c42eabcba5dbf28db4b73e55f85cd2 Mon Sep 17 00:00:00 2001 From: b01o Date: Thu, 9 Oct 2025 10:43:32 +0800 Subject: [PATCH 6/6] Adds `utils` feature for Matroska file utilities Enables a new `utils` feature that introduces utility modules, such as the `view` module, for easier interaction with Matroska files. The `view` module provides `MatroskaView` and `SegmentView` structs, designed for efficiently parsing MKV file metadata without the need to load the entire cluster data into memory, which is useful for large files. Also enables the `utils` feature in the integration tests. --- Cargo.toml | 3 ++- README.md | 19 +++++++++++++++++++ src/lib.rs | 3 +++ tests/view_integration.rs | 2 ++ 4 files changed, 26 insertions(+), 1 deletion(-) 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/lib.rs b/src/lib.rs index 2610150..0d09685 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,9 @@ 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 diff --git a/tests/view_integration.rs b/tests/view_integration.rs index e73f367..39e4b1e 100644 --- a/tests/view_integration.rs +++ b/tests/view_integration.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "utils")] + use mkv_element::io::blocking_impl::{WriteElement, WriteTo}; use mkv_element::prelude::*; use mkv_element::view::MatroskaView;