mirror of
https://github.com/alfg/mp4-rust.git
synced 2024-06-02 13:39:54 +00:00
Update mp4info
This commit is contained in:
parent
4b82165efc
commit
18bc289cab
|
@ -24,11 +24,10 @@ fn copy<P: AsRef<Path>>(src_filename: &P, _dst_filename: &P) -> Result<()> {
|
|||
let size = src_file.metadata()?.len();
|
||||
let reader = BufReader::new(src_file);
|
||||
|
||||
let mut mp4 = mp4::Mp4Reader::new(reader);
|
||||
mp4.read(size)?;
|
||||
let mut mp4 = mp4::Mp4Reader::read_header(reader, size)?;
|
||||
|
||||
for tix in 0..mp4.track_count() {
|
||||
let track_id = tix + 1;
|
||||
for tix in 0..mp4.tracks().len() {
|
||||
let track_id = tix as u32 + 1;
|
||||
let sample_count = mp4.sample_count(track_id)?;
|
||||
for six in 0..sample_count {
|
||||
let sample_id = six + 1;
|
||||
|
|
|
@ -4,7 +4,7 @@ use std::io::prelude::*;
|
|||
use std::io::{self, BufReader};
|
||||
use std::path::Path;
|
||||
|
||||
use mp4::{Mp4Reader, Result, TrackType};
|
||||
use mp4::{Result, Mp4Track, TrackType};
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
|
@ -24,99 +24,46 @@ fn info<P: AsRef<Path>>(filename: &P) -> Result<()> {
|
|||
let size = f.metadata()?.len();
|
||||
let reader = BufReader::new(f);
|
||||
|
||||
let mut mp4 = Mp4Reader::new(reader);
|
||||
mp4.read(size)?;
|
||||
let mp4 = mp4::Mp4Reader::read_header(reader, size)?;
|
||||
|
||||
println!("Metadata:");
|
||||
println!(" size: {}", mp4.size());
|
||||
println!(
|
||||
" brands: {:?} {:?}\n",
|
||||
mp4.major_brand(),
|
||||
mp4.compatible_brands()
|
||||
);
|
||||
println!(" size : {}", mp4.size());
|
||||
println!(" major_brand : {}", mp4.major_brand());
|
||||
let mut compatible_brands = String::new();
|
||||
for brand in mp4.compatible_brands().iter() {
|
||||
compatible_brands.push_str(&brand.to_string());
|
||||
compatible_brands.push_str(",");
|
||||
}
|
||||
println!(" compatible_brands: {}", compatible_brands);
|
||||
println!(
|
||||
"Duration: {}, timescale: {}",
|
||||
mp4.duration()?,
|
||||
mp4.timescale()?
|
||||
mp4.duration(),
|
||||
mp4.timescale()
|
||||
);
|
||||
|
||||
for track in mp4.tracks().iter() {
|
||||
println!(" Track: {}", track.track_id());
|
||||
}
|
||||
|
||||
if let Some(ref moov) = mp4.moov {
|
||||
println!("Found {} Tracks", moov.traks.len());
|
||||
for trak in moov.traks.iter() {
|
||||
let tkhd = &trak.tkhd;
|
||||
println!("Track: {:?}", tkhd.track_id);
|
||||
println!(" flags: {:?}", tkhd.flags);
|
||||
println!(" id: {:?}", tkhd.track_id);
|
||||
println!(" duration: {:?}", tkhd.duration);
|
||||
if tkhd.width != 0 && tkhd.height != 0 {
|
||||
println!(" width: {:?}", tkhd.width);
|
||||
println!(" height: {:?}", tkhd.height);
|
||||
}
|
||||
|
||||
let mdia = &trak.mdia;
|
||||
let hdlr = &mdia.hdlr;
|
||||
let mdhd = &mdia.mdhd;
|
||||
let stts = &mdia.minf.stbl.stts;
|
||||
|
||||
println!(
|
||||
" type: {:?}",
|
||||
get_handler_type(hdlr.handler_type.value.as_ref())
|
||||
);
|
||||
println!(" language: {:?}", mdhd.language);
|
||||
|
||||
println!(" media:");
|
||||
println!(" sample count: {:?}", stts.entries[0].sample_count);
|
||||
println!(" timescale: {:?}", mdhd.timescale);
|
||||
println!(
|
||||
" duration: {:?} (media timescale units)",
|
||||
mdhd.duration
|
||||
);
|
||||
println!(
|
||||
" duration: {:?} (ms)",
|
||||
get_duration_ms(mdhd.duration, mdhd.timescale)
|
||||
);
|
||||
if get_handler_type(hdlr.handler_type.value.as_ref()) == TrackType::Video {
|
||||
println!(
|
||||
" frame rate: (computed): {:?}",
|
||||
get_framerate(stts.entries[0].sample_count, mdhd.duration, mdhd.timescale)
|
||||
);
|
||||
}
|
||||
}
|
||||
let media_info = match track.track_type() {
|
||||
TrackType::Video => video_info(track),
|
||||
TrackType::Audio => audio_info(track),
|
||||
_ => String::from("error")
|
||||
};
|
||||
println!(" Track: #{}({}) {}: {} ({:?}), {}", track.track_id(), track.language(),
|
||||
track.track_type(), track.media_type(), track.box_type(), media_info);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_handler_type(handler: &str) -> TrackType {
|
||||
let mut typ: TrackType = TrackType::Unknown;
|
||||
match handler {
|
||||
"vide" => typ = TrackType::Video,
|
||||
"soun" => typ = TrackType::Audio,
|
||||
"meta" => typ = TrackType::Unknown,
|
||||
_ => (),
|
||||
}
|
||||
return typ;
|
||||
fn video_info(track: &Mp4Track) -> String {
|
||||
format!("{}x{}, {} kb/s, {:.2} fps", track.width(), track.height(),
|
||||
track.bitrate() / 1000, track.frame_rate())
|
||||
}
|
||||
|
||||
fn get_duration_ms(duration: u64, timescale: u32) -> String {
|
||||
let ms = (duration as f64 / timescale as f64) * 1000.0;
|
||||
return format!("{:.2}", ms.floor());
|
||||
fn audio_info(track: &Mp4Track) -> String {
|
||||
let ch = match track.channel_count() {
|
||||
1 => String::from("mono"),
|
||||
2 => String::from("stereo"),
|
||||
n => format!("{}-ch", n),
|
||||
};
|
||||
format!("{} Hz, {}, {} kb/s", track.sample_rate(), ch, track.bitrate() / 1000)
|
||||
}
|
||||
|
||||
fn get_framerate(sample_count: u32, duration: u64, timescale: u32) -> String {
|
||||
let sc = (sample_count as f64) * 1000.0;
|
||||
let ms = (duration as f64 / timescale as f64) * 1000.0;
|
||||
return format!("{:.2}", sc / ms.floor());
|
||||
}
|
||||
|
||||
// fn creation_time(creation_time: u64) -> u64 {
|
||||
// // convert from MP4 epoch (1904-01-01) to Unix epoch (1970-01-01)
|
||||
// if creation_time >= 2082844800 {
|
||||
// creation_time - 2082844800
|
||||
// } else {
|
||||
// creation_time
|
||||
// }
|
||||
// }
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
|
||||
use std::convert::TryInto;
|
||||
use std::fmt;
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
|
||||
use crate::*;
|
||||
|
||||
pub(crate) mod avc;
|
||||
pub(crate) mod avc1;
|
||||
pub(crate) mod co64;
|
||||
pub(crate) mod ctts;
|
||||
pub(crate) mod edts;
|
||||
|
@ -180,7 +181,8 @@ impl From<BoxType> for FourCC {
|
|||
|
||||
impl fmt::Debug for FourCC {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.value)
|
||||
let code: u32 = self.into();
|
||||
write!(f, "{} / {:#010X}", self.value, code)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
|
||||
use num_rational::Ratio;
|
||||
use std::io::{Read, Seek, Write};
|
||||
|
||||
use crate::atoms::*;
|
||||
|
@ -6,9 +7,9 @@ use crate::atoms::*;
|
|||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Mp4aBox {
|
||||
pub data_reference_index: u16,
|
||||
pub channel_count: u16,
|
||||
pub channelcount: u16,
|
||||
pub samplesize: u16,
|
||||
pub samplerate: u32,
|
||||
pub samplerate: Ratio<u32>,
|
||||
pub esds: EsdsBox,
|
||||
}
|
||||
|
||||
|
@ -16,9 +17,9 @@ impl Default for Mp4aBox {
|
|||
fn default() -> Self {
|
||||
Mp4aBox {
|
||||
data_reference_index: 0,
|
||||
channel_count: 2,
|
||||
channelcount: 2,
|
||||
samplesize: 16,
|
||||
samplerate: 0, // XXX
|
||||
samplerate: Ratio::new_raw(48000 * 0x10000, 0x10000),
|
||||
esds: EsdsBox::default(),
|
||||
}
|
||||
}
|
||||
|
@ -43,10 +44,11 @@ impl<R: Read + Seek> ReadBox<&mut R> for Mp4aBox {
|
|||
let data_reference_index = reader.read_u16::<BigEndian>()?;
|
||||
|
||||
reader.read_u64::<BigEndian>()?; // reserved
|
||||
let channel_count = reader.read_u16::<BigEndian>()?;
|
||||
let channelcount = reader.read_u16::<BigEndian>()?;
|
||||
let samplesize = reader.read_u16::<BigEndian>()?;
|
||||
reader.read_u32::<BigEndian>()?; // pre-defined, reserved
|
||||
let samplerate = reader.read_u32::<BigEndian>()?;
|
||||
let samplerate_numer = reader.read_u32::<BigEndian>()?;
|
||||
let samplerate = Ratio::new_raw(samplerate_numer, 0x10000);
|
||||
|
||||
let header = BoxHeader::read(reader)?;
|
||||
let BoxHeader { name, size: s } = header;
|
||||
|
@ -57,7 +59,7 @@ impl<R: Read + Seek> ReadBox<&mut R> for Mp4aBox {
|
|||
|
||||
Ok(Mp4aBox {
|
||||
data_reference_index,
|
||||
channel_count,
|
||||
channelcount,
|
||||
samplesize,
|
||||
samplerate,
|
||||
esds,
|
||||
|
@ -78,10 +80,10 @@ impl<W: Write> WriteBox<&mut W> for Mp4aBox {
|
|||
writer.write_u16::<BigEndian>(self.data_reference_index)?;
|
||||
|
||||
writer.write_u64::<BigEndian>(0)?; // reserved
|
||||
writer.write_u16::<BigEndian>(self.channel_count)?;
|
||||
writer.write_u16::<BigEndian>(self.channelcount)?;
|
||||
writer.write_u16::<BigEndian>(self.samplesize)?;
|
||||
writer.write_u32::<BigEndian>(0)?; // reserved
|
||||
writer.write_u32::<BigEndian>(self.samplerate)?;
|
||||
writer.write_u32::<BigEndian>(*self.samplerate.numer())?;
|
||||
|
||||
self.esds.write_box(writer)?;
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
|
|||
use std::io::{Read, Seek, Write};
|
||||
|
||||
use crate::atoms::*;
|
||||
use crate::atoms::{avc::Avc1Box, mp4a::Mp4aBox};
|
||||
use crate::atoms::{avc1::Avc1Box, mp4a::Mp4aBox};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct StsdBox {
|
||||
|
|
|
@ -16,8 +16,8 @@ pub struct TkhdBox {
|
|||
pub alternate_group: u16,
|
||||
pub volume: Ratio<u16>,
|
||||
pub matrix: Matrix,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub width: Ratio<u32>,
|
||||
pub height: Ratio<u32>,
|
||||
}
|
||||
|
||||
impl Default for TkhdBox {
|
||||
|
@ -33,8 +33,8 @@ impl Default for TkhdBox {
|
|||
alternate_group: 0,
|
||||
volume: Ratio::new_raw(0x0100, 0x100),
|
||||
matrix: Matrix::default(),
|
||||
width: 0,
|
||||
height: 0,
|
||||
width: Ratio::new_raw(0, 0x10000),
|
||||
height: Ratio::new_raw(0, 0x10000),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -113,8 +113,10 @@ impl<R: Read + Seek> ReadBox<&mut R> for TkhdBox {
|
|||
w: reader.read_i32::<BigEndian>()?,
|
||||
};
|
||||
|
||||
let width = reader.read_u32::<BigEndian>()? >> 16;
|
||||
let height = reader.read_u32::<BigEndian>()? >> 16;
|
||||
let width_numer = reader.read_u32::<BigEndian>()?;
|
||||
let width = Ratio::new_raw(width_numer, 0x10000);
|
||||
let height_numer = reader.read_u32::<BigEndian>()?;
|
||||
let height = Ratio::new_raw(height_numer, 0x10000);
|
||||
|
||||
skip_read_to(reader, start + size)?;
|
||||
|
||||
|
@ -174,8 +176,8 @@ impl<W: Write> WriteBox<&mut W> for TkhdBox {
|
|||
writer.write_i32::<BigEndian>(self.matrix.y)?;
|
||||
writer.write_i32::<BigEndian>(self.matrix.w)?;
|
||||
|
||||
writer.write_u32::<BigEndian>(self.width << 16)?;
|
||||
writer.write_u32::<BigEndian>(self.height << 16)?;
|
||||
writer.write_u32::<BigEndian>(*self.width.numer())?;
|
||||
writer.write_u32::<BigEndian>(*self.height.numer())?;
|
||||
|
||||
Ok(size)
|
||||
}
|
||||
|
@ -210,8 +212,8 @@ mod tests {
|
|||
y: 0,
|
||||
w: 0x40000000,
|
||||
},
|
||||
width: 512,
|
||||
height: 288,
|
||||
width: Ratio::new_raw(512 * 0x10000, 0x10000),
|
||||
height: Ratio::new_raw(288 * 0x10000, 0x10000),
|
||||
};
|
||||
let mut buf = Vec::new();
|
||||
src_box.write_box(&mut buf).unwrap();
|
||||
|
@ -249,8 +251,8 @@ mod tests {
|
|||
y: 0,
|
||||
w: 0x40000000,
|
||||
},
|
||||
width: 512,
|
||||
height: 288,
|
||||
width: Ratio::new_raw(512 * 0x10000, 0x10000),
|
||||
height: Ratio::new_raw(288 * 0x10000, 0x10000),
|
||||
};
|
||||
let mut buf = Vec::new();
|
||||
src_box.write_box(&mut buf).unwrap();
|
||||
|
|
21
src/lib.rs
21
src/lib.rs
|
@ -1,4 +1,3 @@
|
|||
use std::convert::TryInto;
|
||||
use std::fmt;
|
||||
|
||||
pub use bytes::Bytes;
|
||||
|
@ -11,15 +10,10 @@ mod atoms;
|
|||
mod reader;
|
||||
pub use reader::Mp4Reader;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
mod track;
|
||||
pub use track::{Mp4Track, TrackType, MediaType};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum TrackType {
|
||||
Audio,
|
||||
Video,
|
||||
Metadata,
|
||||
Unknown,
|
||||
}
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Mp4Sample {
|
||||
|
@ -53,3 +47,12 @@ impl fmt::Display for Mp4Sample {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn creation_time(creation_time: u64) -> u64 {
|
||||
// convert from MP4 epoch (1904-01-01) to Unix epoch (1970-01-01)
|
||||
if creation_time >= 2082844800 {
|
||||
creation_time - 2082844800
|
||||
} else {
|
||||
creation_time
|
||||
}
|
||||
}
|
||||
|
|
320
src/reader.rs
320
src/reader.rs
|
@ -1,75 +1,87 @@
|
|||
use std::io::{Read, Seek, SeekFrom};
|
||||
|
||||
use crate::atoms::*;
|
||||
use crate::atoms::{mvhd::MvhdBox, stbl::StblBox, trak::TrakBox};
|
||||
use crate::{Bytes, Error, Mp4Sample, Result};
|
||||
use crate::{Error, Mp4Sample, Mp4Track, Result};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Mp4Reader<R> {
|
||||
reader: R,
|
||||
pub ftyp: FtypBox,
|
||||
pub moov: Option<MoovBox>,
|
||||
size: u64,
|
||||
ftyp: FtypBox,
|
||||
moov: MoovBox,
|
||||
|
||||
tracks: Vec<TrackReader>,
|
||||
tracks: Vec<Mp4Track>,
|
||||
size: u64,
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> Mp4Reader<R> {
|
||||
pub fn new(reader: R) -> Self {
|
||||
Mp4Reader {
|
||||
reader,
|
||||
ftyp: FtypBox::default(),
|
||||
moov: None,
|
||||
size: 0,
|
||||
tracks: Vec::new(),
|
||||
}
|
||||
}
|
||||
pub fn read_header(mut reader: R, size: u64) -> Result<Self> {
|
||||
let start = reader.seek(SeekFrom::Current(0))?;
|
||||
|
||||
pub fn size(&self) -> u64 {
|
||||
self.size
|
||||
}
|
||||
let mut ftyp = None;
|
||||
let mut moov = None;
|
||||
|
||||
pub fn read(&mut self, size: u64) -> Result<()> {
|
||||
let start = self.reader.seek(SeekFrom::Current(0))?;
|
||||
let mut current = start;
|
||||
while current < size {
|
||||
// Get box header.
|
||||
let header = BoxHeader::read(&mut self.reader)?;
|
||||
let header = BoxHeader::read(&mut reader)?;
|
||||
let BoxHeader { name, size: s } = header;
|
||||
|
||||
// Match and parse the atom boxes.
|
||||
match name {
|
||||
BoxType::FtypBox => {
|
||||
let ftyp = FtypBox::read_box(&mut self.reader, s)?;
|
||||
self.ftyp = ftyp;
|
||||
ftyp = Some(FtypBox::read_box(&mut reader, s)?);
|
||||
}
|
||||
BoxType::FreeBox => {
|
||||
skip_box(&mut self.reader, s)?;
|
||||
skip_box(&mut reader, s)?;
|
||||
}
|
||||
BoxType::MdatBox => {
|
||||
skip_box(&mut self.reader, s)?;
|
||||
skip_box(&mut reader, s)?;
|
||||
}
|
||||
BoxType::MoovBox => {
|
||||
let moov = MoovBox::read_box(&mut self.reader, s)?;
|
||||
self.moov = Some(moov);
|
||||
moov = Some(MoovBox::read_box(&mut reader, s)?);
|
||||
}
|
||||
BoxType::MoofBox => {
|
||||
skip_box(&mut self.reader, s)?;
|
||||
skip_box(&mut reader, s)?;
|
||||
}
|
||||
_ => {
|
||||
// XXX warn!()
|
||||
skip_box(&mut self.reader, s)?;
|
||||
skip_box(&mut reader, s)?;
|
||||
}
|
||||
}
|
||||
current = self.reader.seek(SeekFrom::Current(0))?;
|
||||
current = reader.seek(SeekFrom::Current(0))?;
|
||||
}
|
||||
if let Some(ref moov) = self.moov {
|
||||
|
||||
if ftyp.is_none() {
|
||||
return Err(Error::BoxNotFound(BoxType::FtypBox));
|
||||
}
|
||||
if moov.is_none() {
|
||||
return Err(Error::BoxNotFound(BoxType::MoovBox));
|
||||
}
|
||||
|
||||
let size = current - start;
|
||||
let tracks = if let Some(ref moov) = moov {
|
||||
let mut tracks = Vec::with_capacity(moov.traks.len());
|
||||
for (i, trak) in moov.traks.iter().enumerate() {
|
||||
self.tracks.push(TrackReader::new(i as u32 + 1, trak));
|
||||
let track_id = i as u32 + 1;
|
||||
assert_eq!(track_id, trak.tkhd.track_id);
|
||||
tracks.push(Mp4Track::new(track_id, trak));
|
||||
}
|
||||
}
|
||||
self.size = current - start;
|
||||
Ok(())
|
||||
tracks
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
Ok(Mp4Reader {
|
||||
reader,
|
||||
ftyp: ftyp.unwrap(),
|
||||
moov: moov.unwrap(),
|
||||
size,
|
||||
tracks,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn size(&self) -> u64 {
|
||||
self.size
|
||||
}
|
||||
|
||||
pub fn major_brand(&self) -> &FourCC {
|
||||
|
@ -84,29 +96,15 @@ impl<R: Read + Seek> Mp4Reader<R> {
|
|||
&self.ftyp.compatible_brands
|
||||
}
|
||||
|
||||
fn mvhd(&self) -> Result<&MvhdBox> {
|
||||
if let Some(ref moov) = self.moov {
|
||||
Ok(&moov.mvhd)
|
||||
} else {
|
||||
Err(Error::BoxNotFound(BoxType::VmhdBox))
|
||||
}
|
||||
pub fn duration(&self) -> u64 {
|
||||
self.moov.mvhd.duration
|
||||
}
|
||||
|
||||
pub fn duration(&self) -> Result<u64> {
|
||||
let mvhd = self.mvhd()?;
|
||||
Ok(mvhd.duration)
|
||||
pub fn timescale(&self) -> u32 {
|
||||
self.moov.mvhd.timescale
|
||||
}
|
||||
|
||||
pub fn timescale(&self) -> Result<u32> {
|
||||
let mvhd = self.mvhd()?;
|
||||
Ok(mvhd.timescale)
|
||||
}
|
||||
|
||||
pub fn track_count(&self) -> u32 {
|
||||
self.tracks.len() as u32
|
||||
}
|
||||
|
||||
pub fn tracks(&self) -> &[TrackReader] {
|
||||
pub fn tracks(&self) -> &[Mp4Track] {
|
||||
&self.tracks
|
||||
}
|
||||
|
||||
|
@ -134,217 +132,3 @@ impl<R: Read + Seek> Mp4Reader<R> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TrackReader {
|
||||
track_id: u32,
|
||||
trak: TrakBox,
|
||||
}
|
||||
|
||||
impl TrackReader {
|
||||
pub(crate) fn new(track_id: u32, trak: &TrakBox) -> Self {
|
||||
let trak = trak.clone();
|
||||
Self { track_id, trak }
|
||||
}
|
||||
|
||||
pub fn track_id(&self) -> u32 {
|
||||
self.track_id
|
||||
}
|
||||
|
||||
fn stbl(&self) -> &StblBox {
|
||||
&self.trak.mdia.minf.stbl
|
||||
}
|
||||
|
||||
fn stsc_index(&self, sample_id: u32) -> usize {
|
||||
let stsc = &self.stbl().stsc;
|
||||
|
||||
for (i, entry) in stsc.entries.iter().enumerate() {
|
||||
if sample_id < entry.first_sample {
|
||||
assert_ne!(i, 0);
|
||||
return i - 1;
|
||||
}
|
||||
}
|
||||
|
||||
assert_ne!(stsc.entries.len(), 0);
|
||||
stsc.entries.len() - 1
|
||||
}
|
||||
|
||||
fn chunk_offset(&self, chunk_id: u32) -> Result<u64> {
|
||||
let stbl = self.stbl();
|
||||
|
||||
if let Some(ref stco) = stbl.stco {
|
||||
if let Some(offset) = stco.entries.get(chunk_id as usize - 1) {
|
||||
return Ok(*offset as u64);
|
||||
} else {
|
||||
return Err(Error::EntryInStblNotFound(
|
||||
self.track_id,
|
||||
BoxType::StcoBox,
|
||||
chunk_id,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
if let Some(ref co64) = stbl.co64 {
|
||||
if let Some(offset) = co64.entries.get(chunk_id as usize - 1) {
|
||||
return Ok(*offset);
|
||||
} else {
|
||||
return Err(Error::EntryInStblNotFound(
|
||||
self.track_id,
|
||||
BoxType::Co64Box,
|
||||
chunk_id,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(stbl.stco.is_some() || stbl.co64.is_some());
|
||||
return Err(Error::Box2NotFound(BoxType::StcoBox, BoxType::Co64Box));
|
||||
}
|
||||
|
||||
fn ctts_index(&self, sample_id: u32) -> Result<(usize, u32)> {
|
||||
let stbl = self.stbl();
|
||||
|
||||
assert!(stbl.ctts.is_some());
|
||||
let ctts = if let Some(ref ctts) = stbl.ctts {
|
||||
ctts
|
||||
} else {
|
||||
return Err(Error::BoxInStblNotFound(self.track_id, BoxType::CttsBox));
|
||||
};
|
||||
|
||||
let mut sample_count = 1;
|
||||
for (i, entry) in ctts.entries.iter().enumerate() {
|
||||
if sample_id <= sample_count + entry.sample_count - 1 {
|
||||
return Ok((i, sample_count));
|
||||
}
|
||||
sample_count += entry.sample_count;
|
||||
}
|
||||
|
||||
return Err(Error::EntryInStblNotFound(
|
||||
self.track_id,
|
||||
BoxType::CttsBox,
|
||||
sample_id,
|
||||
));
|
||||
}
|
||||
|
||||
pub fn sample_count(&self) -> u32 {
|
||||
let stsz = &self.stbl().stsz;
|
||||
stsz.sample_sizes.len() as u32
|
||||
}
|
||||
|
||||
pub fn sample_size(&self, sample_id: u32) -> Result<u32> {
|
||||
let stsz = &self.stbl().stsz;
|
||||
if stsz.sample_size > 0 {
|
||||
return Ok(stsz.sample_size);
|
||||
}
|
||||
if let Some(size) = stsz.sample_sizes.get(sample_id as usize - 1) {
|
||||
Ok(*size)
|
||||
} else {
|
||||
return Err(Error::EntryInStblNotFound(
|
||||
self.track_id,
|
||||
BoxType::StszBox,
|
||||
sample_id,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sample_offset(&self, sample_id: u32) -> Result<u64> {
|
||||
let stsc_index = self.stsc_index(sample_id);
|
||||
|
||||
let stsc = &self.stbl().stsc;
|
||||
let stsc_entry = stsc.entries.get(stsc_index).unwrap();
|
||||
|
||||
let first_chunk = stsc_entry.first_chunk;
|
||||
let first_sample = stsc_entry.first_sample;
|
||||
let samples_per_chunk = stsc_entry.samples_per_chunk;
|
||||
|
||||
let chunk_id = first_chunk + (sample_id - first_sample) / samples_per_chunk;
|
||||
|
||||
let chunk_offset = self.chunk_offset(chunk_id)?;
|
||||
|
||||
let first_sample_in_chunk = sample_id - (sample_id - first_sample) % samples_per_chunk;
|
||||
|
||||
let mut sample_offset = 0;
|
||||
for i in first_sample_in_chunk..sample_id {
|
||||
sample_offset += self.sample_size(i)?;
|
||||
}
|
||||
|
||||
Ok(chunk_offset + sample_offset as u64)
|
||||
}
|
||||
|
||||
pub fn sample_time(&self, sample_id: u32) -> Result<(u64, u32)> {
|
||||
let stts = &self.stbl().stts;
|
||||
|
||||
let mut sample_count = 1;
|
||||
let mut elapsed = 0;
|
||||
|
||||
for entry in stts.entries.iter() {
|
||||
if sample_id <= sample_count + entry.sample_count - 1 {
|
||||
let start_time =
|
||||
(sample_id - sample_count) as u64 * entry.sample_delta as u64 + elapsed;
|
||||
return Ok((start_time, entry.sample_delta));
|
||||
}
|
||||
|
||||
sample_count += entry.sample_count;
|
||||
elapsed += entry.sample_count as u64 * entry.sample_delta as u64;
|
||||
}
|
||||
|
||||
return Err(Error::EntryInStblNotFound(
|
||||
self.track_id,
|
||||
BoxType::SttsBox,
|
||||
sample_id,
|
||||
));
|
||||
}
|
||||
|
||||
pub fn sample_rendering_offset(&self, sample_id: u32) -> i32 {
|
||||
let stbl = self.stbl();
|
||||
|
||||
if let Some(ref ctts) = stbl.ctts {
|
||||
if let Ok((ctts_index, _)) = self.ctts_index(sample_id) {
|
||||
let ctts_entry = ctts.entries.get(ctts_index).unwrap();
|
||||
return ctts_entry.sample_offset;
|
||||
}
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
pub fn is_sync_sample(&self, sample_id: u32) -> bool {
|
||||
let stbl = self.stbl();
|
||||
|
||||
if let Some(ref stss) = stbl.stss {
|
||||
match stss.entries.binary_search(&sample_id) {
|
||||
Ok(_) => true,
|
||||
Err(_) => false,
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_sample<R: Read + Seek>(
|
||||
&self,
|
||||
reader: &mut R,
|
||||
sample_id: u32,
|
||||
) -> Result<Option<Mp4Sample>> {
|
||||
let sample_size = match self.sample_size(sample_id) {
|
||||
Ok(size) => size,
|
||||
Err(Error::EntryInStblNotFound(_, _, _)) => return Ok(None),
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
let sample_offset = self.sample_offset(sample_id).unwrap(); // XXX
|
||||
|
||||
let mut buffer = vec![0x0u8; sample_size as usize];
|
||||
reader.seek(SeekFrom::Start(sample_offset))?;
|
||||
reader.read_exact(&mut buffer)?;
|
||||
|
||||
let (start_time, duration) = self.sample_time(sample_id).unwrap(); // XXX
|
||||
let rendering_offset = self.sample_rendering_offset(sample_id);
|
||||
let is_sync = self.is_sync_sample(sample_id);
|
||||
|
||||
Ok(Some(Mp4Sample {
|
||||
start_time,
|
||||
duration,
|
||||
rendering_offset,
|
||||
is_sync,
|
||||
bytes: Bytes::from(buffer),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
384
src/track.rs
Normal file
384
src/track.rs
Normal file
|
@ -0,0 +1,384 @@
|
|||
use std::fmt;
|
||||
use std::time::Duration;
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
|
||||
use crate::atoms::*;
|
||||
use crate::atoms::{trak::TrakBox, stbl::StblBox};
|
||||
use crate::{Bytes, Error, Mp4Sample, Result};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum TrackType {
|
||||
Video,
|
||||
Audio,
|
||||
Hint,
|
||||
Text,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl fmt::Display for TrackType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let s = match self {
|
||||
TrackType::Video => "video",
|
||||
TrackType::Audio => "audio",
|
||||
TrackType::Hint => "hint",
|
||||
TrackType::Text => "text",
|
||||
TrackType::Unknown => "unknown", // XXX
|
||||
};
|
||||
write!(f, "{}", s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for TrackType {
|
||||
fn from(handler: &str) -> TrackType {
|
||||
match handler {
|
||||
"vide" => TrackType::Video,
|
||||
"soun" => TrackType::Audio,
|
||||
"hint" => TrackType::Hint,
|
||||
"text" => TrackType::Text,
|
||||
_ => TrackType::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&FourCC> for TrackType {
|
||||
fn from(fourcc: &FourCC) -> TrackType {
|
||||
TrackType::from(fourcc.value.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum MediaType {
|
||||
H264,
|
||||
AAC,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl fmt::Display for MediaType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let s = match self {
|
||||
MediaType::H264 => "h264",
|
||||
MediaType::AAC => "aac",
|
||||
MediaType::Unknown => "unknown", // XXX
|
||||
};
|
||||
write!(f, "{}", s)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Mp4Track {
|
||||
track_id: u32,
|
||||
track_type: TrackType,
|
||||
media_type: MediaType,
|
||||
trak: TrakBox,
|
||||
}
|
||||
|
||||
impl Mp4Track {
|
||||
pub(crate) fn new(track_id: u32, trak: &TrakBox) -> Self {
|
||||
let trak = trak.clone();
|
||||
let track_type = (&trak.mdia.hdlr.handler_type).into();
|
||||
let media_type = if trak.mdia.minf.stbl.stsd.avc1.is_some() {
|
||||
MediaType::H264
|
||||
} else if trak.mdia.minf.stbl.stsd.mp4a.is_some() {
|
||||
MediaType::AAC
|
||||
} else {
|
||||
MediaType::Unknown
|
||||
};
|
||||
Self { track_id, track_type, media_type, trak }
|
||||
}
|
||||
|
||||
pub fn track_id(&self) -> u32 {
|
||||
self.track_id
|
||||
}
|
||||
|
||||
pub fn track_type(&self) -> TrackType {
|
||||
self.track_type
|
||||
}
|
||||
|
||||
pub fn media_type(&self) -> MediaType {
|
||||
self.media_type
|
||||
}
|
||||
|
||||
pub fn box_type(&self) -> FourCC {
|
||||
if self.trak.mdia.minf.stbl.stsd.avc1.is_some() {
|
||||
FourCC::from(BoxType::Avc1Box)
|
||||
} else if self.trak.mdia.minf.stbl.stsd.mp4a.is_some() {
|
||||
FourCC::from(BoxType::Mp4aBox)
|
||||
} else {
|
||||
FourCC::from("null") // XXX
|
||||
}
|
||||
}
|
||||
|
||||
pub fn width(&self) -> u16 {
|
||||
if let Some(ref avc1) = self.trak.mdia.minf.stbl.stsd.avc1 {
|
||||
avc1.width
|
||||
} else {
|
||||
self.trak.tkhd.width.to_integer() as u16
|
||||
}
|
||||
}
|
||||
|
||||
pub fn height(&self) -> u16 {
|
||||
if let Some(ref avc1) = self.trak.mdia.minf.stbl.stsd.avc1 {
|
||||
avc1.height
|
||||
} else {
|
||||
self.trak.tkhd.height.to_integer() as u16
|
||||
}
|
||||
}
|
||||
|
||||
pub fn frame_rate(&self) -> f64 {
|
||||
let dur_sec_f64 = self.duration().as_secs_f64();
|
||||
if dur_sec_f64 > 0.0 {
|
||||
self.sample_count() as f64 / dur_sec_f64
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sample_rate(&self) -> u32 {
|
||||
if let Some(ref mp4a) = self.trak.mdia.minf.stbl.stsd.mp4a {
|
||||
mp4a.samplerate.to_integer() as u32
|
||||
} else {
|
||||
0 // XXX
|
||||
}
|
||||
}
|
||||
|
||||
pub fn channel_count(&self) -> u16 {
|
||||
if let Some(ref mp4a) = self.trak.mdia.minf.stbl.stsd.mp4a {
|
||||
mp4a.channelcount
|
||||
} else {
|
||||
0 // XXX
|
||||
}
|
||||
}
|
||||
|
||||
pub fn language(&self) -> &str {
|
||||
&self.trak.mdia.mdhd.language
|
||||
}
|
||||
|
||||
pub fn timescale(&self) -> u32 {
|
||||
self.trak.mdia.mdhd.timescale
|
||||
}
|
||||
|
||||
pub fn duration(&self) -> Duration {
|
||||
Duration::from_micros(self.trak.mdia.mdhd.duration * 1_000_000
|
||||
/ self.trak.mdia.mdhd.timescale as u64)
|
||||
}
|
||||
|
||||
pub fn bitrate(&self) -> u32 {
|
||||
let dur_sec = self.duration().as_secs();
|
||||
if dur_sec > 0 {
|
||||
let bitrate = self.total_sample_size() * 8 / dur_sec;
|
||||
bitrate as u32
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sample_count(&self) -> u32 {
|
||||
let stsz = &self.stbl().stsz;
|
||||
stsz.sample_sizes.len() as u32
|
||||
}
|
||||
|
||||
fn stbl(&self) -> &StblBox {
|
||||
&self.trak.mdia.minf.stbl
|
||||
}
|
||||
|
||||
fn stsc_index(&self, sample_id: u32) -> usize {
|
||||
let stsc = &self.stbl().stsc;
|
||||
|
||||
for (i, entry) in stsc.entries.iter().enumerate() {
|
||||
if sample_id < entry.first_sample {
|
||||
assert_ne!(i, 0);
|
||||
return i - 1;
|
||||
}
|
||||
}
|
||||
|
||||
assert_ne!(stsc.entries.len(), 0);
|
||||
stsc.entries.len() - 1
|
||||
}
|
||||
|
||||
fn chunk_offset(&self, chunk_id: u32) -> Result<u64> {
|
||||
let stbl = self.stbl();
|
||||
|
||||
if let Some(ref stco) = stbl.stco {
|
||||
if let Some(offset) = stco.entries.get(chunk_id as usize - 1) {
|
||||
return Ok(*offset as u64);
|
||||
} else {
|
||||
return Err(Error::EntryInStblNotFound(
|
||||
self.track_id,
|
||||
BoxType::StcoBox,
|
||||
chunk_id,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
if let Some(ref co64) = stbl.co64 {
|
||||
if let Some(offset) = co64.entries.get(chunk_id as usize - 1) {
|
||||
return Ok(*offset);
|
||||
} else {
|
||||
return Err(Error::EntryInStblNotFound(
|
||||
self.track_id,
|
||||
BoxType::Co64Box,
|
||||
chunk_id,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(stbl.stco.is_some() || stbl.co64.is_some());
|
||||
return Err(Error::Box2NotFound(BoxType::StcoBox, BoxType::Co64Box));
|
||||
}
|
||||
|
||||
fn ctts_index(&self, sample_id: u32) -> Result<(usize, u32)> {
|
||||
let stbl = self.stbl();
|
||||
|
||||
assert!(stbl.ctts.is_some());
|
||||
let ctts = if let Some(ref ctts) = stbl.ctts {
|
||||
ctts
|
||||
} else {
|
||||
return Err(Error::BoxInStblNotFound(self.track_id, BoxType::CttsBox));
|
||||
};
|
||||
|
||||
let mut sample_count = 1;
|
||||
for (i, entry) in ctts.entries.iter().enumerate() {
|
||||
if sample_id <= sample_count + entry.sample_count - 1 {
|
||||
return Ok((i, sample_count));
|
||||
}
|
||||
sample_count += entry.sample_count;
|
||||
}
|
||||
|
||||
return Err(Error::EntryInStblNotFound(
|
||||
self.track_id,
|
||||
BoxType::CttsBox,
|
||||
sample_id,
|
||||
));
|
||||
}
|
||||
|
||||
fn sample_size(&self, sample_id: u32) -> Result<u32> {
|
||||
let stsz = &self.stbl().stsz;
|
||||
if stsz.sample_size > 0 {
|
||||
return Ok(stsz.sample_size);
|
||||
}
|
||||
if let Some(size) = stsz.sample_sizes.get(sample_id as usize - 1) {
|
||||
Ok(*size)
|
||||
} else {
|
||||
return Err(Error::EntryInStblNotFound(
|
||||
self.track_id,
|
||||
BoxType::StszBox,
|
||||
sample_id,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn total_sample_size(&self) -> u64 {
|
||||
let stsz = &self.stbl().stsz;
|
||||
if stsz.sample_size > 0 {
|
||||
stsz.sample_size as u64 * self.sample_count() as u64
|
||||
} else {
|
||||
let mut total_size = 0;
|
||||
for size in stsz.sample_sizes.iter() {
|
||||
total_size += *size as u64;
|
||||
}
|
||||
total_size
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_offset(&self, sample_id: u32) -> Result<u64> {
|
||||
let stsc_index = self.stsc_index(sample_id);
|
||||
|
||||
let stsc = &self.stbl().stsc;
|
||||
let stsc_entry = stsc.entries.get(stsc_index).unwrap();
|
||||
|
||||
let first_chunk = stsc_entry.first_chunk;
|
||||
let first_sample = stsc_entry.first_sample;
|
||||
let samples_per_chunk = stsc_entry.samples_per_chunk;
|
||||
|
||||
let chunk_id = first_chunk + (sample_id - first_sample) / samples_per_chunk;
|
||||
|
||||
let chunk_offset = self.chunk_offset(chunk_id)?;
|
||||
|
||||
let first_sample_in_chunk = sample_id - (sample_id - first_sample) % samples_per_chunk;
|
||||
|
||||
let mut sample_offset = 0;
|
||||
for i in first_sample_in_chunk..sample_id {
|
||||
sample_offset += self.sample_size(i)?;
|
||||
}
|
||||
|
||||
Ok(chunk_offset + sample_offset as u64)
|
||||
}
|
||||
|
||||
fn sample_time(&self, sample_id: u32) -> Result<(u64, u32)> {
|
||||
let stts = &self.stbl().stts;
|
||||
|
||||
let mut sample_count = 1;
|
||||
let mut elapsed = 0;
|
||||
|
||||
for entry in stts.entries.iter() {
|
||||
if sample_id <= sample_count + entry.sample_count - 1 {
|
||||
let start_time =
|
||||
(sample_id - sample_count) as u64 * entry.sample_delta as u64 + elapsed;
|
||||
return Ok((start_time, entry.sample_delta));
|
||||
}
|
||||
|
||||
sample_count += entry.sample_count;
|
||||
elapsed += entry.sample_count as u64 * entry.sample_delta as u64;
|
||||
}
|
||||
|
||||
return Err(Error::EntryInStblNotFound(
|
||||
self.track_id,
|
||||
BoxType::SttsBox,
|
||||
sample_id,
|
||||
));
|
||||
}
|
||||
|
||||
fn sample_rendering_offset(&self, sample_id: u32) -> i32 {
|
||||
let stbl = self.stbl();
|
||||
|
||||
if let Some(ref ctts) = stbl.ctts {
|
||||
if let Ok((ctts_index, _)) = self.ctts_index(sample_id) {
|
||||
let ctts_entry = ctts.entries.get(ctts_index).unwrap();
|
||||
return ctts_entry.sample_offset;
|
||||
}
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
fn is_sync_sample(&self, sample_id: u32) -> bool {
|
||||
let stbl = self.stbl();
|
||||
|
||||
if let Some(ref stss) = stbl.stss {
|
||||
match stss.entries.binary_search(&sample_id) {
|
||||
Ok(_) => true,
|
||||
Err(_) => false,
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn read_sample<R: Read + Seek>(
|
||||
&self,
|
||||
reader: &mut R,
|
||||
sample_id: u32,
|
||||
) -> Result<Option<Mp4Sample>> {
|
||||
let sample_size = match self.sample_size(sample_id) {
|
||||
Ok(size) => size,
|
||||
Err(Error::EntryInStblNotFound(_, _, _)) => return Ok(None),
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
let sample_offset = self.sample_offset(sample_id).unwrap(); // XXX
|
||||
|
||||
let mut buffer = vec![0x0u8; sample_size as usize];
|
||||
reader.seek(SeekFrom::Start(sample_offset))?;
|
||||
reader.read_exact(&mut buffer)?;
|
||||
|
||||
let (start_time, duration) = self.sample_time(sample_id).unwrap(); // XXX
|
||||
let rendering_offset = self.sample_rendering_offset(sample_id);
|
||||
let is_sync = self.is_sync_sample(sample_id);
|
||||
|
||||
Ok(Some(Mp4Sample {
|
||||
start_time,
|
||||
duration,
|
||||
rendering_offset,
|
||||
is_sync,
|
||||
bytes: Bytes::from(buffer),
|
||||
}))
|
||||
}
|
||||
}
|
28
tests/lib.rs
28
tests/lib.rs
|
@ -1,4 +1,4 @@
|
|||
use mp4;
|
||||
use mp4::{TrackType, MediaType};
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
|
||||
|
@ -9,8 +9,7 @@ fn test_read_mp4() {
|
|||
let size = f.metadata().unwrap().len();
|
||||
let reader = BufReader::new(f);
|
||||
|
||||
let mut mp4 = mp4::Mp4Reader::new(reader);
|
||||
mp4.read(size).unwrap();
|
||||
let mut mp4 = mp4::Mp4Reader::read_header(reader, size).unwrap();
|
||||
|
||||
assert_eq!(2591, mp4.size());
|
||||
|
||||
|
@ -30,9 +29,9 @@ fn test_read_mp4() {
|
|||
assert_eq!(t, true);
|
||||
}
|
||||
|
||||
assert_eq!(mp4.duration().unwrap(), 62);
|
||||
assert_eq!(mp4.timescale().unwrap(), 1000);
|
||||
assert_eq!(mp4.track_count(), 2);
|
||||
assert_eq!(mp4.duration(), 62);
|
||||
assert_eq!(mp4.timescale(), 1000);
|
||||
assert_eq!(mp4.tracks().len(), 2);
|
||||
|
||||
let sample_count = mp4.sample_count(1).unwrap();
|
||||
assert_eq!(sample_count, 0);
|
||||
|
@ -78,4 +77,21 @@ fn test_read_mp4() {
|
|||
|
||||
let eos = mp4.read_sample(2, 4).unwrap();
|
||||
assert!(eos.is_none());
|
||||
|
||||
// track #1
|
||||
let track1 = mp4.tracks().get(0).unwrap();
|
||||
assert_eq!(track1.track_id(), 1);
|
||||
assert_eq!(track1.track_type(), TrackType::Video);
|
||||
assert_eq!(track1.media_type(), MediaType::H264);
|
||||
assert_eq!(track1.width(), 320);
|
||||
assert_eq!(track1.height(), 240);
|
||||
assert_eq!(track1.bitrate(), 0); // XXX
|
||||
assert_eq!(track1.frame_rate(), 0.0); // XXX
|
||||
|
||||
// track #2
|
||||
let track2 = mp4.tracks().get(1).unwrap();
|
||||
assert_eq!(track2.track_type(), TrackType::Audio);
|
||||
assert_eq!(track2.media_type(), MediaType::AAC);
|
||||
assert_eq!(track2.sample_rate(), 48000);
|
||||
assert_eq!(track2.bitrate(), 0); // XXX
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue