123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572 |
- /**
- * mux.js
- *
- * Copyright (c) Brightcove
- * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
- *
- * A stream-based mp2t to mp4 converter. This utility can be used to
- * deliver mp4s to a SourceBuffer on platforms that support native
- * Media Source Extensions.
- */
- 'use strict';
- var Stream = require('../utils/stream.js'),
- CaptionStream = require('./caption-stream'),
- StreamTypes = require('./stream-types'),
- TimestampRolloverStream = require('./timestamp-rollover-stream').TimestampRolloverStream; // object types
- var _TransportPacketStream, _TransportParseStream, _ElementaryStream; // constants
- var MP2T_PACKET_LENGTH = 188,
- // bytes
- SYNC_BYTE = 0x47;
- /**
- * Splits an incoming stream of binary data into MPEG-2 Transport
- * Stream packets.
- */
- _TransportPacketStream = function TransportPacketStream() {
- var buffer = new Uint8Array(MP2T_PACKET_LENGTH),
- bytesInBuffer = 0;
- _TransportPacketStream.prototype.init.call(this); // Deliver new bytes to the stream.
- /**
- * Split a stream of data into M2TS packets
- **/
- this.push = function (bytes) {
- var startIndex = 0,
- endIndex = MP2T_PACKET_LENGTH,
- everything; // If there are bytes remaining from the last segment, prepend them to the
- // bytes that were pushed in
- if (bytesInBuffer) {
- everything = new Uint8Array(bytes.byteLength + bytesInBuffer);
- everything.set(buffer.subarray(0, bytesInBuffer));
- everything.set(bytes, bytesInBuffer);
- bytesInBuffer = 0;
- } else {
- everything = bytes;
- } // While we have enough data for a packet
- while (endIndex < everything.byteLength) {
- // Look for a pair of start and end sync bytes in the data..
- if (everything[startIndex] === SYNC_BYTE && everything[endIndex] === SYNC_BYTE) {
- // We found a packet so emit it and jump one whole packet forward in
- // the stream
- this.trigger('data', everything.subarray(startIndex, endIndex));
- startIndex += MP2T_PACKET_LENGTH;
- endIndex += MP2T_PACKET_LENGTH;
- continue;
- } // If we get here, we have somehow become de-synchronized and we need to step
- // forward one byte at a time until we find a pair of sync bytes that denote
- // a packet
- startIndex++;
- endIndex++;
- } // If there was some data left over at the end of the segment that couldn't
- // possibly be a whole packet, keep it because it might be the start of a packet
- // that continues in the next segment
- if (startIndex < everything.byteLength) {
- buffer.set(everything.subarray(startIndex), 0);
- bytesInBuffer = everything.byteLength - startIndex;
- }
- };
- /**
- * Passes identified M2TS packets to the TransportParseStream to be parsed
- **/
- this.flush = function () {
- // If the buffer contains a whole packet when we are being flushed, emit it
- // and empty the buffer. Otherwise hold onto the data because it may be
- // important for decoding the next segment
- if (bytesInBuffer === MP2T_PACKET_LENGTH && buffer[0] === SYNC_BYTE) {
- this.trigger('data', buffer);
- bytesInBuffer = 0;
- }
- this.trigger('done');
- };
- this.endTimeline = function () {
- this.flush();
- this.trigger('endedtimeline');
- };
- this.reset = function () {
- bytesInBuffer = 0;
- this.trigger('reset');
- };
- };
- _TransportPacketStream.prototype = new Stream();
- /**
- * Accepts an MP2T TransportPacketStream and emits data events with parsed
- * forms of the individual transport stream packets.
- */
- _TransportParseStream = function TransportParseStream() {
- var parsePsi, parsePat, parsePmt, self;
- _TransportParseStream.prototype.init.call(this);
- self = this;
- this.packetsWaitingForPmt = [];
- this.programMapTable = undefined;
- parsePsi = function parsePsi(payload, psi) {
- var offset = 0; // PSI packets may be split into multiple sections and those
- // sections may be split into multiple packets. If a PSI
- // section starts in this packet, the payload_unit_start_indicator
- // will be true and the first byte of the payload will indicate
- // the offset from the current position to the start of the
- // section.
- if (psi.payloadUnitStartIndicator) {
- offset += payload[offset] + 1;
- }
- if (psi.type === 'pat') {
- parsePat(payload.subarray(offset), psi);
- } else {
- parsePmt(payload.subarray(offset), psi);
- }
- };
- parsePat = function parsePat(payload, pat) {
- pat.section_number = payload[7]; // eslint-disable-line camelcase
- pat.last_section_number = payload[8]; // eslint-disable-line camelcase
- // skip the PSI header and parse the first PMT entry
- self.pmtPid = (payload[10] & 0x1F) << 8 | payload[11];
- pat.pmtPid = self.pmtPid;
- };
- /**
- * Parse out the relevant fields of a Program Map Table (PMT).
- * @param payload {Uint8Array} the PMT-specific portion of an MP2T
- * packet. The first byte in this array should be the table_id
- * field.
- * @param pmt {object} the object that should be decorated with
- * fields parsed from the PMT.
- */
- parsePmt = function parsePmt(payload, pmt) {
- var sectionLength, tableEnd, programInfoLength, offset; // PMTs can be sent ahead of the time when they should actually
- // take effect. We don't believe this should ever be the case
- // for HLS but we'll ignore "forward" PMT declarations if we see
- // them. Future PMT declarations have the current_next_indicator
- // set to zero.
- if (!(payload[5] & 0x01)) {
- return;
- } // overwrite any existing program map table
- self.programMapTable = {
- video: null,
- audio: null,
- 'timed-metadata': {}
- }; // the mapping table ends at the end of the current section
- sectionLength = (payload[1] & 0x0f) << 8 | payload[2];
- tableEnd = 3 + sectionLength - 4; // to determine where the table is, we have to figure out how
- // long the program info descriptors are
- programInfoLength = (payload[10] & 0x0f) << 8 | payload[11]; // advance the offset to the first entry in the mapping table
- offset = 12 + programInfoLength;
- while (offset < tableEnd) {
- var streamType = payload[offset];
- var pid = (payload[offset + 1] & 0x1F) << 8 | payload[offset + 2]; // only map a single elementary_pid for audio and video stream types
- // TODO: should this be done for metadata too? for now maintain behavior of
- // multiple metadata streams
- if (streamType === StreamTypes.H264_STREAM_TYPE && self.programMapTable.video === null) {
- self.programMapTable.video = pid;
- } else if (streamType === StreamTypes.ADTS_STREAM_TYPE && self.programMapTable.audio === null) {
- self.programMapTable.audio = pid;
- } else if (streamType === StreamTypes.METADATA_STREAM_TYPE) {
- // map pid to stream type for metadata streams
- self.programMapTable['timed-metadata'][pid] = streamType;
- } // move to the next table entry
- // skip past the elementary stream descriptors, if present
- offset += ((payload[offset + 3] & 0x0F) << 8 | payload[offset + 4]) + 5;
- } // record the map on the packet as well
- pmt.programMapTable = self.programMapTable;
- };
- /**
- * Deliver a new MP2T packet to the next stream in the pipeline.
- */
- this.push = function (packet) {
- var result = {},
- offset = 4;
- result.payloadUnitStartIndicator = !!(packet[1] & 0x40); // pid is a 13-bit field starting at the last bit of packet[1]
- result.pid = packet[1] & 0x1f;
- result.pid <<= 8;
- result.pid |= packet[2]; // if an adaption field is present, its length is specified by the
- // fifth byte of the TS packet header. The adaptation field is
- // used to add stuffing to PES packets that don't fill a complete
- // TS packet, and to specify some forms of timing and control data
- // that we do not currently use.
- if ((packet[3] & 0x30) >>> 4 > 0x01) {
- offset += packet[offset] + 1;
- } // parse the rest of the packet based on the type
- if (result.pid === 0) {
- result.type = 'pat';
- parsePsi(packet.subarray(offset), result);
- this.trigger('data', result);
- } else if (result.pid === this.pmtPid) {
- result.type = 'pmt';
- parsePsi(packet.subarray(offset), result);
- this.trigger('data', result); // if there are any packets waiting for a PMT to be found, process them now
- while (this.packetsWaitingForPmt.length) {
- this.processPes_.apply(this, this.packetsWaitingForPmt.shift());
- }
- } else if (this.programMapTable === undefined) {
- // When we have not seen a PMT yet, defer further processing of
- // PES packets until one has been parsed
- this.packetsWaitingForPmt.push([packet, offset, result]);
- } else {
- this.processPes_(packet, offset, result);
- }
- };
- this.processPes_ = function (packet, offset, result) {
- // set the appropriate stream type
- if (result.pid === this.programMapTable.video) {
- result.streamType = StreamTypes.H264_STREAM_TYPE;
- } else if (result.pid === this.programMapTable.audio) {
- result.streamType = StreamTypes.ADTS_STREAM_TYPE;
- } else {
- // if not video or audio, it is timed-metadata or unknown
- // if unknown, streamType will be undefined
- result.streamType = this.programMapTable['timed-metadata'][result.pid];
- }
- result.type = 'pes';
- result.data = packet.subarray(offset);
- this.trigger('data', result);
- };
- };
- _TransportParseStream.prototype = new Stream();
- _TransportParseStream.STREAM_TYPES = {
- h264: 0x1b,
- adts: 0x0f
- };
- /**
- * Reconsistutes program elementary stream (PES) packets from parsed
- * transport stream packets. That is, if you pipe an
- * mp2t.TransportParseStream into a mp2t.ElementaryStream, the output
- * events will be events which capture the bytes for individual PES
- * packets plus relevant metadata that has been extracted from the
- * container.
- */
- _ElementaryStream = function ElementaryStream() {
- var self = this,
- segmentHadPmt = false,
- // PES packet fragments
- video = {
- data: [],
- size: 0
- },
- audio = {
- data: [],
- size: 0
- },
- timedMetadata = {
- data: [],
- size: 0
- },
- programMapTable,
- parsePes = function parsePes(payload, pes) {
- var ptsDtsFlags;
- var startPrefix = payload[0] << 16 | payload[1] << 8 | payload[2]; // default to an empty array
- pes.data = new Uint8Array(); // In certain live streams, the start of a TS fragment has ts packets
- // that are frame data that is continuing from the previous fragment. This
- // is to check that the pes data is the start of a new pes payload
- if (startPrefix !== 1) {
- return;
- } // get the packet length, this will be 0 for video
- pes.packetLength = 6 + (payload[4] << 8 | payload[5]); // find out if this packets starts a new keyframe
- pes.dataAlignmentIndicator = (payload[6] & 0x04) !== 0; // PES packets may be annotated with a PTS value, or a PTS value
- // and a DTS value. Determine what combination of values is
- // available to work with.
- ptsDtsFlags = payload[7]; // PTS and DTS are normally stored as a 33-bit number. Javascript
- // performs all bitwise operations on 32-bit integers but javascript
- // supports a much greater range (52-bits) of integer using standard
- // mathematical operations.
- // We construct a 31-bit value using bitwise operators over the 31
- // most significant bits and then multiply by 4 (equal to a left-shift
- // of 2) before we add the final 2 least significant bits of the
- // timestamp (equal to an OR.)
- if (ptsDtsFlags & 0xC0) {
- // the PTS and DTS are not written out directly. For information
- // on how they are encoded, see
- // http://dvd.sourceforge.net/dvdinfo/pes-hdr.html
- pes.pts = (payload[9] & 0x0E) << 27 | (payload[10] & 0xFF) << 20 | (payload[11] & 0xFE) << 12 | (payload[12] & 0xFF) << 5 | (payload[13] & 0xFE) >>> 3;
- pes.pts *= 4; // Left shift by 2
- pes.pts += (payload[13] & 0x06) >>> 1; // OR by the two LSBs
- pes.dts = pes.pts;
- if (ptsDtsFlags & 0x40) {
- pes.dts = (payload[14] & 0x0E) << 27 | (payload[15] & 0xFF) << 20 | (payload[16] & 0xFE) << 12 | (payload[17] & 0xFF) << 5 | (payload[18] & 0xFE) >>> 3;
- pes.dts *= 4; // Left shift by 2
- pes.dts += (payload[18] & 0x06) >>> 1; // OR by the two LSBs
- }
- } // the data section starts immediately after the PES header.
- // pes_header_data_length specifies the number of header bytes
- // that follow the last byte of the field.
- pes.data = payload.subarray(9 + payload[8]);
- },
- /**
- * Pass completely parsed PES packets to the next stream in the pipeline
- **/
- flushStream = function flushStream(stream, type, forceFlush) {
- var packetData = new Uint8Array(stream.size),
- event = {
- type: type
- },
- i = 0,
- offset = 0,
- packetFlushable = false,
- fragment; // do nothing if there is not enough buffered data for a complete
- // PES header
- if (!stream.data.length || stream.size < 9) {
- return;
- }
- event.trackId = stream.data[0].pid; // reassemble the packet
- for (i = 0; i < stream.data.length; i++) {
- fragment = stream.data[i];
- packetData.set(fragment.data, offset);
- offset += fragment.data.byteLength;
- } // parse assembled packet's PES header
- parsePes(packetData, event); // non-video PES packets MUST have a non-zero PES_packet_length
- // check that there is enough stream data to fill the packet
- packetFlushable = type === 'video' || event.packetLength <= stream.size; // flush pending packets if the conditions are right
- if (forceFlush || packetFlushable) {
- stream.size = 0;
- stream.data.length = 0;
- } // only emit packets that are complete. this is to avoid assembling
- // incomplete PES packets due to poor segmentation
- if (packetFlushable) {
- self.trigger('data', event);
- }
- };
- _ElementaryStream.prototype.init.call(this);
- /**
- * Identifies M2TS packet types and parses PES packets using metadata
- * parsed from the PMT
- **/
- this.push = function (data) {
- ({
- pat: function pat() {// we have to wait for the PMT to arrive as well before we
- // have any meaningful metadata
- },
- pes: function pes() {
- var stream, streamType;
- switch (data.streamType) {
- case StreamTypes.H264_STREAM_TYPE:
- stream = video;
- streamType = 'video';
- break;
- case StreamTypes.ADTS_STREAM_TYPE:
- stream = audio;
- streamType = 'audio';
- break;
- case StreamTypes.METADATA_STREAM_TYPE:
- stream = timedMetadata;
- streamType = 'timed-metadata';
- break;
- default:
- // ignore unknown stream types
- return;
- } // if a new packet is starting, we can flush the completed
- // packet
- if (data.payloadUnitStartIndicator) {
- flushStream(stream, streamType, true);
- } // buffer this fragment until we are sure we've received the
- // complete payload
- stream.data.push(data);
- stream.size += data.data.byteLength;
- },
- pmt: function pmt() {
- var event = {
- type: 'metadata',
- tracks: []
- };
- programMapTable = data.programMapTable; // translate audio and video streams to tracks
- if (programMapTable.video !== null) {
- event.tracks.push({
- timelineStartInfo: {
- baseMediaDecodeTime: 0
- },
- id: +programMapTable.video,
- codec: 'avc',
- type: 'video'
- });
- }
- if (programMapTable.audio !== null) {
- event.tracks.push({
- timelineStartInfo: {
- baseMediaDecodeTime: 0
- },
- id: +programMapTable.audio,
- codec: 'adts',
- type: 'audio'
- });
- }
- segmentHadPmt = true;
- self.trigger('data', event);
- }
- })[data.type]();
- };
- this.reset = function () {
- video.size = 0;
- video.data.length = 0;
- audio.size = 0;
- audio.data.length = 0;
- this.trigger('reset');
- };
- /**
- * Flush any remaining input. Video PES packets may be of variable
- * length. Normally, the start of a new video packet can trigger the
- * finalization of the previous packet. That is not possible if no
- * more video is forthcoming, however. In that case, some other
- * mechanism (like the end of the file) has to be employed. When it is
- * clear that no additional data is forthcoming, calling this method
- * will flush the buffered packets.
- */
- this.flushStreams_ = function () {
- // !!THIS ORDER IS IMPORTANT!!
- // video first then audio
- flushStream(video, 'video');
- flushStream(audio, 'audio');
- flushStream(timedMetadata, 'timed-metadata');
- };
- this.flush = function () {
- // if on flush we haven't had a pmt emitted
- // and we have a pmt to emit. emit the pmt
- // so that we trigger a trackinfo downstream.
- if (!segmentHadPmt && programMapTable) {
- var pmt = {
- type: 'metadata',
- tracks: []
- }; // translate audio and video streams to tracks
- if (programMapTable.video !== null) {
- pmt.tracks.push({
- timelineStartInfo: {
- baseMediaDecodeTime: 0
- },
- id: +programMapTable.video,
- codec: 'avc',
- type: 'video'
- });
- }
- if (programMapTable.audio !== null) {
- pmt.tracks.push({
- timelineStartInfo: {
- baseMediaDecodeTime: 0
- },
- id: +programMapTable.audio,
- codec: 'adts',
- type: 'audio'
- });
- }
- self.trigger('data', pmt);
- }
- segmentHadPmt = false;
- this.flushStreams_();
- this.trigger('done');
- };
- };
- _ElementaryStream.prototype = new Stream();
- var m2ts = {
- PAT_PID: 0x0000,
- MP2T_PACKET_LENGTH: MP2T_PACKET_LENGTH,
- TransportPacketStream: _TransportPacketStream,
- TransportParseStream: _TransportParseStream,
- ElementaryStream: _ElementaryStream,
- TimestampRolloverStream: TimestampRolloverStream,
- CaptionStream: CaptionStream.CaptionStream,
- Cea608Stream: CaptionStream.Cea608Stream,
- Cea708Stream: CaptionStream.Cea708Stream,
- MetadataStream: require('./metadata-stream')
- };
- for (var type in StreamTypes) {
- if (StreamTypes.hasOwnProperty(type)) {
- m2ts[type] = StreamTypes[type];
- }
- }
- module.exports = m2ts;
|