diff options
-rw-r--r-- | common/src/lib.rs | 5 | ||||
-rw-r--r-- | remuxer/src/import/mod.rs | 25 | ||||
-rw-r--r-- | remuxer/src/lib.rs | 235 | ||||
-rw-r--r-- | server/src/library.rs | 44 | ||||
-rw-r--r-- | server/src/routes/ui/style/directorypage.css | 1 | ||||
-rw-r--r-- | server/src/routes/ui/style/player.css | 2 | ||||
-rw-r--r-- | tools/src/bin/import.rs | 24 |
7 files changed, 210 insertions, 126 deletions
diff --git a/common/src/lib.rs b/common/src/lib.rs index a4380c6..90cbd3e 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -12,13 +12,14 @@ pub struct DirectoryInfo { pub struct ItemInfo { pub title: String, pub duration: f64, // in seconds - pub path: PathBuf, pub banner: Option<PathBuf>, - pub tracks: BTreeMap<u64, SourceTrack>, + pub tracks: BTreeMap<usize, SourceTrack>, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct SourceTrack { + pub path: PathBuf, + pub track_number: u64, pub kind: SourceTrackKind, pub name: String, pub codec: String, diff --git a/remuxer/src/import/mod.rs b/remuxer/src/import/mod.rs index 5147a05..505fbe4 100644 --- a/remuxer/src/import/mod.rs +++ b/remuxer/src/import/mod.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use anyhow::{anyhow, bail, Result}; use jellycommon::{ItemInfo, SourceTrack, SourceTrackKind}; use jellymatroska::{ @@ -8,7 +10,7 @@ use jellymatroska::{ }; use log::{debug, error, info, trace, warn}; -pub fn import_read(input: &mut EbmlReader, iteminfo: &mut ItemInfo) -> Result<()> { +pub fn import_read(path: &PathBuf, input: &mut EbmlReader, iteminfo: &mut ItemInfo) -> Result<()> { // TODO dont traverse the entire file, if the tracks are listed at the end while let Some(item) = input.next() { let item = match item { @@ -35,7 +37,7 @@ pub fn import_read(input: &mut EbmlReader, iteminfo: &mut ItemInfo) -> Result<() MatroskaTag::Segment(_) => { info!("segment start"); let mut children = Unflatten::new_with_end(input, item); - import_read_segment(&mut children, iteminfo)?; + import_read_segment(path, &mut children, iteminfo)?; info!("segment end"); } _ => debug!("(r) tag ignored: {item:?}"), @@ -45,7 +47,11 @@ pub fn import_read(input: &mut EbmlReader, iteminfo: &mut ItemInfo) -> Result<() Ok(()) } -fn import_read_segment(children: &mut Unflatten, iteminfo: &mut ItemInfo) -> Result<()> { +fn import_read_segment( + path: &PathBuf, + children: &mut Unflatten, + iteminfo: &mut ItemInfo, +) -> Result<()> { let (mut timestamp_scale, mut duration) = (None, None); while let Some(Ok(Unflat { children, item })) = children.next() { match item { @@ -99,7 +105,11 @@ fn import_read_segment(children: &mut Unflatten, iteminfo: &mut ItemInfo) -> Res match item { MatroskaTag::CuePoint(_) => { let mut children = children.unwrap(); - while let Some(Ok(Unflat { children: _, item: _ })) = children.next() { + while let Some(Ok(Unflat { + children: _, + item: _, + })) = children.next() + { // error!("{item:?}") } } @@ -170,7 +180,7 @@ fn import_read_segment(children: &mut Unflatten, iteminfo: &mut ItemInfo) -> Res _ => (), } } - // let itrack_index = iteminfo.tracks.len(); + let itrack_index = iteminfo.tracks.len(); let mtrack_index = index.unwrap(); let kind = match kind.ok_or(anyhow!("track type required"))? { 1 => SourceTrackKind::Video { @@ -186,10 +196,11 @@ fn import_read_segment(children: &mut Unflatten, iteminfo: &mut ItemInfo) -> Res 17 => SourceTrackKind::Subtitles, _ => bail!("invalid track type"), }; - // track_mapping.insert(mtrack_index, itrack_index); iteminfo.tracks.insert( - mtrack_index, + itrack_index, SourceTrack { + track_number: mtrack_index, + path: path.to_owned(), default_duration, codec_private, name: name.unwrap_or_else(|| "unnamed".to_string()), diff --git a/remuxer/src/lib.rs b/remuxer/src/lib.rs index 5536a12..fabde10 100644 --- a/remuxer/src/lib.rs +++ b/remuxer/src/lib.rs @@ -1,7 +1,7 @@ pub mod format; pub mod import; -use anyhow::Result; +use anyhow::{anyhow, Result}; use jellycommon::{ItemInfo, SourceTrack, SourceTrackKind}; use jellymatroska::{ block::Block, @@ -11,7 +11,7 @@ use jellymatroska::{ Master, MatroskaTag, }; use log::{debug, info, trace, warn}; -use std::{collections::BTreeMap, fs::File, io::Write, path::PathBuf, sync::Arc}; +use std::{collections::VecDeque, fs::File, io::Write, path::PathBuf, sync::Arc}; pub struct RemuxerContext {} @@ -26,24 +26,44 @@ impl RemuxerContext { _offset: usize, path_base: PathBuf, iteminfo: ItemInfo, - selection: Vec<u64>, + selection: Vec<usize>, webm: bool, ) -> anyhow::Result<()> { - let source_path = path_base.join(&iteminfo.path); - info!("remuxing {source_path:?} to have tracks {selection:?}"); - - let input = File::open(source_path)?; - let mut input = EbmlReader::new(input); + info!("remuxing {:?} to have tracks {selection:?}", iteminfo.title); let mut output = EbmlWriter::new(writer, 0); - // maps original to remuxed - let mapping = BTreeMap::from_iter( - selection - .iter() - .enumerate() - .map(|(i, e)| (*e, i as u64 + 1)), - ); - info!("track mapping: {mapping:?}"); + struct ReaderC { + info: SourceTrack, + reader: EbmlReader, + mapped: u64, + } + + let mut inputs = selection + .iter() + .enumerate() + .map(|(index, sel)| { + let info = iteminfo + .tracks + .get(sel) + .ok_or(anyhow!("track not available"))? + .to_owned(); + let source_path = path_base.join(&info.path); + let mapped = index as u64 + 1; + info!( + "\t- {sel} {source_path:?} ({} => {mapped})", + info.track_number + ); + info!("\t {}", info); + let file = File::open(source_path)?; + let reader = EbmlReader::new(file); + Ok(ReaderC { + reader, + info, + mapped, + }) + }) + .into_iter() + .collect::<anyhow::Result<Vec<_>>>()?; output.write_tag(&MatroskaTag::Ebml(Master::Collected(vec![ MatroskaTag::EbmlVersion(1), @@ -71,101 +91,152 @@ impl RemuxerContext { output.write_tag(&MatroskaTag::Tags(Master::Collected(vec![])))?; // output.write_tag(&MatroskaTag::Cues(Master::Collected(vec![])))?; - let tracks_header = MatroskaTag::Tracks(Master::Collected( - mapping - .iter() - .map(|(norig, nrem)| track_to_ebml(*nrem, &iteminfo.tracks.get(norig).unwrap())) - .collect(), - )); - info!("track header: {tracks_header:?}"); - output.write_tag(&tracks_header)?; + let tracks_header = inputs + .iter() + .map(|rc| track_to_ebml(rc.mapped, &rc.info)) + .collect(); + output.write_tag(&MatroskaTag::Tracks(Master::Collected(tracks_header)))?; + + struct ReaderD<'a> { + info: SourceTrack, + peek: Option<AbsoluteBlock>, + stream: SegmentExtractIter<'a>, + mapped: u64, + } - while let Some(item) = input.next() { - let item = match item { - Ok(item) => item, - Err(e) => { - warn!("{e}"); + // read until start of the segment + let mut ks = vec![]; + for i in &mut inputs { + loop { + let t = i.reader.next().ok_or(anyhow!("early eof"))??; + if let MatroskaTag::Segment(Master::Start) = t { break; } - }; - match item { - MatroskaTag::Ebml(_) => { - Unflatten::new_with_end(&mut input, item); - } - MatroskaTag::Segment(_) => { - info!("segment start"); - let mut children = Unflatten::new_with_end(&mut input, item); - filter_segment(&mut children, &mut output, &iteminfo, &mapping)?; - info!("segment end"); - } - _ => debug!("(r) tag ignored: {item:?}"), } + let mut stream = SegmentExtractIter { + segment: Unflatten::new_with_end( + &mut i.reader, + MatroskaTag::Segment(Master::Start), + ), + extract: i.info.track_number, + emission_queue: VecDeque::new(), + }; + ks.push(ReaderD { + mapped: i.mapped, + peek: Some(stream.next()?), + stream, + info: i.info.clone(), + }); } - output.write_tag(&MatroskaTag::Segment(Master::End))?; + loop { + let next_index = ks + .iter() + .enumerate() + .fold((0, u64::MAX), |(bi, bpts), (i, r)| { + if let Some(peek) = &r.peek { + let pts = peek.pts(); + if pts < bpts { + return (i, pts); + } + } + (bi, bpts) + }) + .0; + + let kn = &mut ks[next_index]; + let mut next = kn.peek.replace(kn.stream.next()?).unwrap(); //.ok_or(anyhow!("eof?")); - Ok(()) + let pts = next.pts(); + next.block.track = kn.mapped; + next.block.timestamp_off = 0; + let buf = next.block.dump(); + output.write_tag(&MatroskaTag::Cluster(Master::Collected(vec![ + MatroskaTag::Timestamp(pts), + MatroskaTag::SimpleBlock(buf), + ])))?; + } + + // output.write_tag(&MatroskaTag::Segment(Master::End))?; + // Ok(()) } } -fn filter_segment( - children: &mut Unflatten, - writer: &mut EbmlWriter, - _iteminfo: &ItemInfo, - mapping: &BTreeMap<u64, u64>, -) -> Result<()> { - while let Some(Ok(Unflat { children, item })) = children.next() { +struct AbsoluteBlock { + pts_base: u64, + block: Block, +} + +struct SegmentExtractIter<'a> { + segment: Unflatten<'a>, + extract: u64, + emission_queue: VecDeque<AbsoluteBlock>, +} + +impl AbsoluteBlock { + pub fn pts(&self) -> u64 { + self.block.timestamp_off as u64 + self.pts_base + } +} + +impl SegmentExtractIter<'_> { + pub fn next(&mut self) -> Result<AbsoluteBlock> { + loop { + if let Some(b) = self.emission_queue.pop_front() { + break Ok(b); + } + self.read()?; + } + } + + pub fn read(&mut self) -> Result<()> { + let Unflat { children, item } = self.segment.next().ok_or(anyhow!("eof"))??; + let mut pts_base = 0; match item { MatroskaTag::SeekHead(_) => {} MatroskaTag::Info(_) => {} MatroskaTag::Cluster(_) => { - let mut cluster = vec![]; let mut children = children.unwrap(); while let Some(Ok(Unflat { children, item })) = children.next() { match item { MatroskaTag::Crc32(_) => (), MatroskaTag::Timestamp(ts) => { info!("ts={ts}"); - cluster.push(MatroskaTag::Timestamp(ts)); + pts_base = ts; } MatroskaTag::BlockGroup(_) => { debug!("group"); let mut children = children.unwrap(); - let mut group = vec![]; + + let mut duration = None; + let mut block = None; + while let Some(Ok(Unflat { children: _, item })) = children.next() { match item { - MatroskaTag::Block(buf) => { - let mut block = Block::parse(&buf)?; - if let Some(outnum) = mapping.get(&block.track) { - block.track = *outnum; - trace!( - "block: track={} tso={}", - block.track, - block.timestamp_off - ); - group.push(MatroskaTag::Block(block.dump())); - } - } - MatroskaTag::BlockDuration(v) => { - group.push(MatroskaTag::BlockDuration(v)); - } + MatroskaTag::Block(buf) => block = Some(buf), + MatroskaTag::BlockDuration(v) => duration = Some(v), _ => debug!("ignored {item:?}"), } } - cluster.push(MatroskaTag::BlockGroup(Master::Collected(group))); + // TODO duration + let block = Block::parse(&block.unwrap())?; + if block.track == self.extract { + trace!("block: track={} tso={}", block.track, block.timestamp_off); + self.emission_queue + .push_back(AbsoluteBlock { pts_base, block }); + } } MatroskaTag::SimpleBlock(buf) => { - let mut block = Block::parse(&buf)?; - if let Some(outnum) = mapping.get(&block.track) { - block.track = *outnum; + let block = Block::parse(&buf)?; + if block.track == self.extract { trace!("block: track={} tso={}", block.track, block.timestamp_off); - cluster.push(MatroskaTag::SimpleBlock(block.dump())); + self.emission_queue + .push_back(AbsoluteBlock { pts_base, block }); } } _ => warn!("(rsc) tag ignored: {item:?}"), } } - writer.write_tag(&MatroskaTag::Cluster(Master::Collected(cluster)))?; } MatroskaTag::Tags(_) => {} MatroskaTag::Cues(_) => {} @@ -173,8 +244,8 @@ fn filter_segment( MatroskaTag::Tracks(_) => {} _ => debug!("(rs) tag ignored: {item:?}"), } + Ok(()) } - Ok(()) } pub fn track_to_ebml(number: u64, track: &SourceTrack) -> MatroskaTag { @@ -194,7 +265,6 @@ pub fn track_to_ebml(number: u64, track: &SourceTrack) -> MatroskaTag { fps: _, } => { els.push(MatroskaTag::TrackType(1)); - els.push(MatroskaTag::DefaultDuration(41708333)); // TODO pls not hardcoded els.push(MatroskaTag::Video(Master::Collected(vec![ MatroskaTag::PixelWidth(width), MatroskaTag::PixelHeight(height), @@ -221,16 +291,3 @@ pub fn track_to_ebml(number: u64, track: &SourceTrack) -> MatroskaTag { } MatroskaTag::TrackEntry(Master::Collected(els)) } - -// pub struct SendWriter(pub Sender<Vec<u8>>); - -// impl Write for SendWriter { -// fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { -// self.0.blocking_send(buf.to_owned()).unwrap(); -// Ok(buf.len()) -// } - -// fn flush(&mut self) -> std::io::Result<()> { -// Ok(()) // TODO should we actually do something here? -// } -// } diff --git a/server/src/library.rs b/server/src/library.rs index a48e746..ebafa9d 100644 --- a/server/src/library.rs +++ b/server/src/library.rs @@ -36,7 +36,9 @@ pub struct Item { impl Library { pub fn open(path: &PathBuf) -> anyhow::Result<Self> { Ok(Self { - root: Node::from_path(path.clone(), PathBuf::new(), true).context("indexing root")?, + root: Node::from_path(path.clone(), PathBuf::new(), true) + .context("indexing root")? + .ok_or(anyhow!("root need directory.json"))?, }) } pub fn nested_path(&self, path: &Path) -> anyhow::Result<Arc<Node>> { @@ -86,9 +88,12 @@ impl Node { path: PathBuf, mut lib_path: PathBuf, root: bool, - ) -> anyhow::Result<Arc<Node>> { + ) -> anyhow::Result<Option<Arc<Node>>> { if path.is_dir() { let mpath = path.join("directory.json"); + if !mpath.exists() { + return Ok(None); + } let data: DirectoryInfo = serde_json::from_reader(File::open(mpath).context("metadata missing")?)?; @@ -109,20 +114,23 @@ impl Node { None } }) - .map(|e| { + .filter_map(|e| { Node::from_path(e.clone(), lib_path.clone(), false) .context(format!("loading {e:?}")) + .transpose() }) .into_iter() .collect::<anyhow::Result<Vec<_>>>()?; - Ok(Node::Directory(Arc::new(Directory { - lib_path, - children, - data, - identifier, - })) - .into()) + Ok(Some( + Node::Directory(Arc::new(Directory { + lib_path, + children, + data, + identifier, + })) + .into(), + )) } else if path.is_file() { info!("loading {path:?}"); let datafile = File::open(path.clone()).context("cant load metadata")?; @@ -134,13 +142,15 @@ impl Node { .to_str() .unwrap() .to_string(); - Ok(Node::Item(Arc::new(Item { - fs_path: path.clone(), - lib_path: lib_path.join(identifier.clone()), - info: data, - identifier, - })) - .into()) + Ok(Some( + Node::Item(Arc::new(Item { + fs_path: path.clone(), + lib_path: lib_path.join(identifier.clone()), + info: data, + identifier, + })) + .into(), + )) } else { bail!("did somebody really put a fifo or socket in the library?!") } diff --git a/server/src/routes/ui/style/directorypage.css b/server/src/routes/ui/style/directorypage.css index 8c6d5d9..2773e3c 100644 --- a/server/src/routes/ui/style/directorypage.css +++ b/server/src/routes/ui/style/directorypage.css @@ -1,4 +1,3 @@ - .page.dir { padding: 1em; padding-left: 3em; diff --git a/server/src/routes/ui/style/player.css b/server/src/routes/ui/style/player.css index b34653e..8d3488f 100644 --- a/server/src/routes/ui/style/player.css +++ b/server/src/routes/ui/style/player.css @@ -42,9 +42,11 @@ input[type="radio"] { width: 1.2em; height: 1.2em; border-radius: 8px; + padding: 2px; background-clip: content-box; border: 2px solid var(--font); background-color: transparent; + transition: background-color 0.3s; } input[type="radio"]:checked { background-color: var(--accent-light); diff --git a/tools/src/bin/import.rs b/tools/src/bin/import.rs index 4af6ac7..d38925b 100644 --- a/tools/src/bin/import.rs +++ b/tools/src/bin/import.rs @@ -20,7 +20,7 @@ struct Args { dry: bool, #[clap(short = 'i', long)] - input: PathBuf, + input: Option<PathBuf>, } fn main() -> anyhow::Result<()> { @@ -37,20 +37,25 @@ fn main() -> anyhow::Result<()> { warn!("using the default instead"); ItemInfo { duration: 0.0, - path: args.input.clone(), - banner: args.banner, - title: args - .title - .unwrap_or(args.item.to_str().unwrap().to_string()), + banner: None, + title: args.item.to_str().unwrap().to_string(), tracks: Default::default(), } } }; - let input = File::open(args.input.clone()).unwrap(); - let mut input = EbmlReader::new(input); + if let Some(title) = args.title { + iteminfo.title = title; + } + if let Some(banner) = args.banner { + iteminfo.banner = Some(banner); + } - import_read(&mut input, &mut iteminfo)?; + if let Some(input_path) = args.input { + let input = File::open(input_path.clone()).unwrap(); + let mut input = EbmlReader::new(input); + import_read(&input_path, &mut input, &mut iteminfo)?; + } let k = serde_json::to_string_pretty(&iteminfo)?; if args.dry { @@ -59,6 +64,5 @@ fn main() -> anyhow::Result<()> { let mut f = File::create(args.item)?; f.write_all(k.as_bytes())?; } - Ok(()) } |