Skip to content

Commit 8ee1a68

Browse files
committed
Add HEVC processing helper
1 parent 8c706d8 commit 8ee1a68

File tree

8 files changed

+303
-15
lines changed

8 files changed

+303
-15
lines changed

Cargo.toml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "hevc_parser"
3-
version = "0.3.4"
3+
version = "0.4.0"
44
authors = ["quietvoid"]
55
edition = "2021"
66
rust-version = "1.56.0"
@@ -11,4 +11,10 @@ repository = "https://github.com/quietvoid/hevc_parser"
1111
[dependencies]
1212
nom = "7.1.1"
1313
bitvec_helpers = "1.0.2"
14-
anyhow = "1.0.56"
14+
anyhow = "1.0.57"
15+
16+
regex = { version = "1.5.5", optional = true }
17+
18+
[features]
19+
default = ["hevc_io"]
20+
hevc_io = ["regex"]

src/hevc/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use anyhow::{bail, Result};
22

33
use self::slice::SliceNAL;
44

5-
use super::BitVecReader;
5+
use super::{BitVecReader, NALUStartCode};
66

77
pub(crate) mod hrd_parameters;
88
pub(crate) mod pps;
@@ -54,6 +54,10 @@ pub struct NALUnit {
5454
pub nal_type: u8,
5555
pub nuh_layer_id: u8,
5656
pub temporal_id: u8,
57+
58+
pub start_code: NALUStartCode,
59+
60+
#[deprecated(since = "0.4.0", note = "Please use `start_code` instead")]
5761
pub start_code_len: u8,
5862

5963
pub decoded_frame_index: u64,

src/hevc/pps.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use super::{scaling_list_data::ScalingListData, BitVecReader};
22
use anyhow::Result;
33

4+
#[allow(clippy::upper_case_acronyms)]
45
#[derive(Default, Debug, PartialEq)]
56
pub struct PPSNAL {
67
pub(crate) pps_id: u64,

src/hevc/sps.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use super::short_term_rps::ShortTermRPS;
66
use super::vui_parameters::VuiParameters;
77
use super::BitVecReader;
88

9+
#[allow(clippy::upper_case_acronyms)]
910
#[derive(Default, Debug, PartialEq, Clone)]
1011
pub struct SPSNAL {
1112
pub(crate) vps_id: u8,

src/hevc/vps.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use super::hrd_parameters::HrdParameters;
44
use super::profile_tier_level::ProfileTierLevel;
55
use super::BitVecReader;
66

7+
#[allow(clippy::upper_case_acronyms)]
78
#[derive(Default, Debug, PartialEq)]
89
pub struct VPSNAL {
910
pub(crate) vps_id: u8,

src/io/mod.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
use std::path::{Path, PathBuf};
2+
3+
use anyhow::{bail, format_err, Result};
4+
use regex::Regex;
5+
6+
pub mod processor;
7+
8+
use super::{HevcParser, NALUStartCode, NALUnit};
9+
10+
#[derive(Debug, PartialEq, Clone)]
11+
pub enum IoFormat {
12+
Raw,
13+
RawStdin,
14+
Matroska,
15+
}
16+
17+
pub trait IoProcessor {
18+
/// Input path
19+
fn input(&self) -> &PathBuf;
20+
/// If the processor has a progress bar, this updates every megabyte read
21+
fn update_progress(&mut self, delta: u64);
22+
23+
/// NALU processing callback
24+
/// This is called after reading a 100kB chunk of the file
25+
/// The resulting NALs are always complete and unique
26+
///
27+
/// The data can be access through `chunk`, using the NAL start/end indices
28+
fn process_nals(&mut self, parser: &HevcParser, nals: &[NALUnit], chunk: &[u8]) -> Result<()>;
29+
30+
/// Finalize callback, when the stream is done being read
31+
/// Called at the end of `HevcProcessor::process_io`
32+
fn finalize(&mut self, parser: &HevcParser) -> Result<()>;
33+
}
34+
35+
/// Data for a frame, with its decoded index
36+
pub struct FrameBuffer {
37+
pub frame_number: u64,
38+
pub nals: Vec<NalBuffer>,
39+
}
40+
41+
/// Data for a NALU, with type
42+
/// The data does not include the start code
43+
pub struct NalBuffer {
44+
pub nal_type: u8,
45+
pub start_code: NALUStartCode,
46+
pub data: Vec<u8>,
47+
}
48+
49+
pub fn format_from_path(input: &Path) -> Result<IoFormat> {
50+
let regex = Regex::new(r"\.(hevc|.?265|mkv)")?;
51+
let file_name = match input.file_name() {
52+
Some(file_name) => file_name
53+
.to_str()
54+
.ok_or_else(|| format_err!("Invalid file name"))?,
55+
None => "",
56+
};
57+
58+
if file_name == "-" {
59+
Ok(IoFormat::RawStdin)
60+
} else if regex.is_match(file_name) && input.is_file() {
61+
if file_name.ends_with(".mkv") {
62+
Ok(IoFormat::Matroska)
63+
} else {
64+
Ok(IoFormat::Raw)
65+
}
66+
} else if file_name.is_empty() {
67+
bail!("Missing input.")
68+
} else if !input.is_file() {
69+
bail!("Input file doesn't exist.")
70+
} else {
71+
bail!("Invalid input file type.")
72+
}
73+
}
74+
75+
impl std::fmt::Display for IoFormat {
76+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77+
match *self {
78+
IoFormat::Matroska => write!(f, "Matroska file"),
79+
IoFormat::Raw => write!(f, "HEVC file"),
80+
IoFormat::RawStdin => write!(f, "HEVC pipe"),
81+
}
82+
}
83+
}

src/io/processor.rs

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
use anyhow::{bail, Result};
2+
use std::io::Read;
3+
4+
use super::{HevcParser, IoFormat, IoProcessor};
5+
6+
/// Base HEVC stream processor
7+
pub struct HevcProcessor {
8+
opts: HevcProcessorOpts,
9+
10+
format: IoFormat,
11+
parser: HevcParser,
12+
13+
chunk_size: usize,
14+
15+
main_buf: Vec<u8>,
16+
sec_buf: Vec<u8>,
17+
consumed: usize,
18+
19+
chunk: Vec<u8>,
20+
end: Vec<u8>,
21+
offsets: Vec<usize>,
22+
23+
last_buffered_frame: u64,
24+
}
25+
26+
/// Options for the processor
27+
#[derive(Default)]
28+
pub struct HevcProcessorOpts {
29+
/// Buffer a frame when using `parse_nalus`.
30+
/// This stops the stream reading as soon as a full frame has been parsed.
31+
pub buffer_frame: bool,
32+
}
33+
34+
impl HevcProcessor {
35+
/// Initialize a HEVC stream processor
36+
pub fn new(format: IoFormat, opts: HevcProcessorOpts, chunk_size: usize) -> Self {
37+
let sec_buf = if format == IoFormat::RawStdin {
38+
vec![0; 50_000]
39+
} else {
40+
Vec::new()
41+
};
42+
43+
Self {
44+
opts,
45+
format,
46+
parser: HevcParser::default(),
47+
48+
chunk_size,
49+
main_buf: vec![0; chunk_size],
50+
sec_buf,
51+
consumed: 0,
52+
53+
chunk: Vec::with_capacity(chunk_size),
54+
end: Vec::with_capacity(chunk_size),
55+
offsets: Vec::with_capacity(2048),
56+
57+
last_buffered_frame: 0,
58+
}
59+
}
60+
61+
/// Fully parse the input stream
62+
pub fn process_io(
63+
&mut self,
64+
reader: &mut dyn Read,
65+
processor: &mut dyn IoProcessor,
66+
) -> Result<()> {
67+
self.parse_nalus(reader, processor)?;
68+
69+
self.parser.finish();
70+
71+
processor.finalize(&self.parser)?;
72+
73+
Ok(())
74+
}
75+
76+
/// Parse NALUs from the stream
77+
/// Depending on the options, this either:
78+
/// - Loops the entire stream until EOF
79+
/// - Loops until a complete frame has been parsed
80+
/// In both cases, the processor callback is called when a NALU payload is ready.
81+
pub fn parse_nalus(
82+
&mut self,
83+
reader: &mut dyn Read,
84+
processor: &mut dyn IoProcessor,
85+
) -> Result<()> {
86+
while let Ok(n) = reader.read(&mut self.main_buf) {
87+
let mut read_bytes = n;
88+
if read_bytes == 0 && self.end.is_empty() && self.chunk.is_empty() {
89+
break;
90+
}
91+
92+
if self.format == IoFormat::RawStdin {
93+
self.chunk.extend_from_slice(&self.main_buf[..read_bytes]);
94+
95+
loop {
96+
match reader.read(&mut self.sec_buf) {
97+
Ok(num) => {
98+
if num > 0 {
99+
read_bytes += num;
100+
101+
self.chunk.extend_from_slice(&self.sec_buf[..num]);
102+
103+
if read_bytes >= self.chunk_size {
104+
break;
105+
}
106+
} else {
107+
break;
108+
}
109+
}
110+
Err(e) => bail!("{:?}", e),
111+
}
112+
}
113+
} else if read_bytes < self.chunk_size {
114+
self.chunk.extend_from_slice(&self.main_buf[..read_bytes]);
115+
} else {
116+
self.chunk.extend_from_slice(&self.main_buf);
117+
}
118+
119+
self.parser.get_offsets(&self.chunk, &mut self.offsets);
120+
121+
if self.offsets.is_empty() {
122+
continue;
123+
}
124+
125+
let last = if read_bytes < self.chunk_size {
126+
*self.offsets.last().unwrap()
127+
} else {
128+
let last = self.offsets.pop().unwrap();
129+
130+
self.end.clear();
131+
self.end.extend_from_slice(&self.chunk[last..]);
132+
133+
last
134+
};
135+
136+
let nals = self
137+
.parser
138+
.split_nals(&self.chunk, &self.offsets, last, true)?;
139+
140+
// Process NALUs
141+
processor.process_nals(&self.parser, &nals, &self.chunk)?;
142+
143+
self.chunk.clear();
144+
145+
if !self.end.is_empty() {
146+
self.chunk.extend_from_slice(&self.end);
147+
self.end.clear()
148+
}
149+
150+
self.consumed += read_bytes;
151+
152+
if self.consumed >= 100_000_000 {
153+
processor.update_progress(1);
154+
self.consumed = 0;
155+
}
156+
157+
if self.opts.buffer_frame {
158+
let next_frame = nals.iter().map(|nal| nal.decoded_frame_index).max();
159+
160+
if let Some(number) = next_frame {
161+
if number > self.last_buffered_frame {
162+
self.last_buffered_frame = number;
163+
164+
// Stop reading
165+
break;
166+
}
167+
}
168+
}
169+
}
170+
171+
Ok(())
172+
}
173+
}

0 commit comments

Comments
 (0)