use crate::error::{Error, Result};
use crate::drive::DriveSession;
use crate::udf;
use crate::mpls;
use crate::clpi;
#[derive(Debug)]
pub struct Disc {
pub volume_id: String,
pub meta_title: Option<String>,
pub format: DiscFormat,
pub capacity_sectors: u32,
pub capacity_bytes: u64,
pub layers: u8,
pub titles: Vec<Title>,
pub region: DiscRegion,
pub aacs: Option<AacsState>,
pub encrypted: bool,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DiscFormat {
Uhd,
BluRay,
Dvd,
Unknown,
}
#[derive(Debug, Clone, PartialEq)]
pub enum DiscRegion {
Free,
BluRay(Vec<BdRegion>),
Dvd(Vec<u8>),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BdRegion {
A,
B,
C,
}
#[derive(Debug, Clone)]
pub struct Title {
pub playlist: String,
pub playlist_id: u16,
pub duration_secs: f64,
pub size_bytes: u64,
pub clips: Vec<Clip>,
pub streams: Vec<Stream>,
pub extents: Vec<Extent>,
}
#[derive(Debug, Clone)]
pub struct Clip {
pub clip_id: String,
pub in_time: u32,
pub out_time: u32,
pub duration_secs: f64,
pub source_packets: u32,
}
#[derive(Debug, Clone)]
pub enum Stream {
Video(VideoStream),
Audio(AudioStream),
Subtitle(SubtitleStream),
}
#[derive(Debug, Clone)]
pub struct VideoStream {
pub pid: u16,
pub codec: Codec,
pub resolution: String,
pub frame_rate: String,
pub hdr: HdrFormat,
pub color_space: ColorSpace,
pub secondary: bool,
pub label: String,
}
#[derive(Debug, Clone)]
pub struct AudioStream {
pub pid: u16,
pub codec: Codec,
pub channels: String,
pub language: String,
pub sample_rate: String,
pub secondary: bool,
pub label: String,
}
#[derive(Debug, Clone)]
pub struct SubtitleStream {
pub pid: u16,
pub codec: Codec,
pub language: String,
pub forced: bool,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Codec {
Hevc,
H264,
Vc1,
Mpeg2,
TrueHd,
DtsHdMa,
DtsHdHr,
Dts,
Ac3,
Ac3Plus,
Lpcm,
Pgs,
Unknown(u8),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum HdrFormat {
Sdr,
Hdr10,
DolbyVision,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ColorSpace {
Bt709,
Bt2020,
Unknown,
}
#[derive(Debug, Clone, Copy)]
pub struct Extent {
pub start_lba: u32,
pub sector_count: u32,
}
impl Codec {
pub fn name(&self) -> &'static str {
match self {
Codec::Hevc => "HEVC",
Codec::H264 => "H.264",
Codec::Vc1 => "VC-1",
Codec::Mpeg2 => "MPEG-2",
Codec::TrueHd => "TrueHD",
Codec::DtsHdMa => "DTS-HD MA",
Codec::DtsHdHr => "DTS-HD HR",
Codec::Dts => "DTS",
Codec::Ac3 => "AC-3",
Codec::Ac3Plus => "AC-3+",
Codec::Lpcm => "LPCM",
Codec::Pgs => "PGS",
Codec::Unknown(_) => "Unknown",
}
}
fn from_coding_type(ct: u8) -> Self {
match ct {
0x24 => Codec::Hevc,
0x1B => Codec::H264,
0xEA => Codec::Vc1,
0x02 => Codec::Mpeg2,
0x83 => Codec::TrueHd,
0x86 => Codec::DtsHdMa,
0x85 => Codec::DtsHdHr,
0x82 => Codec::Dts,
0x81 => Codec::Ac3,
0x84 | 0xA1 => Codec::Ac3Plus,
0x80 => Codec::Lpcm,
0xA2 => Codec::DtsHdHr,
0x90 | 0x91 => Codec::Pgs,
ct => Codec::Unknown(ct),
}
}
}
impl HdrFormat {
pub fn name(&self) -> &'static str {
match self {
HdrFormat::Sdr => "SDR",
HdrFormat::Hdr10 => "HDR10",
HdrFormat::DolbyVision => "Dolby Vision",
}
}
}
impl ColorSpace {
pub fn name(&self) -> &'static str {
match self {
ColorSpace::Bt709 => "BT.709",
ColorSpace::Bt2020 => "BT.2020",
ColorSpace::Unknown => "",
}
}
}
impl Title {
pub fn duration_display(&self) -> String {
let hrs = (self.duration_secs / 3600.0) as u32;
let mins = ((self.duration_secs % 3600.0) / 60.0) as u32;
format!("{}h {:02}m", hrs, mins)
}
pub fn size_gb(&self) -> f64 {
self.size_bytes as f64 / (1024.0 * 1024.0 * 1024.0)
}
pub fn total_sectors(&self) -> u64 {
self.extents.iter().map(|e| e.sector_count as u64).sum()
}
}
#[derive(Debug)]
pub struct AacsState {
pub version: u8,
pub bus_encryption: bool,
pub mkb_version: Option<u32>,
pub disc_hash: String,
pub key_source: KeySource,
pub vuk: [u8; 16],
pub unit_keys: Vec<(u32, [u8; 16])>,
pub read_data_key: Option<[u8; 16]>,
pub volume_id: [u8; 16],
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum KeySource {
KeyDb,
KeyDbDerived,
ProcessingKey,
DeviceKey,
}
impl KeySource {
pub fn name(&self) -> &'static str {
match self {
KeySource::KeyDb => "KEYDB",
KeySource::KeyDbDerived => "KEYDB (derived)",
KeySource::ProcessingKey => "MKB + processing key",
KeySource::DeviceKey => "MKB + device key",
}
}
}
const KEYDB_SEARCH_PATHS: &[&str] = &[
".config/aacs/KEYDB.cfg", ];
const KEYDB_SYSTEM_PATH: &str = "/etc/aacs/KEYDB.cfg";
pub struct ScanOptions {
pub keydb_path: Option<std::path::PathBuf>,
}
impl Default for ScanOptions {
fn default() -> Self {
ScanOptions { keydb_path: None }
}
}
impl ScanOptions {
pub fn with_keydb(path: impl Into<std::path::PathBuf>) -> Self {
ScanOptions { keydb_path: Some(path.into()) }
}
fn resolve_keydb(&self) -> Option<std::path::PathBuf> {
if let Some(p) = &self.keydb_path {
if p.exists() { return Some(p.clone()); }
}
if let Some(home) = std::env::var_os("HOME") {
for relative in KEYDB_SEARCH_PATHS {
let p = std::path::PathBuf::from(&home).join(relative);
if p.exists() { return Some(p); }
}
}
let p = std::path::PathBuf::from(KEYDB_SYSTEM_PATH);
if p.exists() { return Some(p); }
None
}
}
impl Disc {
pub fn capacity_gb(&self) -> f64 {
self.capacity_sectors as f64 * 2048.0 / (1024.0 * 1024.0 * 1024.0)
}
pub fn scan(session: &mut DriveSession, opts: &ScanOptions) -> Result<Self> {
use crate::aacs::{self, KeyDb};
let capacity = Self::read_capacity(session)?;
let udf_fs = udf::read_filesystem(session)?;
let encrypted = udf_fs.find_dir("/AACS").is_some()
|| udf_fs.find_dir("/BDMV/AACS").is_some();
let aacs = if encrypted {
if let Some(keydb_path) = opts.resolve_keydb() {
Self::resolve_aacs(&udf_fs, session, &keydb_path).ok()
} else {
None
}
} else {
None
};
let mut titles = Vec::new();
if let Some(playlist_dir) = udf_fs.find_dir("/BDMV/PLAYLIST") {
for entry in &playlist_dir.entries {
if !entry.is_dir && entry.name.to_lowercase().ends_with(".mpls") {
let path = format!("/BDMV/PLAYLIST/{}", entry.name);
if let Ok(mpls_data) = udf_fs.read_file(session, &path) {
if let Some(title) = Self::parse_playlist(session, &udf_fs, &entry.name, &mpls_data) {
titles.push(title);
}
}
}
}
}
titles.sort_by(|a, b| b.duration_secs.partial_cmp(&a.duration_secs).unwrap_or(std::cmp::Ordering::Equal));
let meta_title = Self::read_meta_title(session, &udf_fs);
crate::labels::apply(session, &udf_fs, &mut titles);
let format = Self::detect_format(&titles);
let layers = if capacity > 24_000_000 { 2 } else { 1 };
let region = if format == DiscFormat::Uhd { DiscRegion::Free } else { DiscRegion::Free };
Ok(Disc {
volume_id: udf_fs.volume_id.clone(),
meta_title,
format,
capacity_sectors: capacity,
capacity_bytes: capacity as u64 * 2048,
layers,
titles,
region,
aacs,
encrypted,
})
}
fn resolve_aacs(
udf_fs: &udf::UdfFs,
session: &mut DriveSession,
keydb_path: &std::path::Path,
) -> Result<AacsState> {
use crate::aacs::{self, KeyDb};
let keydb = KeyDb::load(keydb_path).map_err(|e| Error::AacsError {
detail: format!("failed to load KEYDB: {}", e),
})?;
let uk_ro_data = udf_fs.read_file(session, "/AACS/Unit_Key_RO.inf")
.or_else(|_| udf_fs.read_file(session, "/AACS/DUPLICATE/Unit_Key_RO.inf"))
.map_err(|_| Error::AacsError {
detail: "Unit_Key_RO.inf not found on disc".into(),
})?;
let cc_data = udf_fs.read_file(session, "/AACS/Content000.cer")
.or_else(|_| udf_fs.read_file(session, "/AACS/Content001.cer"))
.ok();
let mkb_data = udf_fs.read_file(session, "/AACS/MKB_RW.inf")
.or_else(|_| udf_fs.read_file(session, "/AACS/MKB_RO.inf"))
.ok();
let mkb_ver = mkb_data.as_deref().and_then(aacs::mkb_version);
let vid_zero = [0u8; 16];
let resolved = aacs::resolve_keys(
&uk_ro_data,
cc_data.as_deref(),
&vid_zero,
&keydb,
mkb_data.as_deref(),
).ok_or_else(|| Error::AacsError {
detail: "disc not in KEYDB".into(),
})?;
Ok(AacsState {
version: if resolved.aacs2 { 2 } else { 1 },
bus_encryption: resolved.bus_encryption,
mkb_version: mkb_ver,
disc_hash: aacs::disc_hash_hex(&resolved.disc_hash),
key_source: match resolved.key_source {
1 => KeySource::KeyDb,
2 => KeySource::KeyDbDerived,
3 => KeySource::ProcessingKey,
4 => KeySource::DeviceKey,
_ => KeySource::KeyDb,
},
vuk: resolved.vuk,
unit_keys: resolved.unit_keys,
read_data_key: None,
volume_id: [0u8; 16],
})
}
fn detect_format(titles: &[Title]) -> DiscFormat {
for title in titles.iter().take(3) {
for stream in &title.streams {
if let Stream::Video(v) = stream {
if v.resolution.contains("2160") {
return DiscFormat::Uhd;
}
if v.resolution.contains("1080") || v.resolution.contains("720") {
return DiscFormat::BluRay;
}
if v.resolution.contains("480") || v.resolution.contains("576") {
return DiscFormat::Dvd;
}
}
}
}
DiscFormat::Unknown
}
fn read_meta_title(session: &mut DriveSession, udf_fs: &udf::UdfFs) -> Option<String> {
let meta_dir = udf_fs.find_dir("/BDMV/META")?;
for sub in &meta_dir.entries {
if !sub.is_dir { continue; }
let dl_path = format!("/BDMV/META/{}", sub.name);
if let Some(dl_dir) = udf_fs.find_dir(&dl_path) {
let xml_files: Vec<_> = dl_dir.entries.iter()
.filter(|e| !e.is_dir && e.name.to_lowercase().ends_with(".xml"))
.collect();
let eng = xml_files.iter().find(|e| e.name.to_lowercase().contains("eng"));
let target = eng.or_else(|| xml_files.first());
if let Some(entry) = target {
let path = format!("{}/{}", dl_path, entry.name);
if let Ok(data) = udf_fs.read_file(session, &path) {
let xml = String::from_utf8_lossy(&data);
if let Some(start) = xml.find("<di:name>") {
let s = start + "<di:name>".len();
if let Some(end) = xml[s..].find("</di:name>") {
let title = xml[s..s + end].trim().to_string();
if !title.is_empty() && title != "Blu-ray" {
return Some(title);
}
}
}
}
}
}
}
None
}
fn read_capacity(session: &mut DriveSession) -> Result<u32> {
let cdb = [0x25, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
let mut buf = [0u8; 8];
session.scsi_execute(&cdb, crate::scsi::DataDirection::FromDevice, &mut buf, 5_000)?;
let lba = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
Ok(lba + 1)
}
fn parse_playlist(
session: &mut DriveSession,
udf_fs: &udf::UdfFs,
filename: &str,
data: &[u8],
) -> Option<Title> {
let parsed = mpls::parse(data).ok()?;
let duration_ticks: u64 = parsed.play_items.iter()
.map(|pi| (pi.out_time.saturating_sub(pi.in_time)) as u64)
.sum();
let duration_secs = duration_ticks as f64 / 45000.0;
if duration_secs < 30.0 {
return None;
}
let mut extents = Vec::new();
let mut total_size: u64 = 0;
let mut clips = Vec::with_capacity(parsed.play_items.len());
for play_item in &parsed.play_items {
let clip_dur = play_item.out_time.saturating_sub(play_item.in_time) as f64 / 45000.0;
let mut pkt_count: u32 = 0;
let clpi_path = format!("/BDMV/CLIPINF/{}.clpi", play_item.clip_id);
if let Ok(clpi_data) = udf_fs.read_file(session, &clpi_path) {
if let Ok(clip_info) = clpi::parse(&clpi_data) {
pkt_count = clip_info.source_packet_count;
total_size += pkt_count as u64 * 192;
let m2ts_path = format!("/BDMV/STREAM/{}.m2ts", play_item.clip_id);
let file_lba = udf_fs.file_start_lba(session, &m2ts_path).unwrap_or(0);
let total_bytes = pkt_count as u64 * 192;
let total_sectors = ((total_bytes + 2047) / 2048) as u32;
if total_sectors > 0 && file_lba > 0 {
extents.push(Extent {
start_lba: file_lba,
sector_count: total_sectors,
});
}
}
}
clips.push(Clip {
clip_id: play_item.clip_id.clone(),
in_time: play_item.in_time,
out_time: play_item.out_time,
duration_secs: clip_dur,
source_packets: pkt_count,
});
}
let streams: Vec<Stream> = parsed.streams.iter().filter_map(|s| {
if s.coding_type == 0 { return None; }
let codec = Codec::from_coding_type(s.coding_type);
match s.stream_type {
1 | 6 | 7 => Some(Stream::Video(VideoStream {
pid: s.pid,
codec,
resolution: format_resolution(s.video_format, s.video_rate),
frame_rate: format_framerate(s.video_rate),
hdr: match s.dynamic_range {
1 => HdrFormat::Hdr10,
2 => HdrFormat::DolbyVision,
_ => HdrFormat::Sdr,
},
color_space: match s.color_space {
1 => ColorSpace::Bt709,
2 => ColorSpace::Bt2020,
_ => ColorSpace::Unknown,
},
secondary: s.secondary,
label: match s.stream_type {
7 => "Dolby Vision EL".to_string(),
_ => String::new(),
},
})),
2 | 5 => {
if matches!(codec, Codec::Pgs) {
Some(Stream::Subtitle(SubtitleStream {
pid: s.pid,
codec,
language: s.language.clone(),
forced: false,
}))
} else {
Some(Stream::Audio(AudioStream {
pid: s.pid,
codec,
channels: format_channels(s.audio_format),
language: s.language.clone(),
sample_rate: format_samplerate(s.audio_rate),
secondary: s.stream_type == 5,
label: String::new(),
}))
}
}
3 => Some(Stream::Subtitle(SubtitleStream {
pid: s.pid,
codec,
language: s.language.clone(),
forced: false,
})),
_ => None,
}
}).collect();
let playlist_num = filename.trim_end_matches(".mpls").trim_end_matches(".MPLS");
let playlist_id = playlist_num.parse::<u16>().unwrap_or(0);
Some(Title {
playlist: filename.to_string(),
playlist_id,
duration_secs,
size_bytes: total_size,
clips,
streams,
extents,
})
}
}
pub struct ContentReader<'a> {
session: &'a mut DriveSession,
aacs: Option<&'a AacsState>,
extents: Vec<Extent>,
current_extent: usize,
current_offset: u32,
unit_key_idx: usize,
read_buf: Vec<u8>,
buf_pos: usize,
buf_len: usize,
batch_sectors: u16,
max_batch_sectors: u16,
ok_streak: u32,
error_streak: u32,
speed_tier: usize,
last_speed_maintain: std::time::Instant,
pub errors: u32,
}
impl Disc {
pub fn open_title<'a>(&'a self, session: &'a mut DriveSession, title_idx: usize) -> Result<ContentReader<'a>> {
let title = self.titles.get(title_idx).ok_or_else(|| Error::DiscError {
detail: format!("title index {} out of range (have {})", title_idx, self.titles.len()),
})?;
let max_batch = detect_max_batch_sectors(session.device_path());
Ok(ContentReader {
session,
aacs: self.aacs.as_ref(),
extents: title.extents.clone(),
current_extent: 0,
current_offset: 0,
unit_key_idx: 0,
read_buf: Vec::with_capacity(max_batch as usize * 2048),
buf_pos: 0,
buf_len: 0,
batch_sectors: max_batch,
max_batch_sectors: max_batch,
ok_streak: 0,
error_streak: 0,
speed_tier: 0,
last_speed_maintain: std::time::Instant::now(),
errors: 0,
})
}
}
fn detect_max_batch_sectors(device_path: &str) -> u16 {
let dev_name = device_path.rsplit('/').next().unwrap_or("");
if dev_name.is_empty() {
return DEFAULT_BATCH_SECTORS;
}
let block_name = if dev_name.starts_with("sg") {
let block_dir = format!("/sys/class/scsi_generic/{}/device/block", dev_name);
std::fs::read_dir(&block_dir).ok()
.and_then(|mut entries| entries.next())
.and_then(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().to_string())
} else {
Some(dev_name.to_string())
};
if let Some(bname) = block_name {
let sysfs_path = format!("/sys/block/{}/queue/max_hw_sectors_kb", bname);
if let Ok(content) = std::fs::read_to_string(&sysfs_path) {
if let Ok(kb) = content.trim().parse::<u32>() {
let sectors = (kb / 2) as u16;
let safe = (sectors * 4 / 5).max(MIN_BATCH_SECTORS);
let aligned = (safe / 3) * 3;
if aligned >= MIN_BATCH_SECTORS {
return aligned.min(MAX_BATCH_SECTORS);
}
}
}
}
DEFAULT_BATCH_SECTORS
}
const MAX_BATCH_SECTORS: u16 = 510; const DEFAULT_BATCH_SECTORS: u16 = 48; const MIN_BATCH_SECTORS: u16 = 3; const RAMP_BATCH_AFTER: u32 = 5; const RAMP_SPEED_AFTER: u32 = 50; const SLOW_SPEED_AFTER: u32 = 3;
const SPEED_TIERS: &[u16] = &[
0xFFFF, 36000, 18000, 9000, 4500, ];
impl<'a> ContentReader<'a> {
pub fn total_bytes(&self) -> u64 {
self.extents.iter().map(|e| e.sector_count as u64 * 2048).sum()
}
pub fn read_unit(&mut self) -> Result<Option<Vec<u8>>> {
if self.buf_pos >= self.buf_len {
if !self.fill_buffer()? {
return Ok(None);
}
}
let start = self.buf_pos * crate::aacs::ALIGNED_UNIT_LEN;
let end = start + crate::aacs::ALIGNED_UNIT_LEN;
let mut unit = self.read_buf[start..end].to_vec();
self.decrypt_unit(&mut unit);
self.buf_pos += 1;
Ok(Some(unit))
}
pub fn read_batch(&mut self) -> Result<Option<&[u8]>> {
if !self.fill_buffer()? {
return Ok(None);
}
let unit_len = crate::aacs::ALIGNED_UNIT_LEN;
if let Some(aacs) = &self.aacs {
let uk = aacs.unit_keys.get(self.unit_key_idx)
.map(|(_, k)| *k)
.unwrap_or([0u8; 16]);
let rdk = aacs.read_data_key.as_ref();
for i in 0..self.buf_len {
let start = i * unit_len;
let end = start + unit_len;
let unit = &mut self.read_buf[start..end];
if crate::aacs::is_unit_encrypted(unit) {
crate::aacs::decrypt_unit_full(unit, &uk, rdk);
}
}
}
let total_bytes = self.buf_len * unit_len;
self.buf_pos = self.buf_len; Ok(Some(&self.read_buf[..total_bytes]))
}
fn decrypt_unit(&self, unit: &mut [u8]) {
if let Some(aacs) = &self.aacs {
if crate::aacs::is_unit_encrypted(unit) {
let uk = aacs.unit_keys.get(self.unit_key_idx)
.map(|(_, k)| *k)
.unwrap_or([0u8; 16]);
crate::aacs::decrypt_unit_full(
unit,
&uk,
aacs.read_data_key.as_ref(),
);
}
}
}
fn read_sectors(&mut self, lba: u32, count: u16) -> Result<()> {
self.session.read_content(lba, count, &mut self.read_buf)?;
Ok(())
}
fn set_speed(&mut self, tier: usize) {
let tier = tier.min(SPEED_TIERS.len() - 1);
if tier != self.speed_tier {
self.speed_tier = tier;
let speed_kbs = SPEED_TIERS[tier];
let cdb = crate::scsi::build_set_cd_speed(speed_kbs);
let mut dummy = [0u8; 0];
let _ = self.session.scsi_execute(
&cdb, crate::scsi::DataDirection::None, &mut dummy, 5_000,
);
}
}
fn fill_buffer(&mut self) -> Result<bool> {
loop {
if self.current_extent >= self.extents.len() {
return Ok(false);
}
let ext_start = self.extents[self.current_extent].start_lba;
let ext_sectors = self.extents[self.current_extent].sector_count;
let remaining = ext_sectors - self.current_offset;
let sectors_to_read = remaining.min(self.batch_sectors as u32) as u16;
let sectors_to_read = sectors_to_read - (sectors_to_read % 3);
if sectors_to_read == 0 {
self.current_extent += 1;
self.current_offset = 0;
continue;
}
let lba = ext_start + self.current_offset;
let byte_count = sectors_to_read as usize * 2048;
self.read_buf.resize(byte_count, 0);
match self.read_sectors(lba, sectors_to_read) {
Ok(_) => {
self.buf_len = sectors_to_read as usize / 3;
self.buf_pos = 0;
self.current_offset += sectors_to_read as u32;
self.error_streak = 0;
if self.current_offset >= ext_sectors {
self.current_extent += 1;
self.current_offset = 0;
}
self.ok_streak += 1;
if self.batch_sectors < self.max_batch_sectors {
if self.ok_streak >= RAMP_BATCH_AFTER {
self.batch_sectors = (self.batch_sectors * 2).min(self.max_batch_sectors);
self.ok_streak = 0;
}
} else if self.speed_tier > 0 && self.ok_streak >= RAMP_SPEED_AFTER {
self.set_speed(self.speed_tier - 1);
self.ok_streak = 0;
}
return Ok(true);
}
Err(_) => {
self.errors += 1;
self.error_streak += 1;
self.ok_streak = 0;
if self.error_streak >= SLOW_SPEED_AFTER
&& self.speed_tier < SPEED_TIERS.len() - 1
{
self.set_speed(self.speed_tier + 1);
self.error_streak = 0; }
if self.batch_sectors > MIN_BATCH_SECTORS {
self.batch_sectors = (self.batch_sectors / 2).max(MIN_BATCH_SECTORS);
std::thread::sleep(std::time::Duration::from_millis(100));
} else {
std::thread::sleep(std::time::Duration::from_millis(500));
self.read_buf.resize(MIN_BATCH_SECTORS as usize * 2048, 0);
if self.read_sectors(lba, MIN_BATCH_SECTORS).is_ok() {
self.buf_len = 1;
self.buf_pos = 0;
self.error_streak = 0;
self.current_offset += MIN_BATCH_SECTORS as u32;
if self.current_offset >= ext_sectors {
self.current_extent += 1;
self.current_offset = 0;
}
return Ok(true);
}
self.current_offset += 3;
if self.current_offset >= ext_sectors {
self.current_extent += 1;
self.current_offset = 0;
}
self.read_buf.resize(crate::aacs::ALIGNED_UNIT_LEN, 0);
self.read_buf.fill(0);
self.buf_len = 1;
self.buf_pos = 0;
return Ok(true);
}
}
}
}
}
}
fn format_resolution(video_format: u8, _video_rate: u8) -> String {
match video_format {
1 => "480i".into(),
2 => "576i".into(),
3 => "480p".into(),
4 => "1080i".into(),
5 => "720p".into(),
6 => "1080p".into(),
7 => "576p".into(),
8 => "2160p".into(),
_ => String::new(),
}
}
fn format_framerate(video_rate: u8) -> String {
match video_rate {
1 => "23.976".into(),
2 => "24".into(),
3 => "25".into(),
4 => "29.97".into(),
6 => "50".into(),
7 => "59.94".into(),
_ => String::new(),
}
}
fn format_channels(audio_format: u8) -> String {
match audio_format {
1 => "mono".into(),
3 => "stereo".into(),
6 => "5.1".into(),
12 => "7.1".into(),
_ if audio_format > 0 => format!("{}ch", audio_format),
_ => String::new(),
}
}
fn format_samplerate(audio_rate: u8) -> String {
match audio_rate {
1 => "48kHz".into(),
4 => "96kHz".into(),
5 => "192kHz".into(),
12 => "48/192kHz".into(),
14 => "48/96kHz".into(),
_ => String::new(),
}
}