mediabunny
Version:
Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.
634 lines (633 loc) • 27.1 kB
JavaScript
/*!
* Copyright (c) 2026-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { buildAdtsHeaderTemplate, parseAacAudioSpecificConfig, writeAdtsFrameLength } from '../../shared/aac-misc.js';
import { validateAudioChunkMetadata, validateVideoChunkMetadata } from '../codec.js';
import { AC3_REGISTRATION_DESCRIPTOR, AvcNalUnitType, concatNalUnitsInAnnexB, deserializeAvcDecoderConfigurationRecord, deserializeHevcDecoderConfigurationRecord, EAC3_REGISTRATION_DESCRIPTOR, extractNalUnitTypeForAvc, extractNalUnitTypeForHevc, HevcNalUnitType, iterateNalUnitsInAnnexB, iterateNalUnitsInLengthPrefixed, } from '../codec-data.js';
import { Bitstream } from '../../shared/bitstream.js';
import { assert, promiseWithResolvers, setUint24, toDataView, toUint8Array } from '../misc.js';
import { Muxer } from '../muxer.js';
import { buildMpegTsMimeType, TIMESCALE, TS_PACKET_SIZE } from './mpeg-ts-misc.js';
// Resources:
// ISO/IEC 13818-1
const PAT_PID = 0x0000;
const PMT_PID = 0x1000;
const FIRST_TRACK_PID = 0x0100;
const VIDEO_STREAM_ID_BASE = 0xE0;
const AUDIO_STREAM_ID_BASE = 0xC0;
const AVC_AUD_NAL = new Uint8Array([0x09, 0xF0]);
const HEVC_AUD_NAL = new Uint8Array([0x46, 0x01]);
export class MpegTsMuxer extends Muxer {
constructor(output, format) {
super(output);
this.trackDatas = [];
this.tablesWritten = false;
this.continuityCounters = new Map();
this.packetBuffer = new Uint8Array(TS_PACKET_SIZE);
this.packetView = toDataView(this.packetBuffer);
this.allTracksKnown = promiseWithResolvers();
this.videoTrackIndex = 0;
this.audioTrackIndex = 0;
this.adaptationFieldBuffer = new Uint8Array(184);
this.payloadBuffer = new Uint8Array(184);
this.format = format;
}
async start() {
const release = await this.mutex.acquire();
this.writer = await this.output._getRootWriter(true);
release();
}
async getMimeType() {
await this.allTracksKnown.promise;
return buildMpegTsMimeType(this.trackDatas.map(x => x.codecString));
}
getVideoTrackData(track, meta) {
const existingTrackData = this.trackDatas.find(x => x.track === track);
if (existingTrackData) {
return existingTrackData;
}
validateVideoChunkMetadata(meta);
assert(meta?.decoderConfig);
const codec = track.source._codec;
assert(codec === 'avc' || codec === 'hevc');
const streamType = codec === 'avc'
? 27 /* MpegTsStreamType.AVC */
: 36 /* MpegTsStreamType.HEVC */;
const pid = FIRST_TRACK_PID + this.trackDatas.length;
const streamId = VIDEO_STREAM_ID_BASE + this.videoTrackIndex++;
const newTrackData = {
track,
pid,
streamType,
streamId,
codecString: meta.decoderConfig.codec,
timestampProcessingQueue: [],
packetQueue: [],
inputIsAnnexB: null,
inputIsAdts: null,
avcDecoderConfig: null,
hevcDecoderConfig: null,
adtsHeader: null,
adtsHeaderBitstream: null,
firstPacketWritten: false,
closed: false,
};
this.trackDatas.push(newTrackData);
if (this.allTracksAreKnown()) {
this.allTracksKnown.resolve();
}
return newTrackData;
}
getAudioTrackData(track, meta) {
const existingTrackData = this.trackDatas.find(x => x.track === track);
if (existingTrackData) {
return existingTrackData;
}
validateAudioChunkMetadata(meta);
assert(meta?.decoderConfig);
const codec = track.source._codec;
assert(codec === 'aac' || codec === 'mp3' || codec === 'ac3' || codec === 'eac3');
let streamType;
let streamId;
switch (codec) {
case 'aac':
{
streamType = 15 /* MpegTsStreamType.AAC */;
streamId = AUDIO_STREAM_ID_BASE + this.audioTrackIndex++;
}
;
break;
case 'mp3':
{
streamType = 3 /* MpegTsStreamType.MP3_MPEG1 */;
streamId = AUDIO_STREAM_ID_BASE + this.audioTrackIndex++;
}
;
break;
case 'ac3':
{
streamType = 129 /* MpegTsStreamType.AC3_SYSTEM_A */;
streamId = 0xbd;
}
;
break;
case 'eac3':
{
streamType = 135 /* MpegTsStreamType.EAC3_SYSTEM_A */;
streamId = 0xbd;
}
;
break;
}
const pid = FIRST_TRACK_PID + this.trackDatas.length;
const newTrackData = {
track,
pid,
streamType,
streamId,
codecString: meta.decoderConfig.codec,
timestampProcessingQueue: [],
packetQueue: [],
inputIsAnnexB: null,
inputIsAdts: null,
avcDecoderConfig: null,
hevcDecoderConfig: null,
adtsHeader: null,
adtsHeaderBitstream: null,
firstPacketWritten: false,
closed: false,
};
this.trackDatas.push(newTrackData);
if (this.allTracksAreKnown()) {
this.allTracksKnown.resolve();
}
return newTrackData;
}
async addEncodedVideoPacket(track, packet, meta) {
const release = await this.mutex.acquire();
try {
const trackData = this.getVideoTrackData(track, meta);
this.validateTimestamp(trackData.track, packet.timestamp, packet.type === 'key');
const preparedData = this.prepareVideoPacket(trackData, packet, meta);
if (packet.type === 'key') {
await this.flushTimestampQueue(trackData);
}
trackData.timestampProcessingQueue.push({
data: preparedData,
presentationTimestamp: packet.timestamp,
decodeTimestamp: null,
isKeyframe: packet.type === 'key',
});
}
finally {
release();
}
}
async addEncodedAudioPacket(track, packet, meta) {
const release = await this.mutex.acquire();
try {
const trackData = this.getAudioTrackData(track, meta);
this.validateTimestamp(trackData.track, packet.timestamp, packet.type === 'key');
const preparedData = this.prepareAudioPacket(trackData, packet, meta);
if (packet.type === 'key') {
await this.flushTimestampQueue(trackData);
}
trackData.timestampProcessingQueue.push({
data: preparedData,
presentationTimestamp: packet.timestamp,
decodeTimestamp: null,
isKeyframe: packet.type === 'key',
});
}
finally {
release();
}
}
async addSubtitleCue() {
throw new Error('MPEG-TS does not support subtitles.');
}
prepareVideoPacket(trackData, packet, meta) {
const codec = trackData.track.source._codec;
if (trackData.inputIsAnnexB === null) {
// This is the first packet
const description = meta?.decoderConfig?.description;
trackData.inputIsAnnexB = !description;
if (!trackData.inputIsAnnexB) {
const bytes = toUint8Array(description);
if (codec === 'avc') {
trackData.avcDecoderConfig = deserializeAvcDecoderConfigurationRecord(bytes);
}
else {
trackData.hevcDecoderConfig = deserializeHevcDecoderConfigurationRecord(bytes);
}
}
}
if (trackData.inputIsAnnexB) {
return this.prepareAnnexBVideoPacket(packet.data, codec);
}
else {
return this.prepareLengthPrefixedVideoPacket(trackData, packet, codec);
}
}
prepareAnnexBVideoPacket(data, codec) {
const nalUnits = [];
for (const loc of iterateNalUnitsInAnnexB(data)) {
const nalUnit = data.subarray(loc.offset, loc.offset + loc.length);
const isAud = codec === 'avc'
? extractNalUnitTypeForAvc(nalUnit[0]) === AvcNalUnitType.AUD
: extractNalUnitTypeForHevc(nalUnit[0]) === HevcNalUnitType.AUD_NUT;
if (!isAud) {
nalUnits.push(nalUnit);
}
}
// Pretend the AUD
const aud = codec === 'avc'
? AVC_AUD_NAL
: HEVC_AUD_NAL;
nalUnits.unshift(aud);
return concatNalUnitsInAnnexB(nalUnits);
}
prepareLengthPrefixedVideoPacket(trackData, packet, codec) {
const data = packet.data;
const lengthSize = codec === 'avc'
? (trackData.avcDecoderConfig.lengthSizeMinusOne + 1)
: (trackData.hevcDecoderConfig.lengthSizeMinusOne + 1);
const nalUnits = [];
for (const loc of iterateNalUnitsInLengthPrefixed(data, lengthSize)) {
const nalUnit = data.subarray(loc.offset, loc.offset + loc.length);
const isAud = codec === 'avc'
? extractNalUnitTypeForAvc(nalUnit[0]) === AvcNalUnitType.AUD
: extractNalUnitTypeForHevc(nalUnit[0]) === HevcNalUnitType.AUD_NUT;
if (!isAud) {
nalUnits.push(nalUnit);
}
}
if (packet.type === 'key') {
// Add whichever NALUs are missing
if (codec === 'avc') {
const config = trackData.avcDecoderConfig;
for (const pps of config.pictureParameterSets) {
nalUnits.unshift(pps);
}
for (const sps of config.sequenceParameterSets) {
nalUnits.unshift(sps);
}
}
else {
const config = trackData.hevcDecoderConfig;
for (const arr of config.arrays) {
if (arr.nalUnitType === HevcNalUnitType.PPS_NUT) {
for (const nal of arr.nalUnits) {
nalUnits.unshift(nal);
}
}
}
for (const arr of config.arrays) {
if (arr.nalUnitType === HevcNalUnitType.SPS_NUT) {
for (const nal of arr.nalUnits) {
nalUnits.unshift(nal);
}
}
}
for (const arr of config.arrays) {
if (arr.nalUnitType === HevcNalUnitType.VPS_NUT) {
for (const nal of arr.nalUnits) {
nalUnits.unshift(nal);
}
}
}
}
}
// Prepend the AUD
const aud = codec === 'avc'
? AVC_AUD_NAL
: HEVC_AUD_NAL;
nalUnits.unshift(aud);
return concatNalUnitsInAnnexB(nalUnits);
}
prepareAudioPacket(trackData, packet, meta) {
const codec = trackData.track.source._codec;
if (codec === 'mp3' || codec === 'ac3' || codec === 'eac3') {
// We're good
return packet.data;
}
if (trackData.inputIsAdts === null) {
// It's the first packet
const description = meta?.decoderConfig?.description;
trackData.inputIsAdts = !description;
if (!trackData.inputIsAdts) {
const config = parseAacAudioSpecificConfig(toUint8Array(description));
const template = buildAdtsHeaderTemplate(config);
trackData.adtsHeader = template.header;
trackData.adtsHeaderBitstream = template.bitstream;
}
}
if (trackData.inputIsAdts) {
return packet.data;
}
assert(trackData.adtsHeader);
assert(trackData.adtsHeaderBitstream);
const header = trackData.adtsHeader;
const frameLength = packet.data.byteLength + header.byteLength;
writeAdtsFrameLength(trackData.adtsHeaderBitstream, frameLength);
const result = new Uint8Array(frameLength);
result.set(header, 0);
result.set(packet.data, header.byteLength);
return result;
}
allTracksAreKnown() {
for (const track of this.output._tracks) {
if (!track.source._closed && !this.trackDatas.some(x => x.track === track)) {
return false;
}
}
return true;
}
async flushTimestampQueue(trackData, alsoInterleave = true) {
if (trackData.timestampProcessingQueue.length === 0) {
return;
}
const sortedTimestamps = trackData.timestampProcessingQueue
.map(packet => packet.presentationTimestamp)
.sort((a, b) => a - b);
for (let i = 0; i < trackData.timestampProcessingQueue.length; i++) {
const queuedPacket = trackData.timestampProcessingQueue[i];
queuedPacket.decodeTimestamp = sortedTimestamps[i];
trackData.packetQueue.push(queuedPacket);
}
trackData.timestampProcessingQueue.length = 0;
if (alsoInterleave) {
await this.interleavePackets();
}
}
async interleavePackets(isFinalCall = false) {
if (!this.tablesWritten) {
if (!this.allTracksAreKnown() && !isFinalCall) {
return;
}
this.writeTables();
}
outer: while (true) {
let trackWithMinTimestamp = null;
let minTimestamp = Infinity;
for (const trackData of this.trackDatas) {
if (!isFinalCall
&& trackData.packetQueue.length === 0
&& !trackData.closed) {
break outer;
}
if (trackData.packetQueue.length > 0
&& trackData.packetQueue[0].presentationTimestamp < minTimestamp) {
trackWithMinTimestamp = trackData;
minTimestamp = trackData.packetQueue[0].presentationTimestamp;
}
}
if (!trackWithMinTimestamp) {
break;
}
const queuedPacket = trackWithMinTimestamp.packetQueue.shift();
this.writePesPacket(trackWithMinTimestamp, queuedPacket);
}
if (!isFinalCall) {
await this.writer.flush();
}
}
writeTables() {
assert(!this.tablesWritten);
this.writePsiSection(PAT_PID, PAT_SECTION);
this.writePsiSection(PMT_PID, buildPmt(this.trackDatas));
this.tablesWritten = true;
}
writePsiSection(pid, section) {
let offset = 0;
let isFirst = true;
// Long PSI sections might span more than one TS packet
while (offset < section.length) {
const pointerFieldSize = isFirst ? 1 : 0;
const availablePayload = 184 - pointerFieldSize;
const remainingData = section.length - offset;
const chunkSize = Math.min(availablePayload, remainingData);
let payload;
if (isFirst) {
payload = this.payloadBuffer.subarray(0, 1 + chunkSize);
payload[0] = 0x00; // pointer_field
payload.set(section.subarray(offset, offset + chunkSize), 1);
}
else {
payload = section.subarray(offset, offset + chunkSize);
}
this.writeTsPacket(pid, isFirst, null, payload);
offset += chunkSize;
isFirst = false;
}
}
writePesPacket(trackData, queuedPacket) {
const includeDts = trackData.track.type === 'video';
const headerDataLength = includeDts ? 10 : 5;
const pesHeaderBuffer = new Uint8Array(9 + headerDataLength);
const pesView = toDataView(pesHeaderBuffer);
const ptsDtsBitstream = new Bitstream(pesHeaderBuffer.subarray(9));
setUint24(pesView, 0, 0x000001, false); // packet_start_code_prefix
pesHeaderBuffer[3] = trackData.streamId; // stream_id
const pesPacketLength = trackData.track.type === 'video'
? 0 // Unbounded
: Math.min(8 + queuedPacket.data.length, 0xFFFF); // Required for audio for some reason
pesView.setUint16(4, pesPacketLength, false);
// '10' marker, PES_scrambling_control=0, PES_priority=0,
// data_alignment_indicator=1, copyright=0, original_or_copy=0
pesView.setUint8(6, 0x84);
pesView.setUint8(7, includeDts ? 0xC0 : 0x80); // PTS_DTS_flags, other flags=0
pesView.setUint8(8, headerDataLength); // PES_header_data_length
const pts = Math.round(queuedPacket.presentationTimestamp * TIMESCALE);
ptsDtsBitstream.pos = 0;
ptsDtsBitstream.writeBits(4, includeDts ? 0b0011 : 0b0010); // marker
ptsDtsBitstream.writeBits(3, (pts >>> 30) & 0x7); // PTS[32:30]
ptsDtsBitstream.writeBits(1, 1); // marker_bit
ptsDtsBitstream.writeBits(15, (pts >>> 15) & 0x7FFF); // PTS[29:15]
ptsDtsBitstream.writeBits(1, 1); // marker_bit
ptsDtsBitstream.writeBits(15, pts & 0x7FFF); // PTS[14:0]
ptsDtsBitstream.writeBits(1, 1); // marker_bit
if (includeDts) {
assert(queuedPacket.decodeTimestamp !== null);
const dts = Math.round(queuedPacket.decodeTimestamp * TIMESCALE);
ptsDtsBitstream.writeBits(4, 0b0001);
ptsDtsBitstream.writeBits(3, (dts >>> 30) & 0x7); // DTS[32:30]
ptsDtsBitstream.writeBits(1, 1); // marker_bit
ptsDtsBitstream.writeBits(15, (dts >>> 15) & 0x7FFF); // DTS[29:15]
ptsDtsBitstream.writeBits(1, 1); // marker_bit
ptsDtsBitstream.writeBits(15, dts & 0x7FFF); // DTS[14:0]
ptsDtsBitstream.writeBits(1, 1); // marker_bit
}
const totalLength = pesHeaderBuffer.length + queuedPacket.data.length;
let offset = 0;
let isFirstTsPacket = true;
while (offset < totalLength) {
const pusi = isFirstTsPacket;
const remainingData = totalLength - offset;
const randomAccessIndicator = isFirstTsPacket && queuedPacket.isKeyframe;
const discontinuityIndicator = isFirstTsPacket && !trackData.firstPacketWritten;
const basePaddingNeeded = Math.max(0, 184 - remainingData);
let adaptationFieldSize;
if (randomAccessIndicator || discontinuityIndicator) {
// We need at least two bytes
adaptationFieldSize = Math.max(2, basePaddingNeeded);
}
else {
adaptationFieldSize = basePaddingNeeded;
}
let adaptationField = null;
if (adaptationFieldSize > 0) {
const buf = this.adaptationFieldBuffer;
if (adaptationFieldSize === 1) {
buf[0] = 0; // adaptation_field_length
}
else {
buf[0] = adaptationFieldSize - 1; // adaptation_field_length
buf[1]
= (Number(discontinuityIndicator) << 7) // discontinuity_indicator
| (Number(randomAccessIndicator) << 6); // random_access_indicator
buf.fill(0xFF, 2, adaptationFieldSize); // stuffing_bytes
}
adaptationField = buf.subarray(0, adaptationFieldSize);
}
const payloadSize = Math.min(184 - adaptationFieldSize, remainingData);
const payload = this.payloadBuffer.subarray(0, payloadSize);
let payloadOffset = 0;
if (offset < pesHeaderBuffer.length) {
const headerBytes = Math.min(pesHeaderBuffer.length - offset, payloadSize);
payload.set(pesHeaderBuffer.subarray(offset, offset + headerBytes), 0);
payloadOffset = headerBytes;
}
const dataStart = Math.max(0, offset - pesHeaderBuffer.length);
const dataEnd = dataStart + (payloadSize - payloadOffset);
if (payloadOffset < payloadSize) {
payload.set(queuedPacket.data.subarray(dataStart, dataEnd), payloadOffset);
}
this.writeTsPacket(trackData.pid, pusi, adaptationField, payload);
offset += payloadSize;
isFirstTsPacket = false;
}
trackData.firstPacketWritten = true;
}
writeTsPacket(pid, pusi, adaptationField, payload) {
const cc = this.continuityCounters.get(pid) ?? 0;
const hasPayload = payload.length > 0;
const adaptCtrl = adaptationField
? (hasPayload ? 0b11 : 0b10)
: (hasPayload ? 0b01 : 0b00);
this.packetBuffer[0] = 0x47; // sync_byte
this.packetView.setUint16(1, (pusi ? 0x4000 : 0) | (pid & 0x1FFF), false); // TEI=0, PUSI, priority=0, PID
// scrambling=0, adaptation_field_control, continuity_counter
this.packetBuffer[3] = (adaptCtrl << 4) | (cc & 0x0F);
if (hasPayload) {
this.continuityCounters.set(pid, (cc + 1) & 0x0F);
}
let offset = 4;
if (adaptationField) {
this.packetBuffer.set(adaptationField, offset);
offset += adaptationField.length;
}
this.packetBuffer.set(payload, offset);
offset += payload.length;
if (offset < TS_PACKET_SIZE) {
this.packetBuffer.fill(0xFF, offset); // stuffing_bytes
}
const startPos = this.writer.getPos();
this.writer.write(this.packetBuffer);
if (this.format._options.onPacket) {
this.format._options.onPacket(this.packetBuffer.slice(), startPos);
}
}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
async onTrackClose(track) {
const release = await this.mutex.acquire();
const trackData = this.trackDatas.find(x => x.track === track);
if (trackData) {
trackData.closed = true;
await this.flushTimestampQueue(trackData, false);
}
if (this.allTracksAreKnown()) {
this.allTracksKnown.resolve();
}
await this.interleavePackets();
release();
}
async finalize() {
const release = await this.mutex.acquire();
this.allTracksKnown.resolve();
for (const trackData of this.trackDatas) {
trackData.closed = true;
await this.flushTimestampQueue(trackData, false);
}
await this.interleavePackets(true);
release();
}
}
// CRC-32 for MPEG-TS (polynomial 0x04C11DB7, initial value 0xFFFFFFFF)
const MPEG_TS_CRC_POLYNOMIAL = 0x04c11db7;
const MPEG_TS_CRC_TABLE = new Uint32Array(256);
for (let n = 0; n < 256; n++) {
let crc = n << 24;
for (let k = 0; k < 8; k++) {
crc = (crc & 0x80000000)
? ((crc << 1) ^ MPEG_TS_CRC_POLYNOMIAL)
: (crc << 1);
}
MPEG_TS_CRC_TABLE[n] = (crc >>> 0) & 0xffffffff;
}
const computeMpegTsCrc32 = (data) => {
let crc = 0xFFFFFFFF;
for (let i = 0; i < data.length; i++) {
const byte = data[i];
crc = ((crc << 8) ^ MPEG_TS_CRC_TABLE[(crc >>> 24) ^ byte]) >>> 0;
}
return crc;
};
const PAT_SECTION = new Uint8Array(16);
{
const view = toDataView(PAT_SECTION);
PAT_SECTION[0] = 0x00; // table_id
view.setUint16(1, 0xB00D, false); // section_syntax_indicator=1, '0', reserved=11, section_length=13
view.setUint16(3, 0x0001, false); // transport_stream_id
PAT_SECTION[5] = 0xC1; // reserved=11, version_number=0, current_next_indicator=1
PAT_SECTION[6] = 0x00; // section_number
PAT_SECTION[7] = 0x00; // last_section_number
view.setUint16(8, 0x0001, false); // program_number
view.setUint16(10, 0xE000 | (PMT_PID & 0x1FFF), false); // reserved=111, program_map_PID
view.setUint32(12, computeMpegTsCrc32(PAT_SECTION.subarray(0, 12)), false); // CRC_32
}
const buildPmt = (trackDatas) => {
let totalEsBytes = 0;
for (const trackData of trackDatas) {
totalEsBytes += 5;
if (trackData.streamType === 129 /* MpegTsStreamType.AC3_SYSTEM_A */) {
totalEsBytes += AC3_REGISTRATION_DESCRIPTOR.length;
}
else if (trackData.streamType === 135 /* MpegTsStreamType.EAC3_SYSTEM_A */) {
totalEsBytes += EAC3_REGISTRATION_DESCRIPTOR.length;
}
}
const sectionLength = 9 + totalEsBytes + 4;
const section = new Uint8Array(3 + sectionLength - 4);
const view = toDataView(section);
section[0] = 0x02; // table_id
// section_syntax_indicator=1, '0', reserved=11, section_length
view.setUint16(1, 0xB000 | (sectionLength & 0x0FFF), false);
view.setUint16(3, 0x0001, false); // program_number
section[5] = 0xC1; // reserved=11, version_number=0, current_next_indicator=1
section[6] = 0x00; // section_number
section[7] = 0x00; // last_section_number
view.setUint16(8, 0xE000 | 0x1FFF, false); // reserved=111, PCR_PID=0x1FFF (none)
view.setUint16(10, 0xF000, false); // reserved=1111, program_info_length=0
let offset = 12;
for (const trackData of trackDatas) {
section[offset++] = trackData.streamType; // stream_type
view.setUint16(offset, 0xE000 | (trackData.pid & 0x1FFF), false); // reserved=111, elementary_PID
offset += 2;
if (trackData.streamType === 129 /* MpegTsStreamType.AC3_SYSTEM_A */) {
view.setUint16(offset, 0xF000 | AC3_REGISTRATION_DESCRIPTOR.length, false);
offset += 2;
section.set(AC3_REGISTRATION_DESCRIPTOR, offset);
offset += AC3_REGISTRATION_DESCRIPTOR.length;
}
else if (trackData.streamType === 135 /* MpegTsStreamType.EAC3_SYSTEM_A */) {
view.setUint16(offset, 0xF000 | EAC3_REGISTRATION_DESCRIPTOR.length, false);
offset += 2;
section.set(EAC3_REGISTRATION_DESCRIPTOR, offset);
offset += EAC3_REGISTRATION_DESCRIPTOR.length;
}
else {
view.setUint16(offset, 0xF000, false); // reserved=1111, ES_info_length=0
offset += 2;
}
}
const crc = computeMpegTsCrc32(section);
const result = new Uint8Array(section.length + 4);
result.set(section, 0);
toDataView(result).setUint32(section.length, crc, false); // CRC_32
return result;
};