use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use core::marker::PhantomData;
use broadcast_common::{Serialize, Unpackage};
use mpeg_pes::{PesAssembler, PesPacket};
use mpeg_ts::ts::{SectionReassembler, TsPacket, TS_PACKET_SIZE};
use crate::aac_asc::{parse_adts_header, AudioSpecificConfig};
use crate::ac3::{Ac3SyncframeInfo, Ec3SyncframeInfo};
use crate::annexb::iter_annexb_nals;
use crate::avc_config::{AVCConfigurationBox, AVCDecoderConfigurationRecord};
use crate::error::{Error, Result};
use crate::media::{Media, Track};
use crate::mp4esds::{
DecoderConfigDescriptor, DecoderSpecificInfo, ESDescriptor, EsdsBox, ObjectTypeIndication,
SLConfigDescriptor, StreamType as EsdsStreamType,
};
use crate::mpeg_legacy::MpegAudioFrameHeader;
use crate::nalu_types::{AvcPps, AvcSps};
use crate::pipeline::{CodecConfig, Sample, TrackSpec};
const PAT_PID: u16 = 0x0000;
const TABLE_ID_PAT: u8 = 0x00;
const TABLE_ID_PMT: u8 = 0x02;
const SECTION_HEADER_LEN: usize = 8;
const CRC32_LEN: usize = 4;
const SECTION_LENGTH_HI_MASK: u8 = 0x0F;
const PID_HI_MASK: u8 = 0x1F;
const PAT_ENTRY_LEN: usize = 4;
const INFO_LENGTH_HI_MASK: u8 = 0x0F;
const NETWORK_PROGRAM_NUMBER: u16 = 0x0000;
const STREAM_TYPE_MPEG2_VIDEO: u8 = 0x02;
const STREAM_TYPE_MPEG1_AUDIO: u8 = 0x03;
const STREAM_TYPE_MPEG2_AUDIO: u8 = 0x04;
const STREAM_TYPE_AVC: u8 = 0x1B;
const STREAM_TYPE_HEVC: u8 = 0x24;
const STREAM_TYPE_AAC_ADTS: u8 = 0x0F;
const STREAM_TYPE_AC3: u8 = 0x81;
const STREAM_TYPE_EAC3: u8 = 0x87;
const STREAM_TYPE_DTS_82: u8 = 0x82;
const STREAM_TYPE_DTS_85: u8 = 0x85;
const STREAM_TYPE_DTS_8A: u8 = 0x8A;
const NAL_LENGTH_SIZE_MINUS_ONE: u8 = 3;
const H264_NAL_SPS: u8 = 7;
const H264_NAL_PPS: u8 = 8;
const H264_NAL_IDR: u8 = 5;
const H264_NAL_TYPE_MASK: u8 = 0x1F;
const OTI_MPEG4_AUDIO: u8 = 0x40;
const OTI_MPEG2_VIDEO_MAIN: u8 = 0x61;
const OTI_MPEG1_AUDIO: u8 = 0x6B;
const OTI_MPEG2_AUDIO: u8 = 0x69;
const STREAM_TYPE_AUDIO: u8 = 0x05;
const STREAM_TYPE_VISUAL: u8 = 0x04;
const ESDS_ES_ID: u16 = 1;
const SL_CONFIG_PREDEFINED_MP4: u8 = 0x02;
const AUDIO_SAMPLE_SIZE_BITS: u16 = 16;
const VIDEO_TIMESCALE: u32 = 90_000;
const AAC_SAMPLES_PER_FRAME: u32 = 1024;
const ADTS_HEADER_SIZE: usize = 7;
const TS_WRAP: u64 = 1 << 33;
const TS_WRAP_HALF: u64 = TS_WRAP / 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Codec {
H264,
Hevc,
Mpeg2Video,
MpegAudio(bool),
Aac,
Ac3,
Eac3,
Dts,
}
impl Codec {
fn from_stream_type(stream_type: u8) -> Option<Self> {
match stream_type {
STREAM_TYPE_MPEG2_VIDEO => Some(Codec::Mpeg2Video),
STREAM_TYPE_MPEG1_AUDIO => Some(Codec::MpegAudio(false)),
STREAM_TYPE_MPEG2_AUDIO => Some(Codec::MpegAudio(true)),
STREAM_TYPE_AVC => Some(Codec::H264),
STREAM_TYPE_HEVC => Some(Codec::Hevc),
STREAM_TYPE_AAC_ADTS => Some(Codec::Aac),
STREAM_TYPE_AC3 => Some(Codec::Ac3),
STREAM_TYPE_EAC3 => Some(Codec::Eac3),
STREAM_TYPE_DTS_82 | STREAM_TYPE_DTS_85 | STREAM_TYPE_DTS_8A => Some(Codec::Dts),
_ => None,
}
}
}
struct ElementaryStream {
codec: Codec,
assembler: PesAssembler,
access_units: Vec<AccessUnit>,
}
struct AccessUnit {
data: Vec<u8>,
pts: u64,
dts: u64,
}
#[derive(Debug, Default, Clone)]
pub struct TsDemux<'a> {
_marker: PhantomData<&'a [u8]>,
}
impl<'a> TsDemux<'a> {
pub fn new() -> Self {
Self {
_marker: PhantomData,
}
}
pub fn demux(&mut self, input: &'a [u8]) -> Result<Media> {
let mut pat_reasm = SectionReassembler::default();
let mut pmt_reasm: BTreeMap<u16, SectionReassembler> = BTreeMap::new();
let mut pmt_pids: Vec<u16> = Vec::new();
let mut es_defs: Vec<(u16, Codec)> = Vec::new();
let mut es_seen: alloc::collections::BTreeSet<u16> = alloc::collections::BTreeSet::new();
for pkt in iter_ts_packets(input) {
let pid = pkt.header.pid;
let pusi = pkt.header.pusi;
let Some(payload) = pkt.payload else {
continue;
};
if pid == PAT_PID {
pat_reasm.feed(payload, pusi);
while let Some(section) = pat_reasm.pop_section() {
for pmt_pid in parse_pat(§ion)? {
if !pmt_pids.contains(&pmt_pid) {
pmt_pids.push(pmt_pid);
pmt_reasm.entry(pmt_pid).or_default();
}
}
}
} else if let Some(reasm) = pmt_reasm.get_mut(&pid) {
reasm.feed(payload, pusi);
while let Some(section) = reasm.pop_section() {
for (es_pid, codec) in parse_pmt(§ion)? {
if es_seen.insert(es_pid) {
es_defs.push((es_pid, codec));
}
}
}
}
}
let mut streams: BTreeMap<u16, ElementaryStream> = BTreeMap::new();
for &(pid, codec) in &es_defs {
streams.entry(pid).or_insert_with(|| ElementaryStream {
codec,
assembler: PesAssembler::new(),
access_units: Vec::new(),
});
}
for pkt in iter_ts_packets(input) {
let pid = pkt.header.pid;
let pusi = pkt.header.pusi;
let Some(payload) = pkt.payload else {
continue;
};
if let Some(es) = streams.get_mut(&pid) {
if let Some(completed) = es.assembler.feed(pusi, payload) {
push_access_unit(es, &completed);
}
}
}
for es in streams.values_mut() {
if let Some(completed) = es.assembler.flush() {
push_access_unit(es, &completed);
}
}
let mut tracks: Vec<Track> = Vec::new();
let mut track_id: u32 = 1;
for &(pid, _) in &es_defs {
let Some(es) = streams.get(&pid) else {
continue;
};
if es.access_units.is_empty() {
continue;
}
if let Some(track) = build_track(es, track_id) {
tracks.push(track);
track_id += 1;
}
}
Ok(Media::new(tracks, VIDEO_TIMESCALE))
}
}
impl<'a> Unpackage for TsDemux<'a> {
type Input = &'a [u8];
type Media = Media;
type Error = Error;
fn unpackage(&mut self, input: &'a [u8]) -> Result<Media> {
self.demux(input)
}
}
fn iter_ts_packets(data: &[u8]) -> impl Iterator<Item = TsPacket<'_>> {
data.chunks_exact(TS_PACKET_SIZE)
.filter_map(|chunk| TsPacket::parse(chunk).ok())
}
fn parse_pat(section: &[u8]) -> Result<Vec<u16>> {
if section.first().copied() != Some(TABLE_ID_PAT) {
return Ok(Vec::new());
}
let body = section_body(section, "PAT")?;
let mut pids = Vec::new();
let mut off = 0usize;
while off + PAT_ENTRY_LEN <= body.len() {
let program_number = u16::from_be_bytes([body[off], body[off + 1]]);
let pid = (((body[off + 2] & PID_HI_MASK) as u16) << 8) | body[off + 3] as u16;
if program_number != NETWORK_PROGRAM_NUMBER {
pids.push(pid);
}
off += PAT_ENTRY_LEN;
}
Ok(pids)
}
fn parse_pmt(section: &[u8]) -> Result<Vec<(u16, Codec)>> {
if section.first().copied() != Some(TABLE_ID_PMT) {
return Ok(Vec::new());
}
let body = section_body(section, "PMT")?;
if body.len() < 4 {
return Err(Error::BufferTooShort {
need: 4,
have: body.len(),
what: "PMT program-info prefix",
});
}
let program_info_length = (((body[2] & INFO_LENGTH_HI_MASK) as usize) << 8) | body[3] as usize;
let mut off = 4 + program_info_length;
let mut out = Vec::new();
while off + 5 <= body.len() {
let stream_type = body[off];
let es_pid = (((body[off + 1] & PID_HI_MASK) as u16) << 8) | body[off + 2] as u16;
let es_info_length =
(((body[off + 3] & INFO_LENGTH_HI_MASK) as usize) << 8) | body[off + 4] as usize;
if let Some(codec) = Codec::from_stream_type(stream_type) {
out.push((es_pid, codec));
}
off += 5 + es_info_length;
}
Ok(out)
}
fn section_body<'a>(section: &'a [u8], what: &'static str) -> Result<&'a [u8]> {
if section.len() < SECTION_HEADER_LEN + CRC32_LEN {
return Err(Error::BufferTooShort {
need: SECTION_HEADER_LEN + CRC32_LEN,
have: section.len(),
what,
});
}
let section_length =
(((section[1] & SECTION_LENGTH_HI_MASK) as usize) << 8) | section[2] as usize;
let total = 3 + section_length;
let end = total.min(section.len());
if end < SECTION_HEADER_LEN + CRC32_LEN {
return Err(Error::BufferTooShort {
need: SECTION_HEADER_LEN + CRC32_LEN,
have: end,
what,
});
}
Ok(§ion[SECTION_HEADER_LEN..end - CRC32_LEN])
}
fn push_access_unit(es: &mut ElementaryStream, pes_bytes: &[u8]) {
let Ok(pes) = PesPacket::parse(pes_bytes) else {
return;
};
if pes.payload.is_empty() {
return;
}
let (pts, dts) = pes
.header
.as_ref()
.map(|h| {
let pts = h.pts.map(|p| p.0);
let dts = h.dts.map(|d| d.0);
let pts = pts.or(dts).unwrap_or(0);
let dts = dts.unwrap_or(pts);
(pts, dts)
})
.unwrap_or((0, 0));
es.access_units.push(AccessUnit {
data: pes.payload.to_vec(),
pts,
dts,
});
}
fn build_track(es: &ElementaryStream, track_id: u32) -> Option<Track> {
match es.codec {
Codec::H264 => build_h264_track(es, track_id),
Codec::Mpeg2Video => build_mpeg2_video_track(es, track_id),
Codec::MpegAudio(is_mpeg2) => build_mpeg_audio_track(es, track_id, is_mpeg2),
Codec::Aac => build_aac_track(es, track_id),
Codec::Ac3 => build_ac3_track(es, track_id),
Codec::Eac3 => build_eac3_track(es, track_id),
Codec::Hevc | Codec::Dts => None,
}
}
fn unwrap_ts(prev_unwrapped: i128, prev_raw: u64, raw: u64) -> i128 {
let mut delta = raw as i128 - prev_raw as i128;
if delta > TS_WRAP_HALF as i128 {
delta -= TS_WRAP as i128; } else if delta < -(TS_WRAP_HALF as i128) {
delta += TS_WRAP as i128; }
prev_unwrapped + delta
}
fn decode_order(units: &[AccessUnit]) -> Vec<(usize, i128, i128)> {
if units.is_empty() {
return Vec::new();
}
let mut unwrapped: Vec<(i128, i128)> = Vec::with_capacity(units.len());
let (mut prev_dts_raw, mut prev_dts_uw) = (units[0].dts, units[0].dts as i128);
let (mut prev_pts_raw, mut prev_pts_uw) = (units[0].pts, units[0].pts as i128);
for (i, au) in units.iter().enumerate() {
let (dts_uw, pts_uw) = if i == 0 {
(units[0].dts as i128, units[0].pts as i128)
} else {
(
unwrap_ts(prev_dts_uw, prev_dts_raw, au.dts),
unwrap_ts(prev_pts_uw, prev_pts_raw, au.pts),
)
};
prev_dts_raw = au.dts;
prev_dts_uw = dts_uw;
prev_pts_raw = au.pts;
prev_pts_uw = pts_uw;
unwrapped.push((pts_uw, dts_uw));
}
let mut order: Vec<usize> = (0..units.len()).collect();
order.sort_by_key(|&i| unwrapped[i].1); order
.into_iter()
.map(|i| (i, unwrapped[i].0, unwrapped[i].1))
.collect()
}
fn durations_from_dts(ordered: &[(usize, i128, i128)]) -> Vec<u32> {
let n = ordered.len();
let mut durs = alloc::vec![0u32; n];
for i in 0..n {
let dur = if i + 1 < n {
(ordered[i + 1].2 - ordered[i].2).max(0) as u64
} else if i > 0 {
durs[i - 1] as u64
} else {
0
};
durs[i] = dur as u32;
}
durs
}
fn build_h264_track(es: &ElementaryStream, track_id: u32) -> Option<Track> {
let mut sps: Option<Vec<u8>> = None;
let mut pps: Option<Vec<u8>> = None;
let mut is_idr: Vec<bool> = Vec::with_capacity(es.access_units.len());
for au in &es.access_units {
let mut idr = false;
for nal in iter_annexb_nals(&au.data) {
match nal[0] & H264_NAL_TYPE_MASK {
H264_NAL_SPS if sps.is_none() => sps = Some(nal.to_vec()),
H264_NAL_PPS if pps.is_none() => pps = Some(nal.to_vec()),
H264_NAL_IDR => idr = true,
_ => {}
}
}
is_idr.push(idr);
}
let sps = sps?;
let pps = pps?;
if sps.len() < 4 {
return None;
}
let (width, height) = crate::sps::decode_avc_sps(&sps)
.map(|i| (i.width as u16, i.height as u16))
.unwrap_or((0, 0));
let record = AVCDecoderConfigurationRecord {
configuration_version: 1,
profile_indication: sps[1],
profile_compatibility: sps[2],
level_indication: sps[3],
length_size_minus_one: NAL_LENGTH_SIZE_MINUS_ONE,
sps: alloc::vec![AvcSps(sps)],
pps: alloc::vec![AvcPps(pps)],
chroma_format: None,
bit_depth_luma_minus8: None,
bit_depth_chroma_minus8: None,
sps_ext: alloc::vec![],
};
let config = AVCConfigurationBox::new(record);
let ordered = decode_order(&es.access_units);
let durations = durations_from_dts(&ordered);
let samples: Vec<Sample> = ordered
.iter()
.enumerate()
.map(|(pos, &(i, pts, dts))| {
let composition_offset = (pts - dts) as i32;
Sample::from_annexb(
&es.access_units[i].data,
durations[pos],
is_idr[i],
composition_offset,
)
})
.collect();
Some(Track::new(
TrackSpec {
track_id,
timescale: VIDEO_TIMESCALE,
config: CodecConfig::Avc {
config,
width,
height,
},
},
samples,
))
}
const MPEG2_PICTURE_START_CODE: u8 = 0x00;
const MPEG2_PICTURE_CODING_TYPE_I: u8 = 0x01;
const ESDS_VIDEO_ES_ID: u16 = 2;
fn mpeg2_is_sync(au: &[u8]) -> bool {
let mut i = 0usize;
while i + 4 <= au.len() {
if au[i] == 0x00 && au[i + 1] == 0x00 && au[i + 2] == 0x01 {
let code = au[i + 3];
if code == crate::mpeg_legacy::SEQUENCE_HEADER_CODE[3] {
return true;
}
if code == MPEG2_PICTURE_START_CODE && i + 6 <= au.len() {
let pct = (au[i + 5] >> 3) & 0x07;
return pct == MPEG2_PICTURE_CODING_TYPE_I;
}
}
i += 1;
}
false
}
fn build_mpeg2_video_track(es: &ElementaryStream, track_id: u32) -> Option<Track> {
let seq = es
.access_units
.iter()
.find_map(|au| crate::mpeg_legacy::Mpeg2SeqHeader::find(&au.data).ok())?;
let esds = EsdsBox::new(ESDescriptor {
es_id: ESDS_VIDEO_ES_ID,
stream_dependence_flag: false,
url_flag: false,
ocr_stream_flag: false,
stream_priority: 0,
depends_on_es_id: None,
url: None,
ocr_es_id: None,
decoder_config: Some(DecoderConfigDescriptor {
object_type_indication: ObjectTypeIndication(OTI_MPEG2_VIDEO_MAIN),
stream_type: EsdsStreamType(STREAM_TYPE_VISUAL),
up_stream: false,
buffer_size_db: 0,
max_bitrate: 0,
avg_bitrate: 0,
decoder_specific_info: None,
}),
sl_config: Some(SLConfigDescriptor {
body: alloc::vec![SL_CONFIG_PREDEFINED_MP4],
}),
});
let is_sync: Vec<bool> = es
.access_units
.iter()
.map(|au| mpeg2_is_sync(&au.data))
.collect();
let ordered = decode_order(&es.access_units);
let durations = durations_from_dts(&ordered);
let samples: Vec<Sample> = ordered
.iter()
.enumerate()
.map(|(pos, &(i, pts, dts))| Sample {
data: es.access_units[i].data.clone(),
duration: durations[pos],
is_sync: is_sync[i],
composition_offset: (pts - dts) as i32,
})
.collect();
Some(Track::new(
TrackSpec {
track_id,
timescale: VIDEO_TIMESCALE,
config: CodecConfig::Mpeg2Video {
esds,
width: seq.width,
height: seq.height,
},
},
samples,
))
}
fn split_mpeg_audio_frames(payload: &[u8]) -> Vec<&[u8]> {
let mut frames = Vec::new();
let mut off = 0usize;
while off + 4 <= payload.len() {
let Ok(hdr) = MpegAudioFrameHeader::parse(&payload[off..]) else {
break;
};
let flen = hdr.frame_length;
if flen < 4 || off + flen > payload.len() {
break;
}
frames.push(&payload[off..off + flen]);
off += flen;
}
frames
}
fn build_mpeg_audio_track(es: &ElementaryStream, track_id: u32, is_mpeg2: bool) -> Option<Track> {
let first = es
.access_units
.iter()
.find_map(|au| MpegAudioFrameHeader::parse(&au.data).ok())?;
let sample_rate = first.sample_rate;
let channel_count = first.channels;
let samples_per_frame = first.samples_per_frame;
let oti = if is_mpeg2 {
OTI_MPEG2_AUDIO
} else {
OTI_MPEG1_AUDIO
};
let esds = EsdsBox::new(ESDescriptor {
es_id: ESDS_ES_ID,
stream_dependence_flag: false,
url_flag: false,
ocr_stream_flag: false,
stream_priority: 0,
depends_on_es_id: None,
url: None,
ocr_es_id: None,
decoder_config: Some(DecoderConfigDescriptor {
object_type_indication: ObjectTypeIndication(oti),
stream_type: EsdsStreamType(STREAM_TYPE_AUDIO),
up_stream: false,
buffer_size_db: 0,
max_bitrate: 0,
avg_bitrate: 0,
decoder_specific_info: None,
}),
sl_config: Some(SLConfigDescriptor {
body: alloc::vec![SL_CONFIG_PREDEFINED_MP4],
}),
});
let mut samples: Vec<Sample> = Vec::new();
for au in &es.access_units {
for frame in split_mpeg_audio_frames(&au.data) {
samples.push(Sample::from_raw(frame.to_vec(), samples_per_frame));
}
}
if samples.is_empty() {
return None;
}
Some(Track::new(
TrackSpec {
track_id,
timescale: sample_rate,
config: CodecConfig::MpegAudio {
esds,
layer: first.layer,
channel_count,
sample_rate,
sample_size: AUDIO_SAMPLE_SIZE_BITS,
},
},
samples,
))
}
fn split_adts_frames(payload: &[u8]) -> Vec<&[u8]> {
let mut frames = Vec::new();
let mut off = 0usize;
while off + ADTS_HEADER_SIZE <= payload.len() {
let Ok(hdr) = parse_adts_header(&payload[off..]) else {
break;
};
let frame_len = hdr.frame_length as usize;
if frame_len < ADTS_HEADER_SIZE || off + frame_len > payload.len() {
break;
}
frames.push(&payload[off..off + frame_len]);
off += frame_len;
}
frames
}
fn build_aac_track(es: &ElementaryStream, track_id: u32) -> Option<Track> {
let first_hdr = es
.access_units
.iter()
.find_map(|au| parse_adts_header(&au.data).ok())?;
let asc = AudioSpecificConfig::from_adts_header(&first_hdr);
let asc_bytes = asc.to_bytes();
let sample_rate = sfi_to_hz(first_hdr.sampling_frequency_index)?;
let channel_count = first_hdr.channel_configuration as u16;
let esds = EsdsBox::new(ESDescriptor {
es_id: ESDS_ES_ID,
stream_dependence_flag: false,
url_flag: false,
ocr_stream_flag: false,
stream_priority: 0,
depends_on_es_id: None,
url: None,
ocr_es_id: None,
decoder_config: Some(DecoderConfigDescriptor {
object_type_indication: ObjectTypeIndication(OTI_MPEG4_AUDIO),
stream_type: EsdsStreamType(STREAM_TYPE_AUDIO),
up_stream: false,
buffer_size_db: 0,
max_bitrate: 0,
avg_bitrate: 0,
decoder_specific_info: Some(DecoderSpecificInfo { data: asc_bytes }),
}),
sl_config: Some(SLConfigDescriptor {
body: alloc::vec![SL_CONFIG_PREDEFINED_MP4],
}),
});
let mut samples: Vec<Sample> = Vec::new();
for au in &es.access_units {
for frame in split_adts_frames(&au.data) {
if frame.len() > ADTS_HEADER_SIZE {
samples.push(Sample::from_raw(
frame[ADTS_HEADER_SIZE..].to_vec(),
AAC_SAMPLES_PER_FRAME,
));
}
}
}
if samples.is_empty() {
return None;
}
Some(Track::new(
TrackSpec {
track_id,
timescale: sample_rate,
config: CodecConfig::Aac {
esds,
channel_count,
sample_rate,
sample_size: AUDIO_SAMPLE_SIZE_BITS,
},
},
samples,
))
}
fn build_ac3_track(es: &ElementaryStream, track_id: u32) -> Option<Track> {
let info = es
.access_units
.iter()
.find_map(|au| Ac3SyncframeInfo::from_es(&au.data).ok())?;
let sample_rate = info.sample_rate;
let channel_count = info.channel_count() as u16;
let config = info.into_dac3();
let samples: Vec<Sample> = es
.access_units
.iter()
.map(|au| Sample::from_raw(au.data.clone(), 0))
.collect();
Some(Track::new(
TrackSpec {
track_id,
timescale: sample_rate,
config: CodecConfig::Ac3 {
config,
channel_count,
sample_rate,
sample_size: AUDIO_SAMPLE_SIZE_BITS,
},
},
samples,
))
}
fn build_eac3_track(es: &ElementaryStream, track_id: u32) -> Option<Track> {
let info = es
.access_units
.iter()
.find_map(|au| Ec3SyncframeInfo::from_es(&au.data).ok())?;
let sample_rate = info.sample_rate;
let channel_count = info.channel_count() as u16;
let config = info.into_dec3();
let samples: Vec<Sample> = es
.access_units
.iter()
.map(|au| Sample::from_raw(au.data.clone(), 0))
.collect();
Some(Track::new(
TrackSpec {
track_id,
timescale: sample_rate,
config: CodecConfig::Eac3 {
config,
channel_count,
sample_rate,
sample_size: AUDIO_SAMPLE_SIZE_BITS,
},
},
samples,
))
}
fn sfi_to_hz(sfi: u8) -> Option<u32> {
Some(match sfi {
0 => 96000,
1 => 88200,
2 => 64000,
3 => 48000,
4 => 44100,
5 => 32000,
6 => 24000,
7 => 22050,
8 => 16000,
9 => 12000,
10 => 11025,
11 => 8000,
12 => 7350,
_ => return None,
})
}