1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
|
/*
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) 2025 metamuffin <metamuffin.org>
*/
use anyhow::{Context, Result};
use bincode::{Decode, Encode};
use jellycache::cache_memory;
use jellymatroska::{
block::Block,
read::EbmlReader,
unflatten::{Unflat, Unflatten},
MatroskaTag,
};
use log::{debug, info, trace, warn};
use std::{collections::BTreeMap, fs::File, io::BufReader, path::Path, sync::Arc};
#[derive(Debug, Clone, Default, Decode, Encode)]
pub struct SeekIndex {
pub blocks: Vec<BlockIndex>,
pub keyframes: Vec<usize>,
}
#[derive(Debug, Clone, Decode, Encode)]
pub struct BlockIndex {
pub pts: u64,
// pub duration: Option<u64>,
pub source_off: u64, // points to start of SimpleBlock or BlockGroup (not the Block inside it)
pub size: usize,
}
pub fn get_seek_index(path: &Path) -> anyhow::Result<Arc<BTreeMap<u64, Arc<SeekIndex>>>> {
cache_memory("seekindex-v1", path, move || {
info!("generating seek index for {path:?}");
let input = File::open(path).context("opening source file")?;
let mut input = EbmlReader::new(BufReader::new(input));
let index = import_seek_index(&mut input)?;
info!("done");
Ok(index.into_iter().map(|(k, v)| (k, Arc::new(v))).collect())
})
}
pub fn get_track_sizes(path: &Path) -> Result<BTreeMap<u64, usize>> {
Ok(get_seek_index(path)?
.iter()
.map(|(k, v)| (*k, v.blocks.iter().map(|b| b.size).sum::<usize>()))
.collect())
}
pub fn import_seek_index(input: &mut EbmlReader) -> Result<BTreeMap<u64, SeekIndex>> {
let mut seek_index = BTreeMap::new();
while let Some(item) = input.next() {
let item = match item {
Ok((_, item)) => item,
Err(e) => {
if !matches!(e, jellymatroska::error::Error::Io(_)) {
warn!("{e}");
}
break;
}
};
match item {
MatroskaTag::Segment(_) => {
info!("segment start");
let mut children = Unflatten::new_with_end(input, item);
import_seek_index_segment(&mut children, &mut seek_index)?;
info!("segment end");
}
_ => debug!("(r) tag ignored: {item:?}"),
}
}
Ok(seek_index)
}
fn import_seek_index_segment(
segment: &mut Unflatten,
seek_index: &mut BTreeMap<u64, SeekIndex>,
) -> Result<()> {
while let Some(Ok(Unflat { children, item, .. })) = segment.n() {
match item {
MatroskaTag::SeekHead(_) => {}
MatroskaTag::Info(_) => {}
MatroskaTag::Tags(_) => {}
MatroskaTag::Cues(_) => {}
MatroskaTag::Chapters(_) => {}
MatroskaTag::Tracks(_) => {}
MatroskaTag::Void(_) => {}
MatroskaTag::Cluster(_) => {
let mut children = children.unwrap();
let mut pts = 0;
while 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();
while let Some(Ok(Unflat {
children: _, item, ..
})) = children.n()
{
match item {
MatroskaTag::Block(ref block) => {
debug!(
"block: track={} tso={}",
block.track, block.timestamp_off
);
seek_index_add(seek_index, block, position.unwrap(), pts);
}
_ => trace!("{item:?}"),
}
}
}
MatroskaTag::SimpleBlock(block) => {
trace!(
"simple block: track={} tso={}",
block.track,
block.timestamp_off
);
trace!("{pts} {}", block.timestamp_off);
seek_index_add(seek_index, &block, position.unwrap(), pts);
}
_ => trace!("(rsc) tag ignored: {item:?}"),
}
}
}
_ => debug!("(rs) tag ignored: {item:?}"),
};
}
Ok(())
}
fn seek_index_add(
seek_index: &mut BTreeMap<u64, SeekIndex>,
block: &Block,
position: u64,
pts_base: u64,
) {
let trs = seek_index.entry(block.track).or_default();
if block.flags.keyframe() {
trs.keyframes.push(trs.blocks.len());
}
trs.blocks.push(BlockIndex {
pts: (pts_base as i64 + block.timestamp_off as i64) as u64,
source_off: position,
size: block.data.len(),
});
}
|