mpegts.js
Version:
HTML5 MPEG2-TS Stream Player
1,138 lines (994 loc) • 89.6 kB
text/typescript
/*
* Copyright (C) 2021 magicxqq. All Rights Reserved.
*
* @author magicxqq <xqq@xqq.im>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Log from '../utils/logger';
import DemuxErrors from './demux-errors';
import MediaInfo from '../core/media-info';
import {IllegalStateException} from '../utils/exception';
import BaseDemuxer from './base-demuxer';
import { PAT, PESData, SectionData, SliceQueue, PIDToSliceQueues, PMT, ProgramToPMTMap, StreamType } from './pat-pmt-pes';
import { AVCDecoderConfigurationRecord, H264AnnexBParser, H264NaluAVC1, H264NaluPayload, H264NaluType } from './h264';
import SPSParser from './sps-parser';
import { AACADTSParser, AACFrame, AACLOASParser, AudioSpecificConfig, LOASAACFrame } from './aac';
import { MPEG4AudioObjectTypes, MPEG4SamplingFrequencyIndex } from './mpeg4-audio';
import { PESPrivateData, PESPrivateDataDescriptor } from './pes-private-data';
import { readSCTE35, SCTE35Data } from './scte35';
import { H265AnnexBParser, H265NaluHVC1, H265NaluPayload, H265NaluType, HEVCDecoderConfigurationRecord } from './h265';
import H265Parser from './h265-parser';
import { SMPTE2038Data, smpte2038parse } from './smpte2038';
import { MP3Data } from './mp3';
import { AC3Config, AC3Frame, AC3Parser, EAC3Config, EAC3Frame, EAC3Parser } from './ac3';
import { KLVData, klv_parse } from './klv';
import AV1OBUInMpegTsParser from './av1';
import AV1OBUParser from './av1-parser';
type AdaptationFieldInfo = {
discontinuity_indicator?: number;
random_access_indicator?: number;
elementary_stream_priority_indicator?: number;
};
type AACAudioMetadata = {
codec: 'aac',
audio_object_type: MPEG4AudioObjectTypes;
sampling_freq_index: MPEG4SamplingFrequencyIndex;
sampling_frequency: number;
channel_config: number;
};
type AC3AudioMetadata = {
codec: 'ac-3',
sampling_frequency: number;
bit_stream_identification: number;
bit_stream_mode: number;
low_frequency_effects_channel_on: number;
channel_mode: number;
};
type EAC3AudioMetadata = {
codec: 'ec-3',
sampling_frequency: number;
bit_stream_identification: number;
low_frequency_effects_channel_on: number;
channel_mode: number;
num_blks: number;
};
type OpusAudioMetadata = {
codec: 'opus';
channel_count: number;
channel_config_code: number;
sample_rate: number;
}
type MP3AudioMetadata = {
codec: 'mp3',
object_type: number,
sample_rate: number,
channel_count: number;
};
type AudioData = {
codec: 'aac';
data: AACFrame;
} | {
codec: 'ac-3';
data: AC3Frame,
} | {
codec: 'ec-3';
data: EAC3Frame,
} | {
codec: 'opus';
meta: OpusAudioMetadata,
} | {
codec: 'mp3';
data: MP3Data;
}
class TSDemuxer extends BaseDemuxer {
private readonly TAG: string = 'TSDemuxer';
private config_: any;
private ts_packet_size_: number;
private sync_offset_: number;
private first_parse_: boolean = true;
private media_info_ = new MediaInfo();
private timescale_ = 90;
private duration_ = 0;
private pat_: PAT;
private current_program_: number;
private current_pmt_pid_: number = -1;
private pmt_: PMT;
private program_pmt_map_: ProgramToPMTMap = {};
private pes_slice_queues_: PIDToSliceQueues = {};
private section_slice_queues_: PIDToSliceQueues = {};
private video_metadata_: {
vps: H265NaluHVC1 | undefined,
sps: H264NaluAVC1 | H265NaluHVC1 | undefined,
pps: H264NaluAVC1 | H265NaluHVC1 | undefined,
av1c: Uint8Array | undefined,
details: any
} = {
vps: undefined,
sps: undefined,
pps: undefined,
av1c: undefined,
details: undefined
};
private audio_metadata_: AACAudioMetadata | AC3AudioMetadata | EAC3AudioMetadata | OpusAudioMetadata | MP3AudioMetadata = {
codec: undefined,
audio_object_type: undefined,
sampling_freq_index: undefined,
sampling_frequency: undefined,
channel_config: undefined
};
private last_pcr_: number | undefined;
private last_pcr_base_: number = NaN;
private timestamp_offset_: number = 0;
private audio_last_sample_pts_: number = undefined;
private aac_last_incomplete_data_: Uint8Array = null;
private has_video_ = false;
private has_audio_ = false;
private video_init_segment_dispatched_ = false;
private audio_init_segment_dispatched_ = false;
private video_metadata_changed_ = false;
private audio_metadata_changed_ = false;
private loas_previous_frame: LOASAACFrame | null = null;
private video_track_ = {type: 'video', id: 1, sequenceNumber: 0, samples: [], length: 0};
private audio_track_ = {type: 'audio', id: 2, sequenceNumber: 0, samples: [], length: 0};
public constructor(probe_data: any, config: any) {
super();
this.ts_packet_size_ = probe_data.ts_packet_size;
this.sync_offset_ = probe_data.sync_offset;
this.config_ = config;
}
public destroy() {
this.media_info_ = null;
this.pes_slice_queues_ = null;
this.section_slice_queues_ = null;
this.video_metadata_ = null;
this.audio_metadata_ = null;
this.aac_last_incomplete_data_ = null;
this.video_track_ = null;
this.audio_track_ = null;
super.destroy();
}
public static probe(buffer: ArrayBuffer) {
let data = new Uint8Array(buffer);
let sync_offset = -1;
let ts_packet_size = 188;
if (data.byteLength <= 3 * ts_packet_size) {
return {needMoreData: true};
}
while (sync_offset === -1) {
let scan_window = Math.min(1000, data.byteLength - 3 * ts_packet_size);
for (let i = 0; i < scan_window; ) {
// sync_byte should all be 0x47
if (data[i] === 0x47
&& data[i + ts_packet_size] === 0x47
&& data[i + 2 * ts_packet_size] === 0x47) {
sync_offset = i;
break;
} else {
i++;
}
}
// find sync_offset failed in previous ts_packet_size
if (sync_offset === -1) {
if (ts_packet_size === 188) {
// try 192 packet size (BDAV, etc.)
ts_packet_size = 192;
} else if (ts_packet_size === 192) {
// try 204 packet size (European DVB, etc.)
ts_packet_size = 204;
} else {
// 192, 204 also failed, exit
break;
}
}
}
if (sync_offset === -1) {
// both 188, 192, 204 failed, Non MPEG-TS
return {match: false};
}
if (ts_packet_size === 192 && sync_offset >= 4) {
Log.v('TSDemuxer', `ts_packet_size = 192, m2ts mode`);
sync_offset -= 4;
} else if (ts_packet_size === 204) {
Log.v('TSDemuxer', `ts_packet_size = 204, RS encoded MPEG2-TS stream`);
}
return {
match: true,
consumed: 0,
ts_packet_size,
sync_offset
};
}
public bindDataSource(loader) {
loader.onDataArrival = this.parseChunks.bind(this);
return this;
}
public resetMediaInfo() {
this.media_info_ = new MediaInfo();
}
public parseChunks(chunk: ArrayBuffer, byte_start: number): number {
if (!this.onError
|| !this.onMediaInfo
|| !this.onTrackMetadata
|| !this.onDataAvailable) {
throw new IllegalStateException('onError & onMediaInfo & onTrackMetadata & onDataAvailable callback must be specified');
}
let offset = 0;
if (this.first_parse_) {
this.first_parse_ = false;
offset = this.sync_offset_;
}
while (offset + this.ts_packet_size_ <= chunk.byteLength) {
let file_position = byte_start + offset;
if (this.ts_packet_size_ === 192) {
// skip ATS field (2-bits copy-control + 30-bits timestamp) for m2ts
offset += 4;
}
let data = new Uint8Array(chunk, offset, 188);
let sync_byte = data[0];
if (sync_byte !== 0x47) {
Log.e(this.TAG, `sync_byte = ${sync_byte}, not 0x47`);
break;
}
let payload_unit_start_indicator = (data[1] & 0x40) >>> 6;
let transport_priority = (data[1] & 0x20) >>> 5;
let pid = ((data[1] & 0x1F) << 8) | data[2];
let adaptation_field_control = (data[3] & 0x30) >>> 4;
let continuity_conunter = (data[3] & 0x0F);
let is_pcr_pid: boolean = (this.pmt_ && this.pmt_.pcr_pid === pid) ? true : false;
let adaptation_field_info: AdaptationFieldInfo = {};
let ts_payload_start_index = 4;
if (adaptation_field_control == 0x02 || adaptation_field_control == 0x03) {
// Adaptation field exists along with / without payload
let adaptation_field_length = data[4];
if (adaptation_field_length > 0 && (is_pcr_pid || adaptation_field_control == 0x03)) {
// Parse adaptation field
adaptation_field_info.discontinuity_indicator = (data[5] & 0x80) >>> 7;
adaptation_field_info.random_access_indicator = (data[5] & 0x40) >>> 6;
adaptation_field_info.elementary_stream_priority_indicator = (data[5] & 0x20) >>> 5;
let PCR_flag = (data[5] & 0x10) >>> 4;
if (PCR_flag) {
let pcr_base = this.getPcrBase(data);
let pcr_extension = ((data[10] & 0x01) << 8) | data[11];
let pcr = pcr_base * 300 + pcr_extension;
this.last_pcr_ = pcr;
}
}
if (adaptation_field_control == 0x02 || 5 + adaptation_field_length === 188) {
// TS packet only has adaption field, jump to next
offset += 188;
if (this.ts_packet_size_ === 204) {
// skip parity word (16 bytes) for RS encoded TS
offset += 16;
}
continue;
} else {
// Point ts_payload_start_index to the start of payload
ts_payload_start_index = 4 + 1 + adaptation_field_length;
}
}
if (adaptation_field_control == 0x01 || adaptation_field_control == 0x03) {
if (pid === 0 || // PAT (pid === 0)
pid === this.current_pmt_pid_ || // PMT
(this.pmt_ != undefined && this.pmt_.pid_stream_type[pid] === StreamType.kSCTE35)) { // SCTE35
let ts_payload_length = 188 - ts_payload_start_index;
this.handleSectionSlice(chunk,
offset + ts_payload_start_index,
ts_payload_length,
{
pid,
file_position,
payload_unit_start_indicator,
continuity_conunter,
random_access_indicator: adaptation_field_info.random_access_indicator
});
} else if (this.pmt_ != undefined && this.pmt_.pid_stream_type[pid] != undefined) {
// PES
let ts_payload_length = 188 - ts_payload_start_index;
let stream_type = this.pmt_.pid_stream_type[pid];
// process PES only for known common_pids
if (pid === this.pmt_.common_pids.h264
|| pid === this.pmt_.common_pids.h265
|| pid === this.pmt_.common_pids.av1
|| pid === this.pmt_.common_pids.adts_aac
|| pid === this.pmt_.common_pids.loas_aac
|| pid === this.pmt_.common_pids.ac3
|| pid === this.pmt_.common_pids.eac3
|| pid === this.pmt_.common_pids.opus
|| pid === this.pmt_.common_pids.mp3
|| this.pmt_.pes_private_data_pids[pid] === true
|| this.pmt_.timed_id3_pids[pid] === true
|| this.pmt_.synchronous_klv_pids[pid] === true
|| this.pmt_.asynchronous_klv_pids[pid] === true
) {
this.handlePESSlice(chunk,
offset + ts_payload_start_index,
ts_payload_length,
{
pid,
stream_type,
file_position,
payload_unit_start_indicator,
continuity_conunter,
random_access_indicator: adaptation_field_info.random_access_indicator
});
}
}
}
offset += 188;
if (this.ts_packet_size_ === 204) {
// skip parity word (16 bytes) for RS encoded TS
offset += 16;
}
}
// dispatch parsed frames to the remuxer (consumer)
this.dispatchAudioVideoMediaSegment();
return offset; // consumed bytes
}
private handleSectionSlice(buffer: ArrayBuffer, offset: number, length: number, misc: any): void {
let data = new Uint8Array(buffer, offset, length);
let slice_queue = this.section_slice_queues_[misc.pid];
if (misc.payload_unit_start_indicator) {
let pointer_field = data[0];
if (slice_queue != undefined && slice_queue.total_length !== 0) {
let remain_section = new Uint8Array(buffer, offset + 1, Math.min(length, pointer_field));
slice_queue.slices.push(remain_section);
slice_queue.total_length += remain_section.byteLength;
if (slice_queue.total_length === slice_queue.expected_length) {
this.emitSectionSlices(slice_queue, misc);
} else {
this.clearSlices(slice_queue, misc);
}
}
for (let i = 1 + pointer_field; i < data.byteLength; ){
let table_id = data[i + 0];
if (table_id === 0xFF) { break; }
let section_length = ((data[i + 1] & 0x0F) << 8) | data[i + 2];
this.section_slice_queues_[misc.pid] = new SliceQueue();
slice_queue = this.section_slice_queues_[misc.pid];
slice_queue.expected_length = section_length + 3;
slice_queue.file_position = misc.file_position;
slice_queue.random_access_indicator = misc.random_access_indicator;
let remain_section = new Uint8Array(buffer, offset + i, Math.min(length - i, slice_queue.expected_length - slice_queue.total_length));
slice_queue.slices.push(remain_section);
slice_queue.total_length += remain_section.byteLength;
if (slice_queue.total_length === slice_queue.expected_length) {
this.emitSectionSlices(slice_queue, misc);
} else if (slice_queue.total_length >= slice_queue.expected_length) {
this.clearSlices(slice_queue, misc);
}
i += remain_section.byteLength;
}
} else if (slice_queue != undefined && slice_queue.total_length !== 0) {
let remain_section = new Uint8Array(buffer, offset, Math.min(length, slice_queue.expected_length - slice_queue.total_length));
slice_queue.slices.push(remain_section);
slice_queue.total_length += remain_section.byteLength;
if (slice_queue.total_length === slice_queue.expected_length) {
this.emitSectionSlices(slice_queue, misc);
} else if (slice_queue.total_length >= slice_queue.expected_length) {
this.clearSlices(slice_queue, misc);
}
}
}
private handlePESSlice(buffer: ArrayBuffer, offset: number, length: number, misc: any): void {
let data = new Uint8Array(buffer, offset, length);
let packet_start_code_prefix = (data[0] << 16) | (data[1] << 8) | (data[2]);
let stream_id = data[3];
let PES_packet_length = (data[4] << 8) | data[5];
if (misc.payload_unit_start_indicator) {
if (packet_start_code_prefix !== 1) {
Log.e(this.TAG, `handlePESSlice: packet_start_code_prefix should be 1 but with value ${packet_start_code_prefix}`);
return;
}
// handle queued PES slices:
// Merge into a big Uint8Array then call parsePES()
let slice_queue = this.pes_slice_queues_[misc.pid];
if (slice_queue) {
if (slice_queue.expected_length === 0 || slice_queue.expected_length === slice_queue.total_length) {
this.emitPESSlices(slice_queue, misc);
} else {
this.clearSlices(slice_queue, misc);
}
}
// Make a new PES queue for new PES slices
this.pes_slice_queues_[misc.pid] = new SliceQueue();
this.pes_slice_queues_[misc.pid].file_position = misc.file_position;
this.pes_slice_queues_[misc.pid].random_access_indicator = misc.random_access_indicator;
}
if (this.pes_slice_queues_[misc.pid] == undefined) {
// ignore PES slices without [PES slice that has payload_unit_start_indicator]
return;
}
// push subsequent PES slices into pes_queue
let slice_queue = this.pes_slice_queues_[misc.pid];
slice_queue.slices.push(data);
if (misc.payload_unit_start_indicator) {
slice_queue.expected_length = PES_packet_length === 0 ? 0 : PES_packet_length + 6;
}
slice_queue.total_length += data.byteLength;
if (slice_queue.expected_length > 0 && slice_queue.expected_length === slice_queue.total_length) {
this.emitPESSlices(slice_queue, misc);
} else if (slice_queue.expected_length > 0 && slice_queue.expected_length < slice_queue.total_length) {
this.clearSlices(slice_queue, misc);
}
}
private emitSectionSlices(slice_queue: SliceQueue, misc: any): void {
let data = new Uint8Array(slice_queue.total_length);
for (let i = 0, offset = 0; i < slice_queue.slices.length; i++) {
let slice = slice_queue.slices[i];
data.set(slice, offset);
offset += slice.byteLength;
}
slice_queue.slices = [];
slice_queue.expected_length = -1;
slice_queue.total_length = 0;
let section_data = new SectionData();
section_data.pid = misc.pid;
section_data.data = data;
section_data.file_position = slice_queue.file_position;
section_data.random_access_indicator = slice_queue.random_access_indicator;
this.parseSection(section_data);
}
private emitPESSlices(slice_queue: SliceQueue, misc: any): void {
let data = new Uint8Array(slice_queue.total_length);
for (let i = 0, offset = 0; i < slice_queue.slices.length; i++) {
let slice = slice_queue.slices[i];
data.set(slice, offset);
offset += slice.byteLength;
}
slice_queue.slices = [];
slice_queue.expected_length = -1;
slice_queue.total_length = 0;
let pes_data = new PESData();
pes_data.pid = misc.pid;
pes_data.data = data;
pes_data.stream_type = misc.stream_type;
pes_data.file_position = slice_queue.file_position;
pes_data.random_access_indicator = slice_queue.random_access_indicator;
this.parsePES(pes_data);
}
private clearSlices(slice_queue: SliceQueue, misc: any): void {
slice_queue.slices = [];
slice_queue.expected_length = -1;
slice_queue.total_length = 0;
}
private parseSection(section_data: SectionData): void {
let data = section_data.data;
let pid = section_data.pid;
if (pid === 0x00) {
this.parsePAT(data);
} else if (pid === this.current_pmt_pid_) {
this.parsePMT(data);
} else if (this.pmt_ != undefined && this.pmt_.scte_35_pids[pid]) {
this.parseSCTE35(data);
}
}
private parsePES(pes_data: PESData): void {
let data = pes_data.data;
let packet_start_code_prefix = (data[0] << 16) | (data[1] << 8) | (data[2]);
let stream_id = data[3];
let PES_packet_length = (data[4] << 8) | data[5];
if (packet_start_code_prefix !== 1) {
Log.e(this.TAG, `parsePES: packet_start_code_prefix should be 1 but with value ${packet_start_code_prefix}`);
return;
}
if (stream_id !== 0xBC // program_stream_map
&& stream_id !== 0xBE // padding_stream
&& stream_id !== 0xBF // private_stream_2
&& stream_id !== 0xF0 // ECM
&& stream_id !== 0xF1 // EMM
&& stream_id !== 0xFF // program_stream_directory
&& stream_id !== 0xF2 // DSMCC
&& stream_id !== 0xF8) {
let PES_scrambling_control = (data[6] & 0x30) >>> 4;
let PTS_DTS_flags = (data[7] & 0xC0) >>> 6;
let PES_header_data_length = data[8];
let pts: number | undefined;
let dts: number | undefined;
if (PTS_DTS_flags === 0x02 || PTS_DTS_flags === 0x03) {
pts = this.getTimestamp(data, 9);
dts = PTS_DTS_flags === 0x03 ? this.getTimestamp(data, 14) : pts;
}
let payload_start_index = 6 + 3 + PES_header_data_length;
let payload_length: number;
if (PES_packet_length !== 0) {
if (PES_packet_length < 3 + PES_header_data_length) {
Log.v(this.TAG, `Malformed PES: PES_packet_length < 3 + PES_header_data_length`);
return;
}
payload_length = PES_packet_length - 3 - PES_header_data_length;
} else { // PES_packet_length === 0
payload_length = data.byteLength - payload_start_index;
}
let payload = data.subarray(payload_start_index, payload_start_index + payload_length);
switch (pes_data.stream_type) {
case StreamType.kMPEG1Audio:
case StreamType.kMPEG2Audio:
this.parseMP3Payload(payload, pts);
break;
case StreamType.kPESPrivateData:
if (this.pmt_.common_pids.av1 === pes_data.pid) {
this.parseAV1Payload(payload, pts, dts, pes_data.file_position, pes_data.random_access_indicator);
} else if (this.pmt_.common_pids.opus === pes_data.pid) {
this.parseOpusPayload(payload, pts);
} else if (this.pmt_.common_pids.ac3 === pes_data.pid) {
this.parseAC3Payload(payload, pts);
} else if (this.pmt_.common_pids.eac3 === pes_data.pid) {
this.parseEAC3Payload(payload, pts);
} else if (this.pmt_.asynchronous_klv_pids[pes_data.pid]) {
this.parseAsynchronousKLVMetadataPayload(payload, pes_data.pid, stream_id);
} else if (this.pmt_.smpte2038_pids[pes_data.pid]) {
this.parseSMPTE2038MetadataPayload(payload, pts, dts, pes_data.pid, stream_id);
} else {
this.parsePESPrivateDataPayload(payload, pts, dts, pes_data.pid, stream_id);
}
break;
case StreamType.kADTSAAC:
this.parseADTSAACPayload(payload, pts);
break;
case StreamType.kLOASAAC:
this.parseLOASAACPayload(payload, pts);
break;
case StreamType.kAC3:
this.parseAC3Payload(payload, pts);
break;
case StreamType.kEAC3:
this.parseEAC3Payload(payload, pts);
break;
case StreamType.kMetadata:
if (this.pmt_.timed_id3_pids[pes_data.pid]) {
this.parseTimedID3MetadataPayload(payload, pts, dts, pes_data.pid, stream_id);
} else if (this.pmt_.synchronous_klv_pids[pes_data.pid]) {
this.parseSynchronousKLVMetadataPayload(payload, pts, dts, pes_data.pid, stream_id);
}
break;
case StreamType.kH264:
this.parseH264Payload(payload, pts, dts, pes_data.file_position, pes_data.random_access_indicator);
break;
case StreamType.kH265:
this.parseH265Payload(payload, pts, dts, pes_data.file_position, pes_data.random_access_indicator);
break;
default:
break;
}
} else if (stream_id === 0xBC // program_stream_map
|| stream_id === 0xBF // private_stream_2
|| stream_id === 0xF0 // ECM
|| stream_id === 0xF1 // EMM
|| stream_id === 0xFF // program_stream_directory
|| stream_id === 0xF2 // DSMCC_stream
|| stream_id === 0xF8) { // ITU-T Rec. H.222.1 type E stream
if (pes_data.stream_type === StreamType.kPESPrivateData) {
let payload_start_index = 6;
let payload_length: number;
if (PES_packet_length !== 0) {
payload_length = PES_packet_length;
} else { // PES_packet_length === 0
payload_length = data.byteLength - payload_start_index;
}
let payload = data.subarray(payload_start_index, payload_start_index + payload_length);
this.parsePESPrivateDataPayload(payload, undefined, undefined, pes_data.pid, stream_id);
}
}
}
private parsePAT(data: Uint8Array): void {
let table_id = data[0];
if (table_id !== 0x00) {
Log.e(this.TAG, `parsePAT: table_id ${table_id} is not corresponded to PAT!`);
return;
}
let section_length = ((data[1] & 0x0F) << 8) | data[2];
let transport_stream_id = (data[3] << 8) | data[4];
let version_number = (data[5] & 0x3E) >>> 1;
let current_next_indicator = data[5] & 0x01;
let section_number = data[6];
let last_section_number = data[7];
let pat: PAT = null;
if (current_next_indicator === 1 && section_number === 0) {
pat = new PAT();
pat.version_number = version_number;
} else {
pat = this.pat_;
if (pat == undefined) {
return;
}
}
let program_start_index = 8;
let program_bytes = section_length - 5 - 4; // section_length - (headers + crc)
let first_program_number = -1;
let first_pmt_pid = -1;
for (let i = program_start_index; i < program_start_index + program_bytes; i += 4) {
let program_number = (data[i] << 8) | data[i + 1];
let pid = ((data[i + 2] & 0x1F) << 8) | data[i + 3];
if (program_number === 0) {
// network_PID
pat.network_pid = pid;
} else {
// program_map_PID
pat.program_pmt_pid[program_number] = pid;
if (first_program_number === -1) {
first_program_number = program_number;
}
if (first_pmt_pid === -1) {
first_pmt_pid = pid;
}
}
}
// Currently we only deal with first appeared PMT pid
if (current_next_indicator === 1 && section_number === 0) {
if (this.pat_ == undefined) {
Log.v(this.TAG, `Parsed first PAT: ${JSON.stringify(pat)}`);
}
this.pat_ = pat;
this.current_program_ = first_program_number;
this.current_pmt_pid_ = first_pmt_pid;
}
}
private parsePMT(data: Uint8Array): void {
let table_id = data[0];
if (table_id !== 0x02) {
Log.e(this.TAG, `parsePMT: table_id ${table_id} is not corresponded to PMT!`);
return;
}
let section_length = ((data[1] & 0x0F) << 8) | data[2];
let program_number = (data[3] << 8) | data[4];
let version_number = (data[5] & 0x3E) >>> 1;
let current_next_indicator = data[5] & 0x01;
let section_number = data[6];
let last_section_number = data[7];
let pmt: PMT = null;
if (current_next_indicator === 1 && section_number === 0) {
pmt = new PMT();
pmt.program_number = program_number;
pmt.version_number = version_number;
this.program_pmt_map_[program_number] = pmt;
} else {
pmt = this.program_pmt_map_[program_number];
if (pmt == undefined) {
return;
}
}
pmt.pcr_pid = ((data[8] & 0x1F) << 8) | data[9];
let program_info_length = ((data[10] & 0x0F) << 8) | data[11];
let info_start_index = 12 + program_info_length;
let info_bytes = section_length - 9 - program_info_length - 4;
for (let i = info_start_index; i < info_start_index + info_bytes; ) {
let stream_type = data[i] as StreamType;
let elementary_PID = ((data[i + 1] & 0x1F) << 8) | data[i + 2];
let ES_info_length = ((data[i + 3] & 0x0F) << 8) | data[i + 4];
pmt.pid_stream_type[elementary_PID] = stream_type;
let already_has_video = pmt.common_pids.h264 || pmt.common_pids.h265;
let already_has_audio = pmt.common_pids.adts_aac || pmt.common_pids.loas_aac || pmt.common_pids.ac3 || pmt.common_pids.eac3 || pmt.common_pids.opus || pmt.common_pids.mp3;
if (stream_type === StreamType.kH264 && !already_has_video) {
pmt.common_pids.h264 = elementary_PID;
} else if (stream_type === StreamType.kH265 && !already_has_video) {
pmt.common_pids.h265 = elementary_PID;
} else if (stream_type === StreamType.kADTSAAC && !already_has_audio) {
pmt.common_pids.adts_aac = elementary_PID;
} else if (stream_type === StreamType.kLOASAAC && !already_has_audio) {
pmt.common_pids.loas_aac = elementary_PID;
} else if (stream_type === StreamType.kAC3 && !already_has_audio) {
pmt.common_pids.ac3 = elementary_PID; // ATSC AC-3
} else if (stream_type === StreamType.kEAC3 && !already_has_audio) {
pmt.common_pids.eac3 = elementary_PID; // ATSC EAC-3
} else if ((stream_type === StreamType.kMPEG1Audio || stream_type === StreamType.kMPEG2Audio) && !already_has_audio) {
pmt.common_pids.mp3 = elementary_PID;
} else if (stream_type === StreamType.kPESPrivateData) {
pmt.pes_private_data_pids[elementary_PID] = true;
if (ES_info_length > 0) {
// parse descriptor for PES private data
for (let offset = i + 5; offset < i + 5 + ES_info_length; ) {
let tag = data[offset + 0];
let length = data[offset + 1];
if (tag === 0x05) { // Registration Descriptor
let registration = String.fromCharCode(... Array.from(data.subarray(offset + 2, offset + 2 + length)));
if (registration === 'VANC') {
pmt.smpte2038_pids[elementary_PID] = true;
} /* else if (registration === 'AC-3' && !already_has_audio) {
pmt.common_pids.ac3 = elementary_PID; // DVB AC-3 (FIXME: NEED VERIFY)
} */ /* else if (registration === 'EC-3' && !alrady_has_audio) {
pmt.common_pids.eac3 = elementary_PID; // DVB EAC-3 (FIXME: NEED VERIFY)
} */
else if (registration === 'AV01') {
pmt.common_pids.av1 = elementary_PID;
} else if (registration === 'Opus') {
pmt.common_pids.opus = elementary_PID;
} else if (registration === 'KLVA') {
pmt.asynchronous_klv_pids[elementary_PID] = true;
}
} else if (tag === 0x7F) { // DVB extension descriptor
if (elementary_PID === pmt.common_pids.opus) {
let ext_desc_tag = data[offset + 2];
let channel_config_code: number | null = null;
if (ext_desc_tag === 0x80) { // User defined (provisional Opus)
channel_config_code = data[offset + 3];
}
if (channel_config_code == null) {
Log.e(this.TAG, `Not Supported Opus channel count.`);
continue;
}
const meta = {
codec: 'opus',
channel_count: (channel_config_code & 0x0F) === 0 ? 2 : (channel_config_code & 0x0F),
channel_config_code,
sample_rate: 48000
} as const;
const sample = {
codec: 'opus',
meta
} as const;
if (this.audio_init_segment_dispatched_ == false) {
this.audio_metadata_ = meta;
this.dispatchAudioInitSegment(sample);
} else if (this.detectAudioMetadataChange(sample)) {
// flush stashed frames before notify new AudioSpecificConfig
this.dispatchAudioMediaSegment();
// notify new AAC AudioSpecificConfig
this.dispatchAudioInitSegment(sample);
}
}
} else if (tag === 0x80) {
if (elementary_PID === pmt.common_pids.av1) {
this.video_metadata_.av1c = data.subarray(offset + 2, offset + 2 + length)
}
}
offset += 2 + length;
}
// provide descriptor for PES private data via callback
let descriptors = data.subarray(i + 5, i + 5 + ES_info_length);
this.dispatchPESPrivateDataDescriptor(elementary_PID, stream_type, descriptors);
}
} else if (stream_type === StreamType.kMetadata) {
if (ES_info_length > 0) {
// parse descriptor for PES private data
for (let offset = i + 5; offset < i + 5 + ES_info_length; ) {
let tag = data[offset + 0];
let length = data[offset + 1];
if (tag === 0x26) {
let metadata_application_format = (data[offset + 2] << 8) | (data[offset + 3] << 0);
let metadata_application_format_identifier = null;
if (metadata_application_format === 0xFFFF) {
metadata_application_format_identifier = String.fromCharCode(... Array.from(data.subarray(offset + 4, offset + 4 + 4)));
}
let metadata_format = data[offset + 4 + (metadata_application_format === 0xFFFF ? 4 : 0)];
let metadata_format_identifier = null;
if (metadata_format === 0xFF) {
let pad = 4 + (metadata_application_format === 0xFFFF ? 4 : 0) + 1;
metadata_format_identifier = String.fromCharCode(... Array.from(data.subarray(offset + pad, offset + pad + 4)));
}
if (metadata_application_format_identifier === 'ID3 ' && metadata_format_identifier === 'ID3 ') {
pmt.timed_id3_pids[elementary_PID] = true;
} else if (metadata_format_identifier === 'KLVA') {
pmt.synchronous_klv_pids[elementary_PID] = true;
}
}
offset += 2 + length;
}
}
} else if (stream_type === StreamType.kSCTE35) {
pmt.scte_35_pids[elementary_PID] = true;
}
i += 5 + ES_info_length;
}
if (program_number === this.current_program_) {
if (this.pmt_ == undefined) {
Log.v(this.TAG, `Parsed first PMT: ${JSON.stringify(pmt)}`);
}
this.pmt_ = pmt;
if (pmt.common_pids.h264 || pmt.common_pids.h265 || pmt.common_pids.av1) {
this.has_video_ = true;
}
if (pmt.common_pids.adts_aac || pmt.common_pids.loas_aac || pmt.common_pids.ac3 || pmt.common_pids.opus || pmt.common_pids.mp3) {
this.has_audio_ = true;
}
}
}
private parseSCTE35(data: Uint8Array): void {
const scte35 = readSCTE35(data);
if (scte35.pts != undefined) {
let pts_ms = Math.floor(scte35.pts / this.timescale_);
scte35.pts = pts_ms;
} else {
scte35.nearest_pts = this.getNearestTimestampMilliseconds();
}
if (this.onSCTE35Metadata) {
this.onSCTE35Metadata(scte35);
}
}
private parseAV1Payload(data: Uint8Array, pts: number, dts: number, file_position: number, random_access_indicator: number) {
let av1_in_ts_parser = new AV1OBUInMpegTsParser(data);
let payload: Uint8Array | null = null;
let units: {data: Uint8Array}[] = [];
let length = 0;
let keyframe = false;
let details = null;
while ((payload = av1_in_ts_parser.readNextOBUPayload()) != null) {
details = AV1OBUParser.parseOBUs(payload, this.video_metadata_.details);
if (details && details.keyframe === true) {
if (!this.video_init_segment_dispatched_) {
const av1c = new Uint8Array((new ArrayBuffer(this.video_metadata_.av1c.byteLength + details.sequence_header_data.byteLength)));
av1c.set(this.video_metadata_.av1c, 0);
av1c.set(details.sequence_header_data, this.video_metadata_.av1c.byteLength);
details.av1c = av1c;
this.video_metadata_.details = details;
this.dispatchVideoInitSegment();
} else if (this.detectVideoMetadataChange(null, details) === true) {
this.video_metadata_changed_ = true;
// flush stashed frames before changing codec metadata
this.dispatchVideoMediaSegment();
const av1c = new Uint8Array((new ArrayBuffer(this.video_metadata_.av1c.byteLength + details.sequence_header_data.byteLength)));
av1c.set(this.video_metadata_.av1c, 0);
av1c.set(details.sequence_header_data, this.video_metadata_.av1c.byteLength);
details.av1c = av1c;
// notify new codec metadata (maybe changed)
this.dispatchVideoInitSegment();
}
}
this.video_metadata_.details = details;
//if (this.video_init_segment_dispatched_) {
keyframe ||= details.keyframe;
units.push({ data: payload });
length += payload.byteLength;
//}
}
let pts_ms = Math.floor(pts / this.timescale_);
let dts_ms = Math.floor(dts / this.timescale_);
if (units.length) {
let track = this.video_track_;
let av1_sample = {
units,
length,
isKeyframe: keyframe,
dts: dts_ms,
pts: pts_ms,
cts: pts_ms - dts_ms,
file_position
};
track.samples.push(av1_sample);
track.length += length;
}
}
private parseH264Payload(data: Uint8Array, pts: number, dts: number, file_position: number, random_access_indicator: number) {
let annexb_parser = new H264AnnexBParser(data);
let nalu_payload: H264NaluPayload = null;
let units: {type: H264NaluType, data: Uint8Array}[] = [];
let length = 0;
let keyframe = false;
while ((nalu_payload = annexb_parser.readNextNaluPayload()) != null) {
let nalu_avc1 = new H264NaluAVC1(nalu_payload);
if (nalu_avc1.type === H264NaluType.kSliceSPS) {
// Notice: parseSPS requires Nalu without startcode or length-header
let details = SPSParser.parseSPS(nalu_payload.data);
if (!this.video_init_segment_dispatched_) {
this.video_metadata_.sps = nalu_avc1;
this.video_metadata_.details = details;
} else if (this.detectVideoMetadataChange(nalu_avc1, details) === true) {
Log.v(this.TAG, `H264: Critical h264 metadata has been changed, attempt to re-generate InitSegment`);
this.video_metadata_changed_ = true;
this.video_metadata_ = {vps: undefined, sps: nalu_avc1, pps: undefined, av1c: undefined, details: details};
}
} else if (nalu_avc1.type === H264NaluType.kSlicePPS) {
if (!this.video_init_segment_dispatched_ || this.video_metadata_changed_) {
this.video_metadata_.pps = nalu_avc1;
if (this.video_metadata_.sps && this.video_metadata_.pps) {
if (this.video_metadata_changed_) {
// flush stashed frames before changing codec metadata
this.dispatchVideoMediaSegment();
}
// notify new codec metadata (maybe changed)
this.dispatchVideoInitSegment();
}
}
} else if (nalu_avc1.type === H264NaluType.kSliceIDR) {
keyframe = true;
} else if (nalu_avc1.type === H264NaluType.kSliceNonIDR && random_access_indicator === 1) {
// For open-gop stream, use random_access_indicator to identify keyframe
keyframe = true;
}
// Push samples to remuxer only if initialization metadata has been dispatched
if (this.video_init_segment_dispatched_) {
units.push(nalu_avc1);
length += nalu_avc1.data.byteLength;
}
}
let pts_ms = Math.floor(pts / this.timescale_);
let dts_ms = Math.floor(dts / this.timescale_);
if (units.length) {
let track = this.video_track_;
let avc_sample = {
units,
length,
isKeyframe: keyframe,
dts: dts_ms,
pts: pts_ms,
cts: pts_ms - dts_ms,
file_position
};
track.samples.push(avc_sample);
track.length += length;
}
}
private parseH265Payload(data: Uint8Array, pts: number, dts: number, file_position: number, random_access_indicator: number) {
let annexb_parser = new H265AnnexBParser(data);
let nalu_payload: H265NaluPayload = null;
let units: {type: H265NaluType, data: Uint8Array}[] = [];
let length = 0;
let keyframe = false;
while ((nalu_payload = annexb_parser.readNextNaluPayload()) != null) {
let nalu_hvc1 = new H265NaluHVC1(nalu_payload);
if (nalu_hvc1.type === H265NaluType.kSliceVPS) {
if (!this.video_init_segment_dispatched_) {
let details = H265Parser.parseVPS(nalu_payload.data);
this.video_metadata_.vps = nalu_hvc1;
this.video_metadata_.details = {
... this.video_metadata_.details,
... details
};
}
} else if (nalu_hvc1.type === H265NaluType.kSliceSPS) {
let details = H265Parser.parseSPS(nalu_payload.data);
if (!this.video_init_segment_dispatched_) {
this.video_metadata_.sps = nalu_hvc1;
this.video_metadata_.details = {
... this.video_metadata_.details,
... details
};
} else if (this.detectVideoMetadataChange(nalu_hvc1, details) === true) {
Log.v(this.TAG, `H265: Critical h265 metadata has been changed, attempt to re-generate InitSegment`);
this.video_metadata_changed_ = true;
this.video_metadata_ = { vps: undefined, sps: nalu_hvc1, pps: undefined, av1c: undefined, details: details};
}
} else if (nalu_hvc1.type === H265NaluType.kSlicePPS) {
if (!this.video_init_segment_dispatched_ || this.video_metadata_changed_) {
let details = H265Parser.parsePPS(nalu_payload.data);
this.video_metadata_.pps = nalu_hvc1;
this.video_metadata_.details = {
... this.video_metadata_.details,
... details
};
if (this.video_metadata_.vps && this.video_metadata_.sps && this.video_metadata_.pps) {
if (this.video_metadata_changed_) {
// flush stashed frames before changing codec metadata
this.dispatchVideoMediaSegment();
}
// notify new codec metadata (maybe changed)
this.dispatchVideoInitSegment();
}
}
} else if (nalu_hvc1.type === H265NaluType.kSliceIDR_W_RADL || nalu_hvc1.type === H265NaluType.kSliceIDR_N_LP || nalu_hvc1.type === H265NaluType.kSliceCRA_NUT) {
keyframe = true;
}
// Push samples to remuxer only if initialization metadata has been dispatched
if (this.video_init_segment_dispatched_) {
units.push(nalu_hvc1);
length += nalu_hvc1.data.byteLength;
}
}
let pts_ms = Math.floor(pts / this.timescale_);
let dts_ms = Math.floor(dts / this.timescale_);
if (units.length) {
let track = this.video_track_;
let hvc_sample = {
units,
length,
isKeyframe: keyframe,
dts: dts_ms,
pts: pts_ms,
cts: pts_ms - dts_ms,
file_position
};
track.samples.push(hvc_sample);
track.length += length;
}
}
private detectVideoMetadataChange(new_sps: H264NaluAVC1 | H265NaluHVC1, new_details: any): boo