diff options
author | metamuffin <metamuffin@disroot.org> | 2024-01-28 03:21:03 +0100 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2024-01-28 03:21:03 +0100 |
commit | ce9eb140ab9243d1c87ace4727a82b7fa50f964b (patch) | |
tree | b64da94ba58f72a2371cdd8644dc48a178b7d260 | |
parent | b514ec8cea2c2143e0bd7a0eb377c96a6f091d0d (diff) | |
download | jellything-ce9eb140ab9243d1c87ace4727a82b7fa50f964b.tar jellything-ce9eb140ab9243d1c87ace4727a82b7fa50f964b.tar.bz2 jellything-ce9eb140ab9243d1c87ace4727a82b7fa50f964b.tar.zst |
fix yet another fundamental issue in the ebml reader and seekindex.
-rw-r--r-- | common/src/seek_index.rs | 2 | ||||
-rw-r--r-- | ebml_derive/src/lib.rs | 2 | ||||
-rw-r--r-- | matroska/src/bin/experiment.rs | 33 | ||||
-rw-r--r-- | matroska/src/bin/mkvdump.rs | 8 | ||||
-rw-r--r-- | matroska/src/read.rs | 62 | ||||
-rw-r--r-- | matroska/src/unflatten.rs | 24 | ||||
-rw-r--r-- | remuxer/src/extract.rs | 24 | ||||
-rw-r--r-- | remuxer/src/import/mod.rs | 4 | ||||
-rw-r--r-- | remuxer/src/remux.rs | 6 | ||||
-rw-r--r-- | remuxer/src/seek_index.rs | 44 | ||||
-rw-r--r-- | remuxer/src/segment_extractor.rs | 4 | ||||
-rw-r--r-- | stream/src/webvtt.rs | 15 | ||||
-rw-r--r-- | transcoder/src/subtitles.rs | 12 |
13 files changed, 119 insertions, 121 deletions
diff --git a/common/src/seek_index.rs b/common/src/seek_index.rs index fb41b40..d58cd15 100644 --- a/common/src/seek_index.rs +++ b/common/src/seek_index.rs @@ -18,7 +18,7 @@ pub struct SeekIndex { pub struct BlockIndex { pub pts: u64, // pub duration: Option<u64>, - pub source_off: usize, + pub source_off: u64, // points to start of SimpleBlock or BlockGroup (not the Block inside it) pub size: usize, } diff --git a/ebml_derive/src/lib.rs b/ebml_derive/src/lib.rs index 6b650f1..ab6cff8 100644 --- a/ebml_derive/src/lib.rs +++ b/ebml_derive/src/lib.rs @@ -116,7 +116,7 @@ pub fn define_ebml(ts: TokenStream) -> TokenStream { #(#enum_variants),* } impl MatroskaTag { - /// returns path in **reverse** order or None if global. + /// returns path in order top-to-immediate-parent order or None if global. pub fn path(&self) -> Option<&'static [u64]> { match self { #(#path_match),* } } diff --git a/matroska/src/bin/experiment.rs b/matroska/src/bin/experiment.rs deleted file mode 100644 index e185787..0000000 --- a/matroska/src/bin/experiment.rs +++ /dev/null @@ -1,33 +0,0 @@ -/* - This file is part of jellything (https://codeberg.org/metamuffin/jellything) - which is licensed under the GNU Affero General Public License (version 3); see /COPYING. - Copyright (C) 2024 metamuffin <metamuffin.org> -*/ -use jellymatroska::{ - matroska::MatroskaTag, read::EbmlReader, unflatten::IterWithPos, write::EbmlWriter, -}; -use std::{ - fs::File, - io::{stdout, BufReader, BufWriter}, -}; - -fn main() { - env_logger::init_from_env("LOG"); - let path = std::env::args().nth(1).unwrap(); - let mut r = EbmlReader::new(BufReader::new(File::open(path).unwrap())); - let mut w = EbmlWriter::new(BufWriter::new(stdout()), 0); - - // r.seek( - // 631147167 + 52, - // ebml::matroska::MatroskaTag::Cues(Master::Start), - // ) - // .unwrap(); - - while let Some(tag) = r.next() { - let tag = tag.unwrap(); - if MatroskaTag::is_master(tag.id()).unwrap() { - eprintln!("{tag:?}"); - } - w.write_tag(&tag).unwrap(); - } -} diff --git a/matroska/src/bin/mkvdump.rs b/matroska/src/bin/mkvdump.rs index 9e35716..27b4849 100644 --- a/matroska/src/bin/mkvdump.rs +++ b/matroska/src/bin/mkvdump.rs @@ -3,9 +3,7 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2024 metamuffin <metamuffin.org> */ -use jellymatroska::{ - block::Block, matroska::MatroskaTag, read::EbmlReader, unflatten::IterWithPos, -}; +use jellymatroska::{block::Block, matroska::MatroskaTag, read::EbmlReader}; use std::{fs::File, io::BufReader}; fn main() { @@ -14,13 +12,13 @@ fn main() { let mut r = EbmlReader::new(BufReader::new(File::open(path).unwrap())); while let Some(tag) = r.next() { - let tag = tag.unwrap(); + let (position, tag) = tag.unwrap(); match tag { MatroskaTag::SimpleBlock(b) | MatroskaTag::Block(b) => { let b = Block::parse(&b).unwrap(); println!("block kf={} ts_off={}", b.keyframe, b.timestamp_off) } - _ => println!("{} {tag:?}", r.position), + _ => println!("{} {tag:?}", position.unwrap_or(0)), } } } diff --git a/matroska/src/read.rs b/matroska/src/read.rs index 3e858f7..68a8efa 100644 --- a/matroska/src/read.rs +++ b/matroska/src/read.rs @@ -5,7 +5,7 @@ */ use crate::error::Error; use crate::Result; -use crate::{matroska::MatroskaTag, size::EbmlSize, unflatten::IterWithPos, Master}; +use crate::{matroska::MatroskaTag, size::EbmlSize, Master}; use log::{debug, warn}; use std::{ collections::VecDeque, @@ -17,21 +17,21 @@ impl<T: Read + Seek> ReadAndSeek for T {} #[derive(Debug, Clone, Copy)] pub struct StackTag { - end: Option<usize>, + end: Option<u64>, id: u64, } pub struct EbmlReader { inner: Box<dyn ReadAndSeek>, stack: Vec<StackTag>, - queue: VecDeque<MatroskaTag>, - pub position: usize, + queue: VecDeque<(Option<u64>, MatroskaTag)>, + position: u64, } impl Read for EbmlReader { fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { let r = self.inner.read(buf)?; - self.position += r; + self.position += r as u64; Ok(r) } } @@ -53,15 +53,15 @@ impl EbmlReader { self.position += 1; Ok(b[0]) } - + pub fn read_buf(&mut self, size: impl Into<usize>) -> Result<Vec<u8>> { let size = size.into(); let mut b = vec![0u8; size]; self.inner.read_exact(&mut b).map_err(Error::Io)?; - self.position += size; + self.position += size as u64; Ok(b) } - + pub fn read_vint_len(&mut self) -> Result<(u64, usize)> { let s = self.read_byte()?; let len = s.leading_zeros() + 1; @@ -76,24 +76,24 @@ impl EbmlReader { } Ok((value, len as usize)) } - + #[inline] pub fn read_vint(&mut self) -> Result<u64> { Ok(self.read_vint_len()?.0) } - + #[inline] pub fn read_utf8(&mut self, size: impl Into<usize>) -> Result<String> { let b = self.read_buf(size)?; Ok(String::from_utf8(b).map_err(|_| Error::InvalidUTF8)?) } - + #[inline] pub fn read_tag_id(&mut self) -> Result<u64> { let (value, len) = self.read_vint_len()?; Ok(value + (1 << (7 * len))) } - + #[inline] pub fn read_tag_size(&mut self) -> Result<EbmlSize> { Ok(EbmlSize::from_vint(self.read_vint_len()?)) @@ -109,7 +109,7 @@ impl EbmlReader { } self.stack.pop(); self.queue - .push_back(MatroskaTag::construct_master(e.id, Master::End)?); + .push_back((None, MatroskaTag::construct_master(e.id, Master::End)?)); } else { break; } @@ -118,6 +118,7 @@ impl EbmlReader { } } + let start_position = self.position; let id = self.read_tag_id()?; let size = self.read_tag_size()?; let is_master = MatroskaTag::is_master(id)?; @@ -131,30 +132,29 @@ impl EbmlReader { if let Some(path) = tag.path() { // we have slightly different rules for closing tags implicitly // this closes as many tags as needed to make the next tag a valid child - while let Some(tag @ StackTag { end: None, .. }) = self.stack.last() { - if path.last() == Some(&tag.id) { + while let Some(stag @ StackTag { end: None, .. }) = self.stack.last() { + if path.last() == Some(&stag.id) { break; } else { - self.queue.push_back(MatroskaTag::construct_master( - self.stack.pop().unwrap().id, - Master::End, - )?); + let end = + MatroskaTag::construct_master(self.stack.pop().unwrap().id, Master::End)?; + self.queue.push_back((None, end)); } } } if is_master { self.stack.push(StackTag { - end: size.some().map(|s| s + self.position), + end: size.some().map(|s| s as u64 + self.position), id, }); } - self.queue.push_back(tag); + self.queue.push_back((Some(start_position), tag)); Ok(()) } /// context should be the next expected tag, such that the stack can be derived from its path. - pub fn seek(&mut self, position: usize, context: MatroskaTag) -> Result<()> { + pub fn seek(&mut self, position: u64, context: MatroskaTag) -> Result<()> { let path = context.path().ok_or(Error::GlobalTagsAsContext)?; debug!( "seeking to {position} with a context restored from path {:x?}", @@ -162,9 +162,7 @@ impl EbmlReader { ); self.queue.clear(); self.position = position; - self.inner - .seek(SeekFrom::Start(position as u64)) - .map_err(Error::Io)?; + self.inner.seek(SeekFrom::Start(position as u64))?; self.stack = path .iter() .map(|id| StackTag { id: *id, end: None }) @@ -173,13 +171,8 @@ impl EbmlReader { } } -impl IterWithPos for EbmlReader { - type Item = Result<MatroskaTag>; - - fn position(&self) -> usize { - self.position - } - +impl Iterator for EbmlReader { + type Item = Result<(Option<u64>, MatroskaTag)>; fn next(&mut self) -> Option<Self::Item> { if let Some(t) = self.queue.pop_front() { // match t { @@ -198,7 +191,10 @@ impl IterWithPos for EbmlReader { match self.queue.pop_front() { Some(q) => Some(Ok(q)), None => match self.stack.pop() { - Some(q) => Some(MatroskaTag::construct_master(q.id, Master::End)), + Some(q) => Some(Ok(( + None, + MatroskaTag::construct_master(q.id, Master::End).unwrap(), + ))), None => Some(Err(e)), }, } diff --git a/matroska/src/unflatten.rs b/matroska/src/unflatten.rs index 71089f4..944d2c7 100644 --- a/matroska/src/unflatten.rs +++ b/matroska/src/unflatten.rs @@ -3,30 +3,23 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2024 metamuffin <metamuffin.org> */ -use crate::{matroska::MatroskaTag, Master}; use crate::Result; - - -pub trait IterWithPos { - type Item; - fn next(&mut self) -> Option<Self::Item>; - fn position(&self) -> usize; -} +use crate::{matroska::MatroskaTag, Master}; pub struct Unflat<'a> { pub item: MatroskaTag, pub children: Option<Unflatten<'a>>, - pub position: usize, + pub position: Option<u64>, } pub struct Unflatten<'a> { - inner: &'a mut dyn IterWithPos<Item = Result<MatroskaTag>>, + inner: &'a mut dyn Iterator<Item = Result<(Option<u64>, MatroskaTag)>>, stop: bool, end: Option<MatroskaTag>, } impl<'a> Unflatten<'a> { - pub fn new(inner: &'a mut dyn IterWithPos<Item = Result<MatroskaTag>>) -> Self { + pub fn new(inner: &'a mut dyn Iterator<Item = Result<(Option<u64>, MatroskaTag)>>) -> Self { Self { inner, stop: false, @@ -34,7 +27,7 @@ impl<'a> Unflatten<'a> { } } pub fn new_with_end( - inner: &'a mut dyn IterWithPos<Item = Result<MatroskaTag>>, + inner: &'a mut dyn Iterator<Item = Result<(Option<u64>, MatroskaTag)>>, start: MatroskaTag, ) -> Self { Self { @@ -47,19 +40,14 @@ impl<'a> Unflatten<'a> { self.stop = true; } - pub fn position(&self) -> usize { - self.inner.position() - } - pub fn n(&mut self) -> Option<Result<Unflat>> { if self.stop { return None; } - let position = self.inner.position(); match self.inner.next() { None => None, Some(Err(e)) => Some(Err(e)), - Some(Ok(item)) => { + Some(Ok((position, item))) => { let master = MatroskaTag::is_master(item.id()).unwrap(); if Some(&item) == self.end.as_ref() { self.stop = true; diff --git a/remuxer/src/extract.rs b/remuxer/src/extract.rs index 108b76c..1b2b50c 100644 --- a/remuxer/src/extract.rs +++ b/remuxer/src/extract.rs @@ -6,8 +6,8 @@ use crate::seek_index::get_seek_index; use anyhow::{anyhow, bail}; use jellycommon::LocalTrack; -use jellymatroska::{block::Block, read::EbmlReader, unflatten::IterWithPos, Master, MatroskaTag}; -use log::{debug, info}; +use jellymatroska::{block::Block, read::EbmlReader, Master, MatroskaTag}; +use log::debug; use std::{fs::File, io::BufReader, path::PathBuf}; pub fn extract_track( @@ -20,12 +20,16 @@ pub fn extract_track( let index = get_seek_index(&source_path)?; let index = index .get(&(track_info.track as u64)) - .ok_or(anyhow!("track missing 4"))?; + .ok_or(anyhow!("track missing"))?; let mut out = Vec::new(); for b in &index.blocks { - reader.seek(b.source_off, MatroskaTag::Cluster(Master::Start))?; + reader.seek(b.source_off, MatroskaTag::BlockGroup(Master::Start))?; let (duration, block) = read_group(&mut reader)?; + assert_eq!( + track_info.track, block.track as usize, + "seek index is wrong" + ); out.push((b.pts, duration, block.data)) } Ok(out) @@ -33,23 +37,25 @@ pub fn extract_track( pub fn read_group(segment: &mut EbmlReader) -> anyhow::Result<(u64, Block)> { let (mut dur, mut block) = (None, None); - loop { - let item = segment.next().ok_or(anyhow!("eof"))??; + for _ in 0..10 { + let (_, item) = segment.next().ok_or(anyhow!("eof"))??; match item { MatroskaTag::Void(_) => (), MatroskaTag::Crc32(_) => (), - MatroskaTag::Cluster(_) => (), + MatroskaTag::Cluster(_) => bail!("unexpected cluster"), MatroskaTag::Timestamp(_) => (), MatroskaTag::SimpleBlock(_buf) => { - // bail!("unexpected simpleblock, where a group was expected") + let block = Block::parse(&_buf)?; + return Ok((1000, block)); // HDMV/PGS does not use duration?! } MatroskaTag::BlockGroup(Master::Start) => (), MatroskaTag::BlockGroup(Master::End) => return Ok((dur.unwrap(), block.unwrap())), MatroskaTag::BlockDuration(duration) => dur = Some(duration), MatroskaTag::Block(buf) => block = Some(Block::parse(&buf)?), MatroskaTag::Cues(_) => bail!("reached cues, this is the end"), - MatroskaTag::Segment(Master::End) => info!("extractor reached segment end"), + MatroskaTag::Segment(Master::End) => bail!("extractor reached segment end"), _ => debug!("(rs) tag ignored: {item:?}"), } } + bail!(".") } diff --git a/remuxer/src/import/mod.rs b/remuxer/src/import/mod.rs index 2981cea..d6fa2d0 100644 --- a/remuxer/src/import/mod.rs +++ b/remuxer/src/import/mod.rs @@ -9,7 +9,7 @@ use jellycommon::{Chapter, LocalTrack, SourceTrack, SourceTrackKind}; use jellymatroska::{ matroska::MatroskaTag, read::EbmlReader, - unflatten::{IterWithPos, Unflat, Unflatten}, + unflatten::{Unflat, Unflatten}, }; use log::{debug, error, info, warn}; use std::{path::PathBuf, time::Instant}; @@ -30,7 +30,7 @@ pub struct MatroskaMetadata { pub fn import_metadata(input: &mut EbmlReader) -> Result<MatroskaMetadata> { while let Some(item) = input.next() { let item = match item { - Ok(item) => item, + Ok((_, item)) => item, Err(e) => { if !matches!(e, jellymatroska::error::Error::Io(_)) { warn!("{e}"); diff --git a/remuxer/src/remux.rs b/remuxer/src/remux.rs index 8273c9e..851b43d 100644 --- a/remuxer/src/remux.rs +++ b/remuxer/src/remux.rs @@ -31,7 +31,7 @@ use std::{ struct ClusterLayout { position: usize, timestamp: u64, - source_offsets: Vec<Option<usize>>, + source_offsets: Vec<Option<u64>>, blocks: Vec<(usize, BlockIndex)>, } @@ -277,7 +277,7 @@ pub fn remux_stream_into( // this should be fine since tracks are only read according to segment_layout find_first_cluster_with_off(&segment_layout, skip, i) .ok_or(anyhow!("cluster hole at eof"))?, - MatroskaTag::Cluster(Master::Start), + MatroskaTag::Cluster(Master::Start), // TODO shouldn't this be a child of cluster? ) .context("seeking in input")?; let mut stream = @@ -333,7 +333,7 @@ fn find_first_cluster_with_off( segment_layout: &[ClusterLayout], skip: usize, track: usize, -) -> Option<usize> { +) -> Option<u64> { for skip in skip..segment_layout.len() { if let Some(off) = segment_layout[skip].source_offsets[track] { return Some(off); diff --git a/remuxer/src/seek_index.rs b/remuxer/src/seek_index.rs index eba7344..7e9cee1 100644 --- a/remuxer/src/seek_index.rs +++ b/remuxer/src/seek_index.rs @@ -9,7 +9,7 @@ use jellycommon::seek_index::{BlockIndex, SeekIndex}; use jellymatroska::{ block::Block, read::EbmlReader, - unflatten::{IterWithPos, Unflat, Unflatten}, + unflatten::{Unflat, Unflatten}, MatroskaTag, }; use log::{debug, info, trace, warn}; @@ -30,7 +30,7 @@ pub fn import_seek_index(input: &mut EbmlReader) -> Result<BTreeMap<u64, SeekInd let mut seek_index = BTreeMap::new(); while let Some(item) = input.next() { let item = match item { - Ok(item) => item, + Ok((_, item)) => item, Err(e) => { if !matches!(e, jellymatroska::error::Error::Io(_)) { warn!("{e}"); @@ -67,20 +67,20 @@ fn import_seek_index_segment( MatroskaTag::Cluster(_) => { let mut children = children.unwrap(); let mut pts = 0; - let mut position = children.position(); - loop { - if let Some(Ok(Unflat { children, item, .. })) = children.n() { + if let Some(Ok(Unflat { + children, + item, + position, + })) = children.n() + { match item { MatroskaTag::Timestamp(ts) => pts = ts, MatroskaTag::BlockGroup(_) => { trace!("group"); let mut children = children.unwrap(); - // let position = children.position(); //? TODO where should this point to? cluster or block? // probably block while let Some(Ok(Unflat { - children: _, - item, - position: _block_position, + children: _, item, .. })) = children.n() { match item { @@ -90,7 +90,12 @@ fn import_seek_index_segment( "block: track={} tso={}", block.track, block.timestamp_off ); - seek_index_add(seek_index, &block, position, pts); + seek_index_add( + seek_index, + &block, + position.unwrap(), + pts, + ); } _ => trace!("{item:?}"), } @@ -104,14 +109,13 @@ fn import_seek_index_segment( block.timestamp_off ); trace!("{pts} {}", block.timestamp_off); - seek_index_add(seek_index, &block, position, pts); + seek_index_add(seek_index, &block, position.unwrap(), pts); } _ => trace!("(rsc) tag ignored: {item:?}"), } } else { break; } - position = children.position(); } } _ => debug!("(rs) tag ignored: {item:?}"), @@ -123,9 +127,23 @@ fn import_seek_index_segment( fn seek_index_add( seek_index: &mut BTreeMap<u64, SeekIndex>, block: &Block, - position: usize, + position: u64, pts_base: u64, ) { + //* I heard this helped debugging once. + // { + // let mut f = File::open("/home/muffin/videos/itte-yorushika.mkv").unwrap(); + // f.seek(std::io::SeekFrom::Start(position.try_into().unwrap())) + // .unwrap(); + // let mut buf = [0u8]; + // f.read_exact(&mut buf).unwrap(); + + // eprintln!("{}", buf[0]); + // if buf[0] != 0xa0 && buf[0] != 0xa3 { + // warn!("invalid position {position}") + // } + // } + let trs = seek_index .entry(block.track) .or_insert(SeekIndex::default()); diff --git a/remuxer/src/segment_extractor.rs b/remuxer/src/segment_extractor.rs index 89d5784..ec645c3 100644 --- a/remuxer/src/segment_extractor.rs +++ b/remuxer/src/segment_extractor.rs @@ -4,7 +4,7 @@ Copyright (C) 2024 metamuffin <metamuffin.org> */ use anyhow::{anyhow, bail, Result}; -use jellymatroska::{block::Block, read::EbmlReader, unflatten::IterWithPos, Master, MatroskaTag}; +use jellymatroska::{block::Block, read::EbmlReader, Master, MatroskaTag}; use log::{debug, info, trace}; pub struct SegmentExtractIter<'a> { @@ -23,7 +23,7 @@ impl<'a> SegmentExtractIter<'a> { let mut group = false; let mut saved_block = None; loop { - let item = self.segment.next().ok_or(anyhow!("eof"))??; + let (_, item) = self.segment.next().ok_or(anyhow!("eof"))??; match item { MatroskaTag::Void(_) => (), MatroskaTag::Crc32(_) => (), diff --git a/stream/src/webvtt.rs b/stream/src/webvtt.rs index 2de9835..e720800 100644 --- a/stream/src/webvtt.rs +++ b/stream/src/webvtt.rs @@ -7,7 +7,9 @@ use anyhow::{anyhow, bail, Context, Result}; use jellybase::CONF; use jellycommon::{jhls::SubtitleCue, stream::StreamSpec, LocalTrack, Node}; use jellyremuxer::extract::extract_track; -use jellytranscoder::subtitles::{parse_ass_blocks, parse_webvtt_blocks, write_webvtt}; +use jellytranscoder::subtitles::{ + parse_ass_blocks, parse_pgs_blocks, parse_webvtt_blocks, write_webvtt, +}; use tokio::io::{AsyncWriteExt, DuplexStream}; pub async fn vtt_stream( @@ -48,6 +50,17 @@ pub async fn vtt_stream( let subtitles = parse_webvtt_blocks(webvtt_blocks).context("parsing subtitles")?; write(subtitles)?; } + "S_HDMV/PGS" => { + let webvtt_blocks = tokio::task::spawn_blocking(move || { + extract_track(CONF.media_path.clone(), local_track) + }) + .await??; + + let subtitles = parse_pgs_blocks(webvtt_blocks).context("parsing subtitles")?; + write(subtitles)?; + } + "S_HDMV/TEXTST" => bail!("no HDMV/PGSs yet"), + "S_ARISUB" => bail!("no arisub yet"), "S_TEXT/UTF8" => bail!("no subrip yet"), "S_VOBSUB" => bail!("no vobsub yet"), "S_TEXT/ASS" => { diff --git a/transcoder/src/subtitles.rs b/transcoder/src/subtitles.rs index 1fea3cf..84db5a9 100644 --- a/transcoder/src/subtitles.rs +++ b/transcoder/src/subtitles.rs @@ -39,6 +39,18 @@ pub fn parse_webvtt_blocks(blocks: Vec<(u64, u64, Vec<u8>)>) -> anyhow::Result<V } Ok(out) } +pub fn parse_pgs_blocks(blocks: Vec<(u64, u64, Vec<u8>)>) -> anyhow::Result<Vec<SubtitleCue>> { + let mut out = Vec::new(); + for (pts, dur, block) in blocks { + let _content = String::from_utf8_lossy(&block).trim().to_string(); + out.push(SubtitleCue { + content: "PGS stub".to_string(), + start: pts as f64 / 1000., + end: (pts + dur) as f64 / 1000., + }); + } + Ok(out) +} pub fn parse_ass_blocks( _codec_private: Vec<u8>, |