piss
This commit is contained in:
commit
96eff29088
|
@ -0,0 +1 @@
|
||||||
|
/target
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,24 @@
|
||||||
|
[package]
|
||||||
|
name = "trk_dec"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "3.0", features = ["derive"] }
|
||||||
|
reqwest = { version = "0.11", features = ["rustls-tls", "json"], default-features = false }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_urlencoded = "0.7"
|
||||||
|
serde_json = "1.0"
|
||||||
|
tokio = { version = "1.17", features = ["rt-multi-thread", "macros"] }
|
||||||
|
md-5 = "0.10"
|
||||||
|
hex = "0.4"
|
||||||
|
byteorder = "1.4"
|
||||||
|
|
||||||
|
base64 = "0.13"
|
||||||
|
hkdf = "0.12"
|
||||||
|
sha2 = "0.10"
|
||||||
|
aes = "0.8"
|
||||||
|
cbc = "0.1"
|
||||||
|
ctr = "0.9"
|
|
@ -0,0 +1,5 @@
|
||||||
|
# qobuz decryptor crap
|
||||||
|
|
||||||
|
build: it's `cargo build` u ass
|
||||||
|
|
||||||
|
usage: `./trk_dec <auth token> <track id> <format id>`
|
|
@ -0,0 +1,202 @@
|
||||||
|
use std::{time::{SystemTime, UNIX_EPOCH}, io::Write};
|
||||||
|
|
||||||
|
use reqwest::Client;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::{Serialize, de::DeserializeOwned};
|
||||||
|
use serde_json::json;
|
||||||
|
use clap::ArgEnum;
|
||||||
|
|
||||||
|
use crate::crypto::{gen_signed_req, get_session_key, get_track_key, decrypt_frame};
|
||||||
|
use crate::mp4::{parse_segment, BoxType, ExtBoxType};
|
||||||
|
|
||||||
|
const API_URL: &str = "https://www.qobuz.com/api.json/0.2/";
|
||||||
|
|
||||||
|
const APP_ID: u32 = 950096963;
|
||||||
|
|
||||||
|
const USER_AGENT: &str = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36";
|
||||||
|
|
||||||
|
#[derive(Clone, ArgEnum, Debug)]
|
||||||
|
pub enum Method {
|
||||||
|
GET,
|
||||||
|
POST
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct APIClient {
|
||||||
|
client: Client,
|
||||||
|
auth_token: Option<String>,
|
||||||
|
session: Session
|
||||||
|
}
|
||||||
|
|
||||||
|
impl APIClient {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let client = Client::builder()
|
||||||
|
.user_agent(USER_AGENT)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
client,
|
||||||
|
auth_token: None,
|
||||||
|
session: Session {
|
||||||
|
id: String::new(),
|
||||||
|
key: [0u8; 16],
|
||||||
|
expiry: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_auth_token(&mut self, auth_token: &str) {
|
||||||
|
self.auth_token = Some(auth_token.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn api_call<T, R>(&self, object: &str, method: &str, params: &T, req_method: Method, signed: bool) -> Result<R, reqwest::Error>
|
||||||
|
where T: Serialize + ?Sized,
|
||||||
|
R: DeserializeOwned {
|
||||||
|
let url = format!("{API_URL}{object}/{method}");
|
||||||
|
let reqwest_method = match req_method {
|
||||||
|
Method::GET => reqwest::Method::GET,
|
||||||
|
Method::POST => reqwest::Method::POST
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut req = self.client.request(reqwest_method, url)
|
||||||
|
.header("x-app-id", APP_ID);
|
||||||
|
|
||||||
|
if let Some(token) = &self.auth_token {
|
||||||
|
req = req.header("x-user-auth-token", token);
|
||||||
|
}
|
||||||
|
|
||||||
|
match req_method {
|
||||||
|
Method::GET => {
|
||||||
|
if signed {
|
||||||
|
req = req.query(&gen_signed_req(object, method, params));
|
||||||
|
}
|
||||||
|
req = req.query(params);
|
||||||
|
},
|
||||||
|
Method::POST => {
|
||||||
|
let mut form_data = String::new();
|
||||||
|
if signed {
|
||||||
|
form_data.push_str(&serde_urlencoded::to_string(&gen_signed_req(object, method, params)).unwrap());
|
||||||
|
}
|
||||||
|
let params_form = serde_urlencoded::to_string(params).unwrap();
|
||||||
|
if !params_form.is_empty() {
|
||||||
|
form_data.push('&');
|
||||||
|
form_data.push_str(¶ms_form);
|
||||||
|
}
|
||||||
|
req = req.body(form_data)
|
||||||
|
.header("content-type", "application/x-www-form-urlencoded");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if object == "file" && method == "url" {
|
||||||
|
req = req.header("x-session-id", &self.session.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let resp = req.send().await?
|
||||||
|
.error_for_status()?;
|
||||||
|
|
||||||
|
resp.json().await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn renew_session(&mut self) {
|
||||||
|
let timestamp = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_secs();
|
||||||
|
|
||||||
|
if timestamp >= self.session.expiry {
|
||||||
|
let resp: SessionResp = self.api_call("session", "start", &json!({"profile":"qbz-1"}), Method::POST, true).await.unwrap();
|
||||||
|
|
||||||
|
self.session.id = resp.session_id;
|
||||||
|
get_session_key(&resp.infos, &mut self.session.key);
|
||||||
|
self.session.expiry = resp.expires_at;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_track_url(&mut self, track_id: u32, format_id: u32) -> Result<TrackUrl, reqwest::Error> {
|
||||||
|
self.renew_session().await;
|
||||||
|
|
||||||
|
let params = json!(
|
||||||
|
{
|
||||||
|
"track_id": track_id,
|
||||||
|
"format_id": format_id,
|
||||||
|
"intent": "stream"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let resp: TrackUrlResp = self.api_call("file", "url", ¶ms, Method::GET, true).await?;
|
||||||
|
let mut key = [0u8; 16];
|
||||||
|
get_track_key(&resp.key, &self.session.key, &mut key);
|
||||||
|
|
||||||
|
Ok(TrackUrl {
|
||||||
|
url_template: resp.url_template,
|
||||||
|
key,
|
||||||
|
n_segments: resp.n_segments
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn dl_track<T>(&self, url_template: &str, key: &[u8; 16], n_segments: u64, out: &mut T) -> Result<(), reqwest::Error>
|
||||||
|
where T: Write {
|
||||||
|
for seg in 0..n_segments + 1 {
|
||||||
|
let resp = self.client.get(url_template.replace("$SEGMENT$", &seg.to_string()))
|
||||||
|
.send().await?
|
||||||
|
.error_for_status()?;
|
||||||
|
|
||||||
|
let block = resp.bytes().await?;
|
||||||
|
|
||||||
|
let boxes = parse_segment(&block);
|
||||||
|
println!("segment {}", seg);
|
||||||
|
//println!("{:#?}", boxes);
|
||||||
|
for atom in &boxes {
|
||||||
|
println!("box size {}", atom.box_data.len());
|
||||||
|
if let BoxType::ExtendedBox {ext_box_type, ..} = &atom.box_type {
|
||||||
|
match ext_box_type {
|
||||||
|
ExtBoxType::Init { initial_data, .. } => {
|
||||||
|
out.write_all(initial_data).unwrap();
|
||||||
|
},
|
||||||
|
ExtBoxType::Segment {frames, ..} => {
|
||||||
|
for frame in frames {
|
||||||
|
if frame.flags != 0 {
|
||||||
|
let frame_data = decrypt_frame(&frame.frame_data, &key, &frame.iv_raw);
|
||||||
|
out.write_all(&frame_data).unwrap();
|
||||||
|
} else {
|
||||||
|
out.write_all(frame.frame_data).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*for b in boxes {
|
||||||
|
out.write_all(b.box_data).unwrap();
|
||||||
|
}*/
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct TrackUrlResp {
|
||||||
|
url_template: String,
|
||||||
|
key: String,
|
||||||
|
n_segments: u64
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TrackUrl {
|
||||||
|
pub url_template: String,
|
||||||
|
pub key: [u8; 16],
|
||||||
|
pub n_segments: u64
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct SessionResp {
|
||||||
|
session_id: String,
|
||||||
|
infos: String,
|
||||||
|
expires_at: u64
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Session {
|
||||||
|
id: String,
|
||||||
|
key: [u8; 16],
|
||||||
|
expiry: u64
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
use serde::{ser, Serialize};
|
||||||
|
use md5::{Md5, Digest};
|
||||||
|
use aes::cipher::{KeyIvInit, BlockDecryptMut, block_padding::NoPadding, StreamCipher};
|
||||||
|
use sha2::Sha256;
|
||||||
|
use hkdf::Hkdf;
|
||||||
|
|
||||||
|
const APP_SECRET: &str = "979549437fcc4a3faad4867b5cd25dcb";
|
||||||
|
|
||||||
|
type Aes128CbcDec = cbc::Decryptor<aes::Aes128>;
|
||||||
|
type Aes128Ctr64BE = ctr::Ctr64BE<aes::Aes128>;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct SignedReq {
|
||||||
|
pub request_ts: u64,
|
||||||
|
pub request_sig: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gen_signed_req<T>(object: &str, method: &str, params: &T) -> SignedReq
|
||||||
|
where T: ser::Serialize + ?Sized {
|
||||||
|
let timestamp = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_secs();
|
||||||
|
|
||||||
|
let mut signature = format!("{object}{method}");
|
||||||
|
|
||||||
|
let params = serde_urlencoded::to_string(params).unwrap();
|
||||||
|
let params: BTreeMap<String, String> = serde_urlencoded::from_str(¶ms).unwrap();
|
||||||
|
for (key, val) in params {
|
||||||
|
signature.push_str(&key);
|
||||||
|
signature.push_str(&val);
|
||||||
|
};
|
||||||
|
|
||||||
|
signature.push_str(×tamp.to_string());
|
||||||
|
signature.push_str(APP_SECRET);
|
||||||
|
|
||||||
|
let mut hasher = Md5::new();
|
||||||
|
hasher.update(signature);
|
||||||
|
let signature = hex::encode(hasher.finalize());
|
||||||
|
|
||||||
|
SignedReq {request_ts: timestamp, request_sig: signature}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_session_key(infos: &str, out: &mut [u8; 16]) {
|
||||||
|
let mut infos = infos.split('.');
|
||||||
|
let salt = infos.next().unwrap();
|
||||||
|
let info = infos.next().unwrap();
|
||||||
|
|
||||||
|
let salt = base64::decode_config(salt, base64::URL_SAFE_NO_PAD).unwrap();
|
||||||
|
let info = base64::decode_config(info, base64::URL_SAFE_NO_PAD).unwrap();
|
||||||
|
|
||||||
|
let hk = Hkdf::<Sha256>::new(Some(&salt), &hex::decode(APP_SECRET).unwrap());
|
||||||
|
hk.expand(&info, out).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_track_key(key_str: &str, session_key: &[u8; 16], out: &mut [u8; 16]) {
|
||||||
|
let mut key = key_str.split('.');
|
||||||
|
key.next();
|
||||||
|
|
||||||
|
let mut trk_key = [0u8; 32];
|
||||||
|
let mut iv = [0u8; 16];
|
||||||
|
|
||||||
|
base64::decode_config_slice(key.next().unwrap(), base64::URL_SAFE_NO_PAD, &mut trk_key).unwrap();
|
||||||
|
base64::decode_config_slice(key.next().unwrap(), base64::URL_SAFE_NO_PAD, &mut iv).unwrap();
|
||||||
|
|
||||||
|
Aes128CbcDec::new(session_key.into(), &iv.into())
|
||||||
|
.decrypt_padded_b2b_mut::<NoPadding>(&trk_key[..16], out)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decrypt_frame(frame: &[u8], key: &[u8; 16], iv_raw: &[u8]) -> Vec<u8> {
|
||||||
|
let mut out = vec![0u8; frame.len()];
|
||||||
|
|
||||||
|
let mut iv = [0u8; 16];
|
||||||
|
iv[..iv_raw.len()].copy_from_slice(iv_raw);
|
||||||
|
|
||||||
|
Aes128Ctr64BE::new(key.into(), &iv.into())
|
||||||
|
.apply_keystream_b2b(frame, &mut out)
|
||||||
|
.unwrap();
|
||||||
|
out
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
use std::fs::File;
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
mod api;
|
||||||
|
use api::APIClient;
|
||||||
|
|
||||||
|
mod crypto;
|
||||||
|
mod mp4;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
struct Args {
|
||||||
|
auth_token: String,
|
||||||
|
track_id: u32,
|
||||||
|
format_id: u32
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
let mut client = APIClient::new();
|
||||||
|
client.set_auth_token(&args.auth_token);
|
||||||
|
|
||||||
|
let url_resp = client.get_track_url(args.track_id, args.format_id).await.unwrap();
|
||||||
|
|
||||||
|
println!("{}", url_resp.url_template);
|
||||||
|
println!("{}", hex::encode(url_resp.key));
|
||||||
|
println!("{}", url_resp.n_segments);
|
||||||
|
|
||||||
|
let track_id = args.track_id;
|
||||||
|
let format_id = args.format_id;
|
||||||
|
|
||||||
|
let path = format!("{track_id}-{format_id}");
|
||||||
|
let mut file = File::create(path).unwrap();
|
||||||
|
|
||||||
|
client.dl_track(&url_resp.url_template, &url_resp.key, url_resp.n_segments, &mut file).await.unwrap();
|
||||||
|
}
|
|
@ -0,0 +1,188 @@
|
||||||
|
use std::io::{Cursor, Read};
|
||||||
|
use byteorder::{BigEndian, ReadBytesExt};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Box<'a> {
|
||||||
|
pub box_data: &'a [u8],
|
||||||
|
pub box_type: BoxType<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum BoxType<'a> {
|
||||||
|
Box { box_type: String },
|
||||||
|
ExtendedBox { ext_box_type: ExtBoxType<'a>, version: u8, flags: u32 },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ExtBoxType<'a> {
|
||||||
|
Init {
|
||||||
|
track_id: u32,
|
||||||
|
file_id: u32,
|
||||||
|
sample_rate: u32,
|
||||||
|
bits_per_sample: u8,
|
||||||
|
channels_count: u8,
|
||||||
|
samples_count: u64,
|
||||||
|
initial_data_size: u16,
|
||||||
|
initial_data: &'a [u8],
|
||||||
|
key_id_size: u8,
|
||||||
|
key_id: &'a [u8],
|
||||||
|
segments_count: u16,
|
||||||
|
segment_infos: Vec<SegmentInfo>
|
||||||
|
},
|
||||||
|
Segment {
|
||||||
|
base_offset: u32,
|
||||||
|
iv_size: u8,
|
||||||
|
n_frames: u32,
|
||||||
|
frames: Vec<Frame<'a>>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SegmentInfo {
|
||||||
|
pub length: u32,
|
||||||
|
pub samples_count: u32
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Frame<'a> {
|
||||||
|
pub length: u32,
|
||||||
|
pub samples_count: u16,
|
||||||
|
pub flags: u16,
|
||||||
|
pub iv_raw: &'a [u8],
|
||||||
|
pub frame_data: &'a [u8]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_segment(buf: &[u8]) -> Vec<Box> {
|
||||||
|
let mut reader = Cursor::new(buf);
|
||||||
|
let mut boxes: Vec<Box> = Vec::new();
|
||||||
|
|
||||||
|
while reader.position() < buf.len() as u64 {
|
||||||
|
let size = reader.read_u32::<BigEndian>().unwrap();
|
||||||
|
|
||||||
|
let box_start = reader.position() - 4;
|
||||||
|
let box_end = box_start + size as u64;
|
||||||
|
|
||||||
|
let box_data = &buf[reader.position() as usize..box_end as usize];
|
||||||
|
let mut box_reader = Cursor::new(box_data);
|
||||||
|
|
||||||
|
let mut box_type = [0u8; 4];
|
||||||
|
box_reader.read_exact(&mut box_type).unwrap();
|
||||||
|
let box_type = String::from_utf8(box_type.to_vec()).unwrap();
|
||||||
|
|
||||||
|
let box_type_enum;
|
||||||
|
|
||||||
|
if box_type == "uuid" {
|
||||||
|
let uuid = box_reader.read_u128::<BigEndian>().unwrap();
|
||||||
|
let uuid = format!("{:016x}", uuid);
|
||||||
|
let version = box_reader.read_u8().unwrap();
|
||||||
|
let flags = box_reader.read_u24::<BigEndian>().unwrap();
|
||||||
|
|
||||||
|
let ext_box_data = &box_data[box_reader.position() as usize..];
|
||||||
|
|
||||||
|
let ext_box_type = match uuid.as_str() {
|
||||||
|
"c7c75df0fdd951e98fc22971e4acf8d2" => parse_init_box(ext_box_data),
|
||||||
|
"3b42129256f35f75923663b69a1f52b2" => parse_segment_box(ext_box_data, box_start, buf),
|
||||||
|
_ => panic!("Unknown extended box type: {}", uuid)
|
||||||
|
};
|
||||||
|
|
||||||
|
box_type_enum = BoxType::ExtendedBox {
|
||||||
|
ext_box_type,
|
||||||
|
version,
|
||||||
|
flags
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
box_type_enum = BoxType::Box {
|
||||||
|
box_type
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
boxes.push(Box {
|
||||||
|
box_data,
|
||||||
|
box_type: box_type_enum
|
||||||
|
});
|
||||||
|
|
||||||
|
reader.set_position(box_end);
|
||||||
|
}
|
||||||
|
|
||||||
|
boxes
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_init_box<'a>(buf: &'a [u8]) -> ExtBoxType<'a> {
|
||||||
|
let mut reader = Cursor::new(buf);
|
||||||
|
|
||||||
|
let track_id = reader.read_u32::<BigEndian>().unwrap();
|
||||||
|
let file_id = reader.read_u32::<BigEndian>().unwrap();
|
||||||
|
let sample_rate = reader.read_u32::<BigEndian>().unwrap();
|
||||||
|
let bits_per_sample = reader.read_u8().unwrap();
|
||||||
|
let channels_count = reader.read_u8().unwrap();
|
||||||
|
let samples_count = reader.read_u64::<BigEndian>().unwrap();
|
||||||
|
let initial_data_size = reader.read_u16::<BigEndian>().unwrap();
|
||||||
|
let initial_data = &buf[reader.position() as usize..reader.position() as usize + initial_data_size as usize];
|
||||||
|
reader.set_position(reader.position() + initial_data_size as u64);
|
||||||
|
let key_id_size = reader.read_u8().unwrap();
|
||||||
|
let key_id = &buf[reader.position() as usize..reader.position() as usize + key_id_size as usize];
|
||||||
|
reader.set_position(reader.position() + key_id_size as u64);
|
||||||
|
let segments_count = reader.read_u16::<BigEndian>().unwrap();
|
||||||
|
|
||||||
|
let mut segment_infos: Vec<SegmentInfo> = Vec::new();
|
||||||
|
for _ in 0..segments_count {
|
||||||
|
segment_infos.push(SegmentInfo {
|
||||||
|
length: reader.read_u32::<BigEndian>().unwrap(),
|
||||||
|
samples_count: reader.read_u32::<BigEndian>().unwrap()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ExtBoxType::Init {
|
||||||
|
track_id,
|
||||||
|
file_id,
|
||||||
|
sample_rate,
|
||||||
|
bits_per_sample,
|
||||||
|
channels_count,
|
||||||
|
samples_count,
|
||||||
|
initial_data_size,
|
||||||
|
initial_data,
|
||||||
|
key_id_size,
|
||||||
|
key_id,
|
||||||
|
segments_count,
|
||||||
|
segment_infos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_segment_box<'a>(buf: &'a [u8], box_start: u64, seg_buf: &'a [u8]) -> ExtBoxType<'a> {
|
||||||
|
let mut reader = Cursor::new(buf);
|
||||||
|
|
||||||
|
let base_offset = reader.read_u32::<BigEndian>().unwrap();
|
||||||
|
let iv_size = reader.read_u8().unwrap();
|
||||||
|
let n_frames = reader.read_u24::<BigEndian>().unwrap();
|
||||||
|
|
||||||
|
let mut frames: Vec<Frame<'a>> = Vec::new();
|
||||||
|
let mut frame_start = box_start as usize + base_offset as usize;
|
||||||
|
for _ in 0..n_frames {
|
||||||
|
let length = reader.read_u32::<BigEndian>().unwrap();
|
||||||
|
let samples_count = reader.read_u16::<BigEndian>().unwrap();
|
||||||
|
let flags = reader.read_u16::<BigEndian>().unwrap();
|
||||||
|
let iv_raw = &buf[reader.position() as usize..reader.position() as usize + iv_size as usize];
|
||||||
|
reader.set_position(reader.position() + iv_size as u64);
|
||||||
|
|
||||||
|
let frame_end = frame_start + length as usize;
|
||||||
|
|
||||||
|
let frame_data = &seg_buf[frame_start..frame_end];
|
||||||
|
|
||||||
|
frames.push(Frame {
|
||||||
|
length,
|
||||||
|
samples_count,
|
||||||
|
flags,
|
||||||
|
iv_raw,
|
||||||
|
frame_data
|
||||||
|
});
|
||||||
|
|
||||||
|
frame_start = frame_end;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExtBoxType::Segment {
|
||||||
|
base_offset,
|
||||||
|
iv_size,
|
||||||
|
n_frames,
|
||||||
|
frames
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue