|
|
|
|
@@ -0,0 +1,820 @@
|
|
|
|
|
//! Track listing functionality for --list-tracks option
|
|
|
|
|
//!
|
|
|
|
|
//! This module provides the ability to list all tracks in media files
|
|
|
|
|
//! without processing them for subtitle extraction.
|
|
|
|
|
|
|
|
|
|
use std::fs::File;
|
|
|
|
|
use std::io::{BufReader, Read, Seek, SeekFrom};
|
|
|
|
|
use std::path::Path;
|
|
|
|
|
|
|
|
|
|
/// Represents a track found in a media file
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct TrackInfo {
|
|
|
|
|
pub track_number: u32,
|
|
|
|
|
pub track_type: TrackType,
|
|
|
|
|
pub codec: String,
|
|
|
|
|
pub language: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Type of media track
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub enum TrackType {
|
|
|
|
|
Video,
|
|
|
|
|
Audio,
|
|
|
|
|
Subtitle,
|
|
|
|
|
ClosedCaption,
|
|
|
|
|
Other(String),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl std::fmt::Display for TrackType {
|
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
|
match self {
|
|
|
|
|
TrackType::Video => write!(f, "Video"),
|
|
|
|
|
TrackType::Audio => write!(f, "Audio"),
|
|
|
|
|
TrackType::Subtitle => write!(f, "Subtitle"),
|
|
|
|
|
TrackType::ClosedCaption => write!(f, "Closed Caption"),
|
|
|
|
|
TrackType::Other(s) => write!(f, "{}", s),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Detected file format
|
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
|
|
|
pub enum FileFormat {
|
|
|
|
|
Mkv,
|
|
|
|
|
Mp4,
|
|
|
|
|
TransportStream,
|
|
|
|
|
Unknown,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Detect the format of a file based on magic bytes
|
|
|
|
|
pub fn detect_format(path: &Path) -> std::io::Result<FileFormat> {
|
|
|
|
|
let file = File::open(path)?;
|
|
|
|
|
let mut reader = BufReader::new(file);
|
|
|
|
|
let mut header = [0u8; 12];
|
|
|
|
|
|
|
|
|
|
let bytes_read = reader.read(&mut header)?;
|
|
|
|
|
if bytes_read < 4 {
|
|
|
|
|
return Ok(FileFormat::Unknown);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for Matroska/WebM (EBML header)
|
|
|
|
|
if header[0] == 0x1a && header[1] == 0x45 && header[2] == 0xdf && header[3] == 0xa3 {
|
|
|
|
|
return Ok(FileFormat::Mkv);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for MP4/MOV (ftyp box or other common boxes)
|
|
|
|
|
if bytes_read >= 8 {
|
|
|
|
|
let box_type = &header[4..8];
|
|
|
|
|
if box_type == b"ftyp" || box_type == b"moov" || box_type == b"mdat" || box_type == b"free" {
|
|
|
|
|
return Ok(FileFormat::Mp4);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for MPEG Transport Stream (sync byte 0x47 at regular intervals)
|
|
|
|
|
if header[0] == 0x47 {
|
|
|
|
|
// Check for sync bytes at 188-byte intervals (standard TS packet size)
|
|
|
|
|
reader.seek(SeekFrom::Start(188))?;
|
|
|
|
|
let mut sync_check = [0u8; 1];
|
|
|
|
|
if reader.read(&mut sync_check)? == 1 && sync_check[0] == 0x47 {
|
|
|
|
|
return Ok(FileFormat::TransportStream);
|
|
|
|
|
}
|
|
|
|
|
// Check for 192-byte packets (M2TS)
|
|
|
|
|
reader.seek(SeekFrom::Start(192))?;
|
|
|
|
|
if reader.read(&mut sync_check)? == 1 && sync_check[0] == 0x47 {
|
|
|
|
|
return Ok(FileFormat::TransportStream);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// M2TS files start with 4-byte timestamp before sync byte
|
|
|
|
|
if bytes_read >= 5 && header[4] == 0x47 {
|
|
|
|
|
reader.seek(SeekFrom::Start(192 + 4))?;
|
|
|
|
|
let mut sync_check = [0u8; 1];
|
|
|
|
|
if reader.read(&mut sync_check)? == 1 && sync_check[0] == 0x47 {
|
|
|
|
|
return Ok(FileFormat::TransportStream);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(FileFormat::Unknown)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// List tracks in a Matroska (MKV/WebM) file
|
|
|
|
|
pub fn list_mkv_tracks(path: &Path) -> std::io::Result<Vec<TrackInfo>> {
|
|
|
|
|
let file = File::open(path)?;
|
|
|
|
|
let file_size = file.metadata()?.len();
|
|
|
|
|
let mut reader = BufReader::new(file);
|
|
|
|
|
let mut tracks = Vec::new();
|
|
|
|
|
|
|
|
|
|
// First, skip EBML header
|
|
|
|
|
let ebml_id = read_element_id(&mut reader)?;
|
|
|
|
|
if ebml_id != 0x1A45DFA3 {
|
|
|
|
|
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "Not a valid EBML file"));
|
|
|
|
|
}
|
|
|
|
|
let ebml_size = read_vint(&mut reader)?;
|
|
|
|
|
reader.seek(SeekFrom::Current(ebml_size as i64))?;
|
|
|
|
|
|
|
|
|
|
// Now look for Segment header
|
|
|
|
|
let segment_id = read_element_id(&mut reader)?;
|
|
|
|
|
if segment_id != 0x18538067 {
|
|
|
|
|
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "Segment not found"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Read segment size - may be "unknown" (all 1s)
|
|
|
|
|
let segment_size = read_vint(&mut reader)?;
|
|
|
|
|
let segment_start = reader.stream_position()?;
|
|
|
|
|
|
|
|
|
|
// If segment size is unknown (0xFFFFFFFFFFFFFF for 8-byte VINT), use file size
|
|
|
|
|
let segment_end = if segment_size >= 0x00FFFFFFFFFFFFFF {
|
|
|
|
|
file_size
|
|
|
|
|
} else {
|
|
|
|
|
std::cmp::min(segment_start + segment_size, file_size)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Limit how far we search to avoid processing huge files
|
|
|
|
|
let max_search_bytes = 50 * 1024 * 1024; // 50MB max search
|
|
|
|
|
let search_end = std::cmp::min(segment_end, segment_start + max_search_bytes);
|
|
|
|
|
|
|
|
|
|
while reader.stream_position()? < search_end {
|
|
|
|
|
let element_pos = reader.stream_position()?;
|
|
|
|
|
|
|
|
|
|
let element_id = match read_element_id(&mut reader) {
|
|
|
|
|
Ok(id) => id,
|
|
|
|
|
Err(_) => break,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let element_size = match read_vint(&mut reader) {
|
|
|
|
|
Ok(size) => size,
|
|
|
|
|
Err(_) => break,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let element_start = reader.stream_position()?;
|
|
|
|
|
|
|
|
|
|
// Tracks element ID: 0x1654AE6B
|
|
|
|
|
if element_id == 0x1654AE6B {
|
|
|
|
|
tracks = parse_mkv_tracks(&mut reader, element_size)?;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Skip known large elements we don't need
|
|
|
|
|
// Cluster: 0x1F43B675, Cues: 0x1C53BB6B, Attachments: 0x1941A469
|
|
|
|
|
if element_id == 0x1F43B675 || element_id == 0x1C53BB6B || element_id == 0x1941A469 {
|
|
|
|
|
// These are usually after tracks, stop searching
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Skip to next element, but check bounds
|
|
|
|
|
let next_pos = element_start.saturating_add(element_size);
|
|
|
|
|
if next_pos > search_end || next_pos <= element_pos {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
reader.seek(SeekFrom::Start(next_pos))?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(tracks)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn read_vint<R: Read>(reader: &mut R) -> std::io::Result<u64> {
|
|
|
|
|
let mut first_byte = [0u8; 1];
|
|
|
|
|
reader.read_exact(&mut first_byte)?;
|
|
|
|
|
|
|
|
|
|
let leading_zeros = first_byte[0].leading_zeros();
|
|
|
|
|
let length = (leading_zeros + 1) as usize;
|
|
|
|
|
|
|
|
|
|
if length > 8 {
|
|
|
|
|
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid VINT length"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mask to extract data bits from first byte
|
|
|
|
|
// For 1-byte: mask = 0x7F (127), for 2-byte: 0x3F, ..., for 8-byte: 0x00
|
|
|
|
|
let mask = if length == 8 { 0 } else { (1u8 << (8 - length)) - 1 };
|
|
|
|
|
let mut value = (first_byte[0] & mask) as u64;
|
|
|
|
|
|
|
|
|
|
for _ in 1..length {
|
|
|
|
|
let mut byte = [0u8; 1];
|
|
|
|
|
reader.read_exact(&mut byte)?;
|
|
|
|
|
value = (value << 8) | byte[0] as u64;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn read_element_id<R: Read>(reader: &mut R) -> std::io::Result<u32> {
|
|
|
|
|
let mut first_byte = [0u8; 1];
|
|
|
|
|
reader.read_exact(&mut first_byte)?;
|
|
|
|
|
|
|
|
|
|
let leading_zeros = first_byte[0].leading_zeros();
|
|
|
|
|
let length = (leading_zeros + 1) as usize;
|
|
|
|
|
|
|
|
|
|
let mut value = first_byte[0] as u32;
|
|
|
|
|
|
|
|
|
|
for _ in 1..length {
|
|
|
|
|
let mut byte = [0u8; 1];
|
|
|
|
|
reader.read_exact(&mut byte)?;
|
|
|
|
|
value = (value << 8) | byte[0] as u32;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parse_mkv_tracks<R: Read + Seek>(reader: &mut R, size: u64) -> std::io::Result<Vec<TrackInfo>> {
|
|
|
|
|
let mut tracks = Vec::new();
|
|
|
|
|
let end_pos = reader.stream_position()? + size;
|
|
|
|
|
|
|
|
|
|
while reader.stream_position()? < end_pos {
|
|
|
|
|
let element_id = read_element_id(reader)?;
|
|
|
|
|
let element_size = read_vint(reader)?;
|
|
|
|
|
let element_start = reader.stream_position()?;
|
|
|
|
|
|
|
|
|
|
// TrackEntry element ID: 0xAE
|
|
|
|
|
if element_id == 0xAE {
|
|
|
|
|
if let Ok(track) = parse_mkv_track_entry(reader, element_size) {
|
|
|
|
|
tracks.push(track);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
reader.seek(SeekFrom::Start(element_start + element_size))?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(tracks)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parse_mkv_track_entry<R: Read + Seek>(reader: &mut R, size: u64) -> std::io::Result<TrackInfo> {
|
|
|
|
|
let end_pos = reader.stream_position()? + size;
|
|
|
|
|
|
|
|
|
|
let mut track_number = 0u32;
|
|
|
|
|
let mut track_type = TrackType::Other("Unknown".to_string());
|
|
|
|
|
let mut codec = String::new();
|
|
|
|
|
let mut language = None;
|
|
|
|
|
|
|
|
|
|
while reader.stream_position()? < end_pos {
|
|
|
|
|
let element_id = read_element_id(reader)?;
|
|
|
|
|
let element_size = read_vint(reader)?;
|
|
|
|
|
let element_start = reader.stream_position()?;
|
|
|
|
|
|
|
|
|
|
match element_id {
|
|
|
|
|
0xD7 => { // TrackNumber
|
|
|
|
|
track_number = read_uint(reader, element_size)? as u32;
|
|
|
|
|
}
|
|
|
|
|
0x83 => { // TrackType
|
|
|
|
|
let type_val = read_uint(reader, element_size)?;
|
|
|
|
|
track_type = match type_val {
|
|
|
|
|
1 => TrackType::Video,
|
|
|
|
|
2 => TrackType::Audio,
|
|
|
|
|
0x11 => TrackType::Subtitle,
|
|
|
|
|
_ => TrackType::Other(format!("Type {}", type_val)),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
0x86 => { // CodecID
|
|
|
|
|
codec = read_string(reader, element_size)?;
|
|
|
|
|
}
|
|
|
|
|
0x22B59C => { // Language
|
|
|
|
|
language = Some(read_string(reader, element_size)?);
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
reader.seek(SeekFrom::Start(element_start + element_size))?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(TrackInfo {
|
|
|
|
|
track_number,
|
|
|
|
|
track_type,
|
|
|
|
|
codec,
|
|
|
|
|
language,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn read_uint<R: Read>(reader: &mut R, size: u64) -> std::io::Result<u64> {
|
|
|
|
|
let mut value = 0u64;
|
|
|
|
|
for _ in 0..size {
|
|
|
|
|
let mut byte = [0u8; 1];
|
|
|
|
|
reader.read_exact(&mut byte)?;
|
|
|
|
|
value = (value << 8) | byte[0] as u64;
|
|
|
|
|
}
|
|
|
|
|
Ok(value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn read_string<R: Read>(reader: &mut R, size: u64) -> std::io::Result<String> {
|
|
|
|
|
let mut buffer = vec![0u8; size as usize];
|
|
|
|
|
reader.read_exact(&mut buffer)?;
|
|
|
|
|
// Remove null terminators if present
|
|
|
|
|
while buffer.last() == Some(&0) {
|
|
|
|
|
buffer.pop();
|
|
|
|
|
}
|
|
|
|
|
Ok(String::from_utf8_lossy(&buffer).to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// List tracks in an MP4 file
|
|
|
|
|
pub fn list_mp4_tracks(path: &Path) -> std::io::Result<Vec<TrackInfo>> {
|
|
|
|
|
let file = File::open(path)?;
|
|
|
|
|
let mut reader = BufReader::new(file);
|
|
|
|
|
let file_size = reader.seek(SeekFrom::End(0))?;
|
|
|
|
|
reader.seek(SeekFrom::Start(0))?;
|
|
|
|
|
|
|
|
|
|
let mut tracks = Vec::new();
|
|
|
|
|
|
|
|
|
|
// Parse top-level boxes to find moov
|
|
|
|
|
while reader.stream_position()? < file_size {
|
|
|
|
|
let pos = reader.stream_position()?;
|
|
|
|
|
|
|
|
|
|
let mut size_buf = [0u8; 4];
|
|
|
|
|
if reader.read(&mut size_buf)? < 4 {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
let size = u32::from_be_bytes(size_buf) as u64;
|
|
|
|
|
|
|
|
|
|
let mut type_buf = [0u8; 4];
|
|
|
|
|
if reader.read(&mut type_buf)? < 4 {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let actual_size = if size == 1 {
|
|
|
|
|
// 64-bit size
|
|
|
|
|
let mut size64_buf = [0u8; 8];
|
|
|
|
|
reader.read_exact(&mut size64_buf)?;
|
|
|
|
|
u64::from_be_bytes(size64_buf)
|
|
|
|
|
} else if size == 0 {
|
|
|
|
|
// Box extends to end of file
|
|
|
|
|
file_size - pos
|
|
|
|
|
} else {
|
|
|
|
|
size
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if &type_buf == b"moov" {
|
|
|
|
|
// Parse moov box for tracks
|
|
|
|
|
let header_size = if size == 1 { 16 } else { 8 };
|
|
|
|
|
tracks = parse_mp4_moov(&mut reader, actual_size - header_size)?;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Skip to next box
|
|
|
|
|
if actual_size > 0 {
|
|
|
|
|
reader.seek(SeekFrom::Start(pos + actual_size))?;
|
|
|
|
|
} else {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(tracks)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parse_mp4_moov<R: Read + Seek>(reader: &mut R, size: u64) -> std::io::Result<Vec<TrackInfo>> {
|
|
|
|
|
let mut tracks = Vec::new();
|
|
|
|
|
let end_pos = reader.stream_position()? + size;
|
|
|
|
|
let mut track_num = 1u32;
|
|
|
|
|
|
|
|
|
|
while reader.stream_position()? < end_pos {
|
|
|
|
|
let pos = reader.stream_position()?;
|
|
|
|
|
|
|
|
|
|
let mut size_buf = [0u8; 4];
|
|
|
|
|
if reader.read(&mut size_buf)? < 4 {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
let box_size = u32::from_be_bytes(size_buf) as u64;
|
|
|
|
|
|
|
|
|
|
let mut type_buf = [0u8; 4];
|
|
|
|
|
if reader.read(&mut type_buf)? < 4 {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let actual_size = if box_size == 1 {
|
|
|
|
|
let mut size64_buf = [0u8; 8];
|
|
|
|
|
reader.read_exact(&mut size64_buf)?;
|
|
|
|
|
u64::from_be_bytes(size64_buf)
|
|
|
|
|
} else {
|
|
|
|
|
box_size
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if &type_buf == b"trak" {
|
|
|
|
|
let header_size = if box_size == 1 { 16 } else { 8 };
|
|
|
|
|
if let Ok(track) = parse_mp4_trak(reader, actual_size - header_size, track_num) {
|
|
|
|
|
tracks.push(track);
|
|
|
|
|
}
|
|
|
|
|
track_num += 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if actual_size > 0 {
|
|
|
|
|
reader.seek(SeekFrom::Start(pos + actual_size))?;
|
|
|
|
|
} else {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(tracks)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parse_mp4_trak<R: Read + Seek>(reader: &mut R, size: u64, track_num: u32) -> std::io::Result<TrackInfo> {
|
|
|
|
|
let end_pos = reader.stream_position()? + size;
|
|
|
|
|
|
|
|
|
|
let mut track_type = TrackType::Other("Unknown".to_string());
|
|
|
|
|
let mut codec = String::new();
|
|
|
|
|
let mut language = None;
|
|
|
|
|
|
|
|
|
|
while reader.stream_position()? < end_pos {
|
|
|
|
|
let pos = reader.stream_position()?;
|
|
|
|
|
|
|
|
|
|
let mut size_buf = [0u8; 4];
|
|
|
|
|
if reader.read(&mut size_buf)? < 4 {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
let box_size = u32::from_be_bytes(size_buf) as u64;
|
|
|
|
|
|
|
|
|
|
let mut type_buf = [0u8; 4];
|
|
|
|
|
if reader.read(&mut type_buf)? < 4 {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let actual_size = if box_size == 1 {
|
|
|
|
|
let mut size64_buf = [0u8; 8];
|
|
|
|
|
reader.read_exact(&mut size64_buf)?;
|
|
|
|
|
u64::from_be_bytes(size64_buf)
|
|
|
|
|
} else {
|
|
|
|
|
box_size
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if &type_buf == b"mdia" {
|
|
|
|
|
let header_size = if box_size == 1 { 16 } else { 8 };
|
|
|
|
|
let (t, c, l) = parse_mp4_mdia(reader, actual_size - header_size)?;
|
|
|
|
|
track_type = t;
|
|
|
|
|
codec = c;
|
|
|
|
|
language = l;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if actual_size > 0 {
|
|
|
|
|
reader.seek(SeekFrom::Start(pos + actual_size))?;
|
|
|
|
|
} else {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(TrackInfo {
|
|
|
|
|
track_number: track_num,
|
|
|
|
|
track_type,
|
|
|
|
|
codec,
|
|
|
|
|
language,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parse_mp4_mdia<R: Read + Seek>(reader: &mut R, size: u64) -> std::io::Result<(TrackType, String, Option<String>)> {
|
|
|
|
|
let end_pos = reader.stream_position()? + size;
|
|
|
|
|
|
|
|
|
|
let mut track_type = TrackType::Other("Unknown".to_string());
|
|
|
|
|
let mut codec = String::new();
|
|
|
|
|
let mut language = None;
|
|
|
|
|
|
|
|
|
|
while reader.stream_position()? < end_pos {
|
|
|
|
|
let pos = reader.stream_position()?;
|
|
|
|
|
|
|
|
|
|
let mut size_buf = [0u8; 4];
|
|
|
|
|
if reader.read(&mut size_buf)? < 4 {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
let box_size = u32::from_be_bytes(size_buf) as u64;
|
|
|
|
|
|
|
|
|
|
let mut type_buf = [0u8; 4];
|
|
|
|
|
if reader.read(&mut type_buf)? < 4 {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let actual_size = if box_size == 1 {
|
|
|
|
|
let mut size64_buf = [0u8; 8];
|
|
|
|
|
reader.read_exact(&mut size64_buf)?;
|
|
|
|
|
u64::from_be_bytes(size64_buf)
|
|
|
|
|
} else {
|
|
|
|
|
box_size
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
match &type_buf {
|
|
|
|
|
b"hdlr" => {
|
|
|
|
|
// Handler reference box - contains media type
|
|
|
|
|
reader.seek(SeekFrom::Current(8))?; // Skip version/flags and pre_defined
|
|
|
|
|
let mut handler_type = [0u8; 4];
|
|
|
|
|
reader.read_exact(&mut handler_type)?;
|
|
|
|
|
track_type = match &handler_type {
|
|
|
|
|
b"vide" => TrackType::Video,
|
|
|
|
|
b"soun" => TrackType::Audio,
|
|
|
|
|
b"subt" | b"sbtl" | b"text" => TrackType::Subtitle,
|
|
|
|
|
b"clcp" => TrackType::ClosedCaption,
|
|
|
|
|
_ => TrackType::Other(String::from_utf8_lossy(&handler_type).to_string()),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
b"mdhd" => {
|
|
|
|
|
// Media header box - contains language
|
|
|
|
|
let mut version = [0u8; 1];
|
|
|
|
|
reader.read_exact(&mut version)?;
|
|
|
|
|
reader.seek(SeekFrom::Current(3))?; // flags
|
|
|
|
|
|
|
|
|
|
if version[0] == 1 {
|
|
|
|
|
reader.seek(SeekFrom::Current(16))?; // creation/modification time (8+8)
|
|
|
|
|
} else {
|
|
|
|
|
reader.seek(SeekFrom::Current(8))?; // creation/modification time (4+4)
|
|
|
|
|
}
|
|
|
|
|
reader.seek(SeekFrom::Current(4))?; // timescale
|
|
|
|
|
if version[0] == 1 {
|
|
|
|
|
reader.seek(SeekFrom::Current(8))?; // duration
|
|
|
|
|
} else {
|
|
|
|
|
reader.seek(SeekFrom::Current(4))?; // duration
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut lang_buf = [0u8; 2];
|
|
|
|
|
reader.read_exact(&mut lang_buf)?;
|
|
|
|
|
let lang_code = u16::from_be_bytes(lang_buf);
|
|
|
|
|
// ISO 639-2/T language code packed in 15 bits
|
|
|
|
|
if lang_code != 0x55C4 { // 'und' (undefined)
|
|
|
|
|
let c1 = ((lang_code >> 10) & 0x1F) as u8 + 0x60;
|
|
|
|
|
let c2 = ((lang_code >> 5) & 0x1F) as u8 + 0x60;
|
|
|
|
|
let c3 = (lang_code & 0x1F) as u8 + 0x60;
|
|
|
|
|
language = Some(format!("{}{}{}", c1 as char, c2 as char, c3 as char));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
b"minf" => {
|
|
|
|
|
let header_size = if box_size == 1 { 16 } else { 8 };
|
|
|
|
|
codec = parse_mp4_minf(reader, actual_size - header_size)?;
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if actual_size > 0 {
|
|
|
|
|
reader.seek(SeekFrom::Start(pos + actual_size))?;
|
|
|
|
|
} else {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok((track_type, codec, language))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parse_mp4_minf<R: Read + Seek>(reader: &mut R, size: u64) -> std::io::Result<String> {
|
|
|
|
|
let end_pos = reader.stream_position()? + size;
|
|
|
|
|
|
|
|
|
|
while reader.stream_position()? < end_pos {
|
|
|
|
|
let pos = reader.stream_position()?;
|
|
|
|
|
|
|
|
|
|
let mut size_buf = [0u8; 4];
|
|
|
|
|
if reader.read(&mut size_buf)? < 4 {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
let box_size = u32::from_be_bytes(size_buf) as u64;
|
|
|
|
|
|
|
|
|
|
let mut type_buf = [0u8; 4];
|
|
|
|
|
if reader.read(&mut type_buf)? < 4 {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let actual_size = if box_size == 1 {
|
|
|
|
|
let mut size64_buf = [0u8; 8];
|
|
|
|
|
reader.read_exact(&mut size64_buf)?;
|
|
|
|
|
u64::from_be_bytes(size64_buf)
|
|
|
|
|
} else {
|
|
|
|
|
box_size
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if &type_buf == b"stbl" {
|
|
|
|
|
let header_size = if box_size == 1 { 16 } else { 8 };
|
|
|
|
|
return parse_mp4_stbl(reader, actual_size - header_size);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if actual_size > 0 {
|
|
|
|
|
reader.seek(SeekFrom::Start(pos + actual_size))?;
|
|
|
|
|
} else {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(String::new())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parse_mp4_stbl<R: Read + Seek>(reader: &mut R, size: u64) -> std::io::Result<String> {
|
|
|
|
|
let end_pos = reader.stream_position()? + size;
|
|
|
|
|
|
|
|
|
|
while reader.stream_position()? < end_pos {
|
|
|
|
|
let pos = reader.stream_position()?;
|
|
|
|
|
|
|
|
|
|
let mut size_buf = [0u8; 4];
|
|
|
|
|
if reader.read(&mut size_buf)? < 4 {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
let box_size = u32::from_be_bytes(size_buf) as u64;
|
|
|
|
|
|
|
|
|
|
let mut type_buf = [0u8; 4];
|
|
|
|
|
if reader.read(&mut type_buf)? < 4 {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let actual_size = if box_size == 1 {
|
|
|
|
|
let mut size64_buf = [0u8; 8];
|
|
|
|
|
reader.read_exact(&mut size64_buf)?;
|
|
|
|
|
u64::from_be_bytes(size64_buf)
|
|
|
|
|
} else {
|
|
|
|
|
box_size
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if &type_buf == b"stsd" {
|
|
|
|
|
// Sample description box
|
|
|
|
|
reader.seek(SeekFrom::Current(4))?; // version + flags
|
|
|
|
|
let mut entry_count_buf = [0u8; 4];
|
|
|
|
|
reader.read_exact(&mut entry_count_buf)?;
|
|
|
|
|
|
|
|
|
|
// Read first entry's codec
|
|
|
|
|
if reader.stream_position()? + 8 <= pos + actual_size {
|
|
|
|
|
reader.seek(SeekFrom::Current(4))?; // entry size
|
|
|
|
|
let mut codec_buf = [0u8; 4];
|
|
|
|
|
reader.read_exact(&mut codec_buf)?;
|
|
|
|
|
return Ok(String::from_utf8_lossy(&codec_buf).to_string());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if actual_size > 0 {
|
|
|
|
|
reader.seek(SeekFrom::Start(pos + actual_size))?;
|
|
|
|
|
} else {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(String::new())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// List tracks in a Transport Stream file
|
|
|
|
|
pub fn list_ts_tracks(path: &Path) -> std::io::Result<Vec<TrackInfo>> {
|
|
|
|
|
let file = File::open(path)?;
|
|
|
|
|
let mut reader = BufReader::new(file);
|
|
|
|
|
let file_size = reader.seek(SeekFrom::End(0))?;
|
|
|
|
|
reader.seek(SeekFrom::Start(0))?;
|
|
|
|
|
|
|
|
|
|
let mut tracks = Vec::new();
|
|
|
|
|
let mut pmt_pids: Vec<u16> = Vec::new();
|
|
|
|
|
let mut found_pids: std::collections::HashSet<u16> = std::collections::HashSet::new();
|
|
|
|
|
|
|
|
|
|
// Determine packet size (188 or 192 for M2TS)
|
|
|
|
|
let mut first_byte = [0u8; 1];
|
|
|
|
|
reader.read_exact(&mut first_byte)?;
|
|
|
|
|
reader.seek(SeekFrom::Start(0))?;
|
|
|
|
|
|
|
|
|
|
let (packet_size, offset) = if first_byte[0] == 0x47 {
|
|
|
|
|
(188usize, 0usize)
|
|
|
|
|
} else {
|
|
|
|
|
(192usize, 4usize) // M2TS has 4-byte timestamp prefix
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut packet = vec![0u8; packet_size];
|
|
|
|
|
let mut packets_read = 0u32;
|
|
|
|
|
let max_packets = 10000; // Limit scanning to avoid processing entire file
|
|
|
|
|
|
|
|
|
|
while reader.stream_position()? + packet_size as u64 <= file_size && packets_read < max_packets {
|
|
|
|
|
reader.read_exact(&mut packet)?;
|
|
|
|
|
packets_read += 1;
|
|
|
|
|
|
|
|
|
|
let sync_byte = packet[offset];
|
|
|
|
|
if sync_byte != 0x47 {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let pid = (((packet[offset + 1] & 0x1F) as u16) << 8) | packet[offset + 2] as u16;
|
|
|
|
|
let payload_start = (packet[offset + 1] & 0x40) != 0;
|
|
|
|
|
|
|
|
|
|
// PAT (PID 0)
|
|
|
|
|
if pid == 0 && payload_start {
|
|
|
|
|
let adaptation_field = (packet[offset + 3] & 0x30) >> 4;
|
|
|
|
|
let mut payload_offset = offset + 4;
|
|
|
|
|
|
|
|
|
|
if adaptation_field == 2 || adaptation_field == 3 {
|
|
|
|
|
let af_length = packet[payload_offset] as usize;
|
|
|
|
|
payload_offset += 1 + af_length;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if payload_offset < packet_size {
|
|
|
|
|
// Skip pointer field
|
|
|
|
|
let pointer = packet[payload_offset] as usize;
|
|
|
|
|
payload_offset += 1 + pointer;
|
|
|
|
|
|
|
|
|
|
if payload_offset + 8 < packet_size {
|
|
|
|
|
// Parse PAT
|
|
|
|
|
let section_length = (((packet[payload_offset + 1] & 0x0F) as usize) << 8)
|
|
|
|
|
| packet[payload_offset + 2] as usize;
|
|
|
|
|
payload_offset += 8; // Skip to program loop
|
|
|
|
|
|
|
|
|
|
let end = std::cmp::min(payload_offset + section_length - 9, packet_size - 4);
|
|
|
|
|
while payload_offset + 4 <= end {
|
|
|
|
|
let program_num = ((packet[payload_offset] as u16) << 8) | packet[payload_offset + 1] as u16;
|
|
|
|
|
let pmt_pid = (((packet[payload_offset + 2] & 0x1F) as u16) << 8) | packet[payload_offset + 3] as u16;
|
|
|
|
|
if program_num != 0 {
|
|
|
|
|
pmt_pids.push(pmt_pid);
|
|
|
|
|
}
|
|
|
|
|
payload_offset += 4;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// PMT
|
|
|
|
|
if pmt_pids.contains(&pid) && payload_start && !found_pids.contains(&pid) {
|
|
|
|
|
found_pids.insert(pid);
|
|
|
|
|
|
|
|
|
|
let adaptation_field = (packet[offset + 3] & 0x30) >> 4;
|
|
|
|
|
let mut payload_offset = offset + 4;
|
|
|
|
|
|
|
|
|
|
if adaptation_field == 2 || adaptation_field == 3 {
|
|
|
|
|
let af_length = packet[payload_offset] as usize;
|
|
|
|
|
payload_offset += 1 + af_length;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if payload_offset < packet_size {
|
|
|
|
|
let pointer = packet[payload_offset] as usize;
|
|
|
|
|
payload_offset += 1 + pointer;
|
|
|
|
|
|
|
|
|
|
if payload_offset + 12 < packet_size {
|
|
|
|
|
let section_length = (((packet[payload_offset + 1] & 0x0F) as usize) << 8)
|
|
|
|
|
| packet[payload_offset + 2] as usize;
|
|
|
|
|
let _pcr_pid = (((packet[payload_offset + 8] & 0x1F) as u16) << 8) | packet[payload_offset + 9] as u16;
|
|
|
|
|
let program_info_length = (((packet[payload_offset + 10] & 0x0F) as usize) << 8)
|
|
|
|
|
| packet[payload_offset + 11] as usize;
|
|
|
|
|
|
|
|
|
|
payload_offset += 12 + program_info_length;
|
|
|
|
|
let end = std::cmp::min(payload_offset + section_length - 13 - program_info_length, packet_size - 4);
|
|
|
|
|
|
|
|
|
|
let mut track_num = 1u32;
|
|
|
|
|
while payload_offset + 5 <= end {
|
|
|
|
|
let stream_type = packet[payload_offset];
|
|
|
|
|
let elementary_pid = (((packet[payload_offset + 1] & 0x1F) as u16) << 8) | packet[payload_offset + 2] as u16;
|
|
|
|
|
let es_info_length = (((packet[payload_offset + 3] & 0x0F) as usize) << 8) | packet[payload_offset + 4] as usize;
|
|
|
|
|
|
|
|
|
|
let (track_type, codec) = match stream_type {
|
|
|
|
|
0x01 | 0x02 => (TrackType::Video, "MPEG-1/2 Video".to_string()),
|
|
|
|
|
0x1B => (TrackType::Video, "H.264/AVC".to_string()),
|
|
|
|
|
0x24 => (TrackType::Video, "H.265/HEVC".to_string()),
|
|
|
|
|
0x03 | 0x04 => (TrackType::Audio, "MPEG Audio".to_string()),
|
|
|
|
|
0x0F => (TrackType::Audio, "AAC".to_string()),
|
|
|
|
|
0x81 => (TrackType::Audio, "AC-3".to_string()),
|
|
|
|
|
0x06 => (TrackType::Subtitle, "Private Data (may contain subtitles)".to_string()),
|
|
|
|
|
0x05 => (TrackType::Other("Private Section".to_string()), "Private Section".to_string()),
|
|
|
|
|
_ => (TrackType::Other(format!("Type 0x{:02X}", stream_type)), format!("Stream type 0x{:02X}", stream_type)),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
tracks.push(TrackInfo {
|
|
|
|
|
track_number: track_num,
|
|
|
|
|
track_type,
|
|
|
|
|
codec: format!("{} (PID {})", codec, elementary_pid),
|
|
|
|
|
language: None,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
track_num += 1;
|
|
|
|
|
payload_offset += 5 + es_info_length;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Stop if we've found PMT data
|
|
|
|
|
if !tracks.is_empty() && packets_read > 1000 {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(tracks)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Main function to list tracks for any supported file
|
|
|
|
|
pub fn list_tracks(path: &Path) -> Result<(), String> {
|
|
|
|
|
let format = detect_format(path).map_err(|e| format!("Error detecting file format: {}", e))?;
|
|
|
|
|
|
|
|
|
|
println!("\nCCExtractor Track Listing");
|
|
|
|
|
println!("-------------------------");
|
|
|
|
|
println!("File: {}", path.display());
|
|
|
|
|
|
|
|
|
|
let tracks = match format {
|
|
|
|
|
FileFormat::Mkv => {
|
|
|
|
|
println!("Format: Matroska (MKV/WebM)\n");
|
|
|
|
|
list_mkv_tracks(path).map_err(|e| format!("Error parsing MKV: {}", e))?
|
|
|
|
|
}
|
|
|
|
|
FileFormat::Mp4 => {
|
|
|
|
|
println!("Format: MP4/MOV\n");
|
|
|
|
|
list_mp4_tracks(path).map_err(|e| format!("Error parsing MP4: {}", e))?
|
|
|
|
|
}
|
|
|
|
|
FileFormat::TransportStream => {
|
|
|
|
|
println!("Format: MPEG Transport Stream\n");
|
|
|
|
|
list_ts_tracks(path).map_err(|e| format!("Error parsing TS: {}", e))?
|
|
|
|
|
}
|
|
|
|
|
FileFormat::Unknown => {
|
|
|
|
|
return Err("Unknown or unsupported file format".to_string());
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if tracks.is_empty() {
|
|
|
|
|
println!("No tracks found in file.");
|
|
|
|
|
} else {
|
|
|
|
|
println!("Available tracks:");
|
|
|
|
|
for track in &tracks {
|
|
|
|
|
print!(" Track {}: Type: {}", track.track_number, track.track_type);
|
|
|
|
|
if !track.codec.is_empty() {
|
|
|
|
|
print!(", Codec: {}", track.codec);
|
|
|
|
|
}
|
|
|
|
|
if let Some(ref lang) = track.language {
|
|
|
|
|
print!(", Language: {}", lang);
|
|
|
|
|
}
|
|
|
|
|
println!();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
println!("\nTrack listing completed.");
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|