UNPKG

hjplayer

Version:

hjplayer, a HTML5 Player, can play flv and hls by Media Source Extension, based on typescript;

1,573 lines (1,417 loc) 63.7 kB
/** * highly optimized TS demuxer: * parse PAT, PMT * extract PES packet from audio and video PIDs * extract AVC/H264 NAL units and AAC/ADTS samples from PES packet * trigger the remuxer upon parsing completion * it also tries to workaround as best as it can audio codec switch (HE-AAC to AAC and vice versa), without having to restart the MediaSource. * it also controls the remuxing process : * upon discontinuity or level switch detection, it will also notifies the remuxer so that it can reset its state. */ import EventEmitter from 'eventemitter3'; import logger from '../../../Utils/Logger'; import * as ADTS from './adts'; import MpegAudio from './mpegaudio'; import Events from '../Events/index'; import ExpGolomb from './exp-golomb'; import { ErrorTypes, ErrorDetails } from '../errors'; import { track, pesData, parsedPesData, avcSample, NALUnit, TSTextTrack, TSId3Track, TSAudioTrack, TSVideoTrack, SampleLike, typeSupported, agentInfo } from '../TSCodecInterface'; import createAVCSample from '../TSUtils/createAVCSample'; import MP4Remuxer from '../Remuxer/mp4-remuxer'; import { videoTrack } from '../../FLVCodec/Interface'; // We are using fixed track IDs for driving the MP4 remuxer // instead of following the TS PIDs. // There is no reason not to do this and some browsers/SourceBuffer-demuxers // may not like if there are TrackID "switches" // See https://github.com/video-dev/hls.js/issues/1331 // Here we are mapping our internal track types to constant MP4 track IDs // With MSE currently one can only have one track of each, and we are muxing // whatever video/audio rendition in them. const RemuxerTrackIdConfig: Record<string, number> = { video: 1, audio: 2, id3: 3, text: 4 }; class TSDemuxer { /** * 事件中心 */ private _emitter: EventEmitter /** * 设置 */ private config: Record<string, any> /** * 浏览器支持回放类型 */ private typeSupported: typeSupported /** * 浏览器代理信息 */ agentInfo: agentInfo /** * 上一个加载的fragment */ private frag: any /** * TS 转码器 */ private remuxer: MP4Remuxer /** * 待查明 */ private sampleAes: any /** * 上报错误的方法 */ private _onError: Function | null /** * 媒体信息 */ private _mediaInfo: Record<string, any> /** * 上报媒体信息的方法 */ private _onMediaInfo: Function | null /** * 上报metaData的方法 */ private _onMetaDataArrived: Function | null /** * 上报 ScriptData 的方法 (TS解码器没用); */ private _onScriptDataArrived: Function | null /** * 上报 TrackMetadata 的方法 */ private _onTrackMetadata: Function | null /** * 上报数据的方法 */ private _onDataAvailable: Function | null /** * fragment的序列号 */ private sequenceNumber: number /** * 时间基础值 */ private _timestampBase: number /** * 是否对有声音的标识进行过覆盖 */ private _hasAudioFlagOverrided: boolean /** * 视频是否有声音 */ private _hasAudio: boolean /** * 是否对有视频的标识进行过覆盖 */ private _hasVideoFlagOverrided: boolean /** * 视频有视频 */ private _hasVideo: boolean /** * 媒体文件长度 */ private _duration: number /** * 是否对duration进行过覆盖 */ private _durationOverrided: boolean /** * fragment 的 ID, 等同于 sequenceNumber */ private id: any /** * PMT表格是否已经解析 */ private pmtParsed: boolean /** * PMT的ID是多少 */ private _pmtId: number /** * AVC Track */ private _avcTrack: TSVideoTrack /** * audio track */ private _audioTrack: TSAudioTrack /** * id3 track */ private _id3Track: TSId3Track /** * txt track */ private _txtTrack: TSTextTrack /** * 溢出的AAC数据 */ aacOverFlow: Uint8Array | null /** * 音频最后的PTS时间 */ aacLastPTS: number | undefined /** * AVC Sample 的存储单元 */ avcSample: avcSample | null /** * 音频解码器类型 */ audioCodec: string | undefined /** * 视频解码器类型 */ videoCodec: string | undefined /** * 是否连续 */ contiguous: boolean /** * 初始的PTS */ _initPTS: undefined /** * 初始的DTS */ _initDTS: undefined constructor( emitter: EventEmitter, config: Record<string, any>, typeSupported: typeSupported, agentInfo: agentInfo ) { this._emitter = emitter; this.config = config; this.typeSupported = typeSupported; this.agentInfo = agentInfo; this.frag = null; this.remuxer = new MP4Remuxer(this._emitter, config, typeSupported, agentInfo); this.sampleAes = null; this._onError = null; this._mediaInfo = Object.create(null); this._onMediaInfo = null; this._onMetaDataArrived = null; this._onScriptDataArrived = null; this._onTrackMetadata = null; this._onDataAvailable = null; this.sequenceNumber = 0; this._timestampBase = 0; this._avcTrack = TSDemuxer.createVideoTrack(); this._audioTrack = TSDemuxer.createAudioTrack(0); this._id3Track = TSDemuxer.createId3Track(); this._txtTrack = TSDemuxer.createTextTrack(); this._hasAudioFlagOverrided = false; this._hasAudio = false; this._hasVideoFlagOverrided = false; this._hasVideo = false; this._durationOverrided = false; this._duration = 0; this.pmtParsed = false; this._pmtId = -1; this.contiguous = false; this.avcSample = null; this.aacOverFlow = null; this.aacLastPTS = undefined; } /** * 文件标签 */ static Tag: 'TSDemuxer' // prototype: function(type: string, metadata: any): void get onTrackMetadata() { return this._onTrackMetadata; } set onTrackMetadata(callback) { this._onTrackMetadata = callback; } // prototype: function(mediaInfo: MediaInfo): void get onMediaInfo() { return this._onMediaInfo; } set onMediaInfo(callback) { this._onMediaInfo = callback; } get onMetaDataArrived() { return this._onMetaDataArrived; } set onMetaDataArrived(callback) { this._onMetaDataArrived = callback; } get onScriptDataArrived() { return this._onScriptDataArrived; } set onScriptDataArrived(callback) { this._onScriptDataArrived = callback; } // prototype: function(type: number, info: string): void get onError() { return this._onError; } set onError(callback) { this._onError = callback; } // prototype: function(videoTrack: any, audioTrack: any): void get onDataAvailable() { return this._onDataAvailable; } set onDataAvailable(callback) { this._onDataAvailable = callback; } // timestamp base for output samples, must be in milliseconds get timestampBase() { return this._timestampBase; } set timestampBase(base) { this._timestampBase = base; } get overridedDuration() { return this._duration; } // Force-override media duration. Must be in milliseconds, int32 set overridedDuration(duration) { this._durationOverrided = true; this._duration = duration; this._mediaInfo.duration = duration; } // Force-override audio track present flag, boolean set overridedHasAudio(hasAudio: boolean) { this._hasAudioFlagOverrided = true; this._hasAudio = hasAudio; this._mediaInfo.hasAudio = hasAudio; } // Force-override video track present flag, boolean set overridedHasVideo(hasVideo: boolean) { this._hasVideoFlagOverrided = true; this._hasVideo = hasVideo; this._mediaInfo.hasVideo = hasVideo; } on(event: string, listener: EventEmitter.ListenerFn) { this._emitter.on(event, listener); } once(event: string, listener: EventEmitter.ListenerFn) { this._emitter.once(event, listener); } off(event: string, listener: EventEmitter.ListenerFn) { this._emitter.off(event, listener); } // TODO 和 push 结合在一起 bindDataSource(loader: any) { loader.onDataArrival = this.parseChunks.bind(this); return this; } /** * 解析数据 * @param { Uint8Aarray } chunks loader返回的额数据 * @param { Number } byteStart 累计的bytelength * @param { * } extraData fragment-loader 返回的数据 */ parseChunks(chunks: Uint8Array, extraData: any) { /** * 发送过来数据块所属的fragment */ const frag = extraData.fragCurrent; /** * 时间偏移 */ const timeOffset = Number.isFinite(frag.startPTS) ? frag.startPTS : frag.start; /** * 解码数据 */ const { decryptdata } = frag; /** * 上一个frag */ const lastFrag = this.frag; /** * 是否不连续 */ const discontinuity = !(lastFrag && frag.cc === lastFrag.cc); /** * 是不是变了level */ const trackSwitch = !(lastFrag && frag.level === lastFrag.level); /** * SN号是否连续的 */ const nextSN = lastFrag && frag.sn === lastFrag.sn + 1; /** * fragment是否是相邻的 */ const contiguous = !trackSwitch && nextSN; if(discontinuity) { logger.debug(TSDemuxer.Tag, `${this.id}:discontinuity detected`); } if(trackSwitch) { logger.debug(TSDemuxer.Tag, `${this.id}:switch detected`); } this.frag = frag; // 暂时不解密 this.pushDecrypted( chunks, decryptdata, new Uint8Array(extraData.initSegmentData), extraData.audioCodec, extraData.videoCodec, timeOffset, discontinuity, trackSwitch, contiguous, extraData.totalduration, extraData.accurateTimeOffset, extraData.defaultInitPTS ); } /** * 推入已经解密过的数据 * @param data 要解析的数据块 * @param decryptdata 解密相关参数 * @param initSegment 初始化片段数据 * @param audioCodec 音频编码格式 * @param videoCodec 视频编码格式 * @param timeOffset 时间偏移 * @param discontinuity 是否不连续 * @param trackSwitch 是都切换了level * @param contiguous 是否连续 * @param duration 媒体时长 * @param accurateTimeOffset 是否为精确的时间偏移 * @param defaultInitPTS 默认初始的PTS */ pushDecrypted( data: Uint8Array, decryptdata: any, initSegment: Uint8Array, audioCodec: string | undefined, videoCodec: string | undefined, timeOffset: number, discontinuity: boolean, trackSwitch: boolean, contiguous: boolean, duration: number, accurateTimeOffset: boolean, defaultInitPTS: number | undefined ) { // 如果过不连续或者变更了level 则重新生成初始化segment if(discontinuity || trackSwitch) { this.resetInitSegment(initSegment, audioCodec, videoCodec, duration); this.remuxer.resetInitSegment(); } // 如果过不连续 重置 时间基础值 if(discontinuity) { this.resetTimeStamp(defaultInitPTS); this.remuxer.resetTimeStamp(defaultInitPTS); } this.append(new Uint8Array(data), timeOffset, contiguous, accurateTimeOffset); } /** * 判断Uint8Array是否为TS数据 * @param data 侦测的数据 */ static probe(data: Uint8Array) { const syncOffset = TSDemuxer._syncOffset(data); if(syncOffset < 0) { return false; } if(syncOffset) { logger.warn( TSDemuxer.Tag, `MPEG2-TS detected but first sync word found @ offset ${syncOffset}, junk ahead ?` ); } return true; } /** * 查找同步位所在位置, 如果没有找到返回 -1, 找到就返回找到的位置, 一般为 0 * @param data 要查找的数据 */ static _syncOffset(data: Uint8Array): number { // scan 1000 first bytes const scanwindow = Math.min(1000, data.length - 3 * 188); let i = 0; while(i < scanwindow) { // a TS fragment should contain at least 3 TS packets, a PAT, a PMT, and one PID, each starting with 0x47 if(data[i] === 0x47 && data[i + 188] === 0x47 && data[i + 2 * 188] === 0x47) { return i; } i++; } return -1; } /** * 创建一个 track 模板, 用于 转码时放数据 * @param {string} type 'audio' | 'video' | 'id3' | 'text' * @param {number} duration * @return { track } */ static createTrack(type: string, duration: number): track { return { container: type === 'video' || type === 'audio' ? 'video/mp2t' : undefined, type, id: RemuxerTrackIdConfig[type], pid: -1, inputTimeScale: 90000, sequenceNumber: 0, samples: [], dropped: type === 'video' ? 0 : undefined, isAAC: type === 'audio' ? true : undefined, duration: type === 'audio' ? duration : undefined }; } /** * 创建视频序列 */ static createVideoTrack(): TSVideoTrack { return { container: 'video/mp2t', type: 'video', id: RemuxerTrackIdConfig.video, pid: -1, inputTimeScale: 90000, sequenceNumber: 0, samples: [], dropped: 0, isAAC: undefined, duration: undefined }; } /** * 创建音频序列 * @param duration 时长 */ static createAudioTrack(duration: number): TSAudioTrack { return { container: 'video/mp2t', type: 'audio', id: RemuxerTrackIdConfig.audio, pid: -1, inputTimeScale: 90000, sequenceNumber: 0, samples: [], dropped: undefined, isAAC: true, duration }; } /** * 创建Id3序列 */ static createId3Track(): TSId3Track { return { container: undefined, type: 'id3', id: RemuxerTrackIdConfig.id3, pid: -1, inputTimeScale: 90000, sequenceNumber: 0, samples: [], dropped: undefined, isAAC: undefined, duration: undefined }; } /** * 创建文本序列 */ static createTextTrack(): TSTextTrack { return { container: undefined, type: 'text', id: RemuxerTrackIdConfig.text, pid: -1, inputTimeScale: 90000, sequenceNumber: 0, samples: [], dropped: undefined, isAAC: undefined, duration: undefined }; } /** * Initializes a new init segment on the demuxer/remuxer interface. Needed for discontinuities/track-switches (or at stream start) * Resets all internal track instances of the demuxer. * 重新初始化初始化片段, 初始化各个track等相关参数, 会在 frament不连续, 切换level或者刚开始的时候调用 * @override Implements generic demuxing/remuxing interface (see DemuxerInline) * @param { Uint8Array } initSegment * @param { string } audioCodec * @param { string } videoCodec * @param { number } duration (in TS timescale = 90kHz) */ resetInitSegment( initSegment: Uint8Array, audioCodec: string | undefined, videoCodec: string | undefined, duration: number ) { this.pmtParsed = false; this._pmtId = -1; this._avcTrack = TSDemuxer.createVideoTrack(); this._audioTrack = TSDemuxer.createAudioTrack(duration); this._id3Track = TSDemuxer.createId3Track(); this._txtTrack = TSDemuxer.createTextTrack(); // flush any partial content this.aacOverFlow = null; this.aacLastPTS = undefined; this.avcSample = null; this.audioCodec = audioCodec; this.videoCodec = videoCodec; this._duration = duration; } /** * 重置时间基准值 */ resetTimeStamp(defaultInitPTS: number | undefined) {} /** * 重置媒体信息 */ resetMediaInfo() { this._mediaInfo = Object.create(null); } // feed incoming data to the front of the parsing pipeline /** * 想解码器添加数据 * @param data 要解码的数据 * @param timeOffset 时间偏移值 * @param contiguous 是否连续 * @param accurateTimeOffset 是否是精确的时间偏移 */ append(data: Uint8Array, timeOffset: number, contiguous: boolean, accurateTimeOffset: boolean) { let start; /** * 要解析数据的长度 */ let len = data.length; /** * ISOIEC13818-1 规定 负载单元起始标示符 为 true 时代表该包中含有 PES packets (refer to 2.4.3.6) or PSI data (refer to 2.4.4). */ let stt = false; /** * ts packet id 包ID */ let pid; /** * adaptation_field_control 自适应域标志 */ let atf; /** * 偏移量 */ let offset; /** * 经 parsePES 方法解析后的 pes 包数据 */ let pes; /** * 是否为未知的PID */ let unknownPIDs = false; this.contiguous = contiguous; let { pmtParsed } = this; const avcTrack = this._avcTrack; const audioTrack = this._audioTrack; const id3Track = this._id3Track; let avcId = avcTrack ? avcTrack.pid : -1; let audioId = audioTrack ? audioTrack.pid : -1; let id3Id = id3Track ? id3Track.pid : -1; let pmtId = this._pmtId; let avcData: pesData | undefined = avcTrack.pesData; let audioData: pesData | undefined = audioTrack.pesData; let id3Data: pesData | undefined = id3Track.pesData; const parsePAT = this._parsePAT; const parsePMT = this._parsePMT; const parsePES = this._parsePES; const parseAVCPES = this._parseAVCPES.bind(this); const parseAACPES = this._parseAACPES.bind(this); const parseMPEGPES = this._parseMPEGPES.bind(this); const parseID3PES = this._parseID3PES.bind(this); const syncOffset = TSDemuxer._syncOffset(data); // don't parse last TS packet if incomplete len -= (len + syncOffset) % 188; // loop through TS packets for(start = syncOffset; start < len; start += 188) { if(data[start] === 0x47) { stt = !!(data[start + 1] & 0x40); // pid is a 13-bit field starting at the last bit of TS[1] pid = ((data[start + 1] & 0x1f) << 8) + data[start + 2]; atf = (data[start + 3] & 0x30) >> 4; // if an adaption field is present, its length is specified by the fifth byte of the TS packet header. if(atf > 1) { offset = start + 5 + data[start + 4]; // continue if there is only adaptation field if(offset === start + 188) { continue; } } else { offset = start + 4; } switch(pid) { case avcId: if(stt) { avcData && (pes = parsePES(avcData)); if(avcData && pes && pes.pts !== undefined) { parseAVCPES(pes, false); } avcData = { data: [], size: 0 }; } if(avcData) { avcData.data.push(data.subarray(offset, start + 188)); avcData.size += start + 188 - offset; } break; case audioId: if(stt) { audioData && (pes = parsePES(audioData)); if(audioData && pes && pes.pts !== undefined) { if(audioTrack.isAAC) { parseAACPES(pes); } else { parseMPEGPES(pes); } } audioData = { data: [], size: 0 }; } if(audioData) { audioData.data.push(data.subarray(offset, start + 188)); audioData.size += start + 188 - offset; } break; case id3Id: if(stt) { id3Data && (pes = parsePES(id3Data)); if(id3Data && pes && pes.pts !== undefined) { parseID3PES(pes); } id3Data = { data: [], size: 0 }; } if(id3Data) { id3Data.data.push(data.subarray(offset, start + 188)); id3Data.size += start + 188 - offset; } break; case 0: if(stt) { offset += data[offset] + 1; } this._pmtId = parsePAT(data, offset); pmtId = this._pmtId; break; // eslint-disable-next-line no-case-declarations case pmtId: { if(stt) { offset += data[offset] + 1; } const parsedPIDs = parsePMT( data, offset, this.typeSupported.mpeg === true || this.typeSupported.mp3 === true, this.sampleAes !== null ); // only update track id if track PID found while parsing PMT // this is to avoid resetting the PID to -1 in case // track PID transiently disappears from the stream // this could happen in case of transient missing audio samples for example // NOTE this is only the PID of the track as found in TS, // but we are not using this for MP4 track IDs. avcId = parsedPIDs.avc; if(avcId > 0) { avcTrack.pid = avcId; } audioId = parsedPIDs.audio; if(audioId > 0) { audioTrack.pid = audioId; audioTrack.isAAC = parsedPIDs.isAAC; } id3Id = parsedPIDs.id3; if(id3Id > 0) { id3Track.pid = id3Id; } if(unknownPIDs && !pmtParsed) { logger.log(TSDemuxer.Tag, 'reparse from beginning'); unknownPIDs = false; // we set it to -188, the += 188 in the for loop will reset start to 0 start = syncOffset - 188; } this.pmtParsed = true; pmtParsed = true; break; } case 17: case 0x1fff: break; default: unknownPIDs = true; break; } } else { this._emitter.emit(Events.ERROR, { type: ErrorTypes.MEDIA_ERROR, details: ErrorDetails.FRAG_PARSING_ERROR, fatal: false, code: -2, reason: 'TS packet did not start with 0x47' }); } } // try to parse last PES packets avcData && (pes = parsePES(avcData)); if(avcData && pes && pes.pts !== undefined) { parseAVCPES(pes, true); avcTrack.pesData = undefined; } else { // either avcData null or PES truncated, keep it for next frag parsing avcTrack.pesData = avcData; } audioData && (pes = parsePES(audioData)); if(audioData && pes && pes.pts !== undefined) { if(audioTrack.isAAC) { parseAACPES(pes, true); } else { parseMPEGPES(pes); } audioTrack.pesData = undefined; } else { if(audioData && audioData.size) { logger.log( TSDemuxer.Tag, 'last AAC PES packet truncated,might overlap between fragments' ); } // either audioData null or PES truncated, keep it for next frag parsing audioTrack.pesData = audioData; } id3Data && (pes = parsePES(id3Data)); if(id3Data && pes && pes.pts !== undefined) { parseID3PES(pes); id3Track.pesData = undefined; } else { // either id3Data null or PES truncated, keep it for next frag parsing id3Track.pesData = id3Data; } this.sequenceNumber += 1; const { sequenceNumber } = this; audioTrack.sequenceNumber = sequenceNumber; avcTrack.sequenceNumber = sequenceNumber; this._parseMediaInfo(audioTrack, avcTrack); this._createMetadata(audioTrack, avcTrack); this.remuxer.remux( audioTrack, avcTrack, id3Track, this._txtTrack, timeOffset, contiguous, accurateTimeOffset ); } /** * 销毁功能 */ destroy() { this._initDTS = undefined; this._initPTS = undefined; this._duration = 0; this.sequenceNumber = 0; this._emitter.removeAllListeners(); delete this._emitter; delete this.config; delete this.typeSupported; delete this.agentInfo; this.frag = null; delete this.remuxer; this.sampleAes = null; this._onError = null; delete this._mediaInfo; this._onMediaInfo = null; this._onMetaDataArrived = null; this._onScriptDataArrived = null; this._onTrackMetadata = null; this._onDataAvailable = null; this.sequenceNumber = 0; this._timestampBase = 0; delete this._avcTrack; delete this._audioTrack; delete this._id3Track; delete this._txtTrack; this._hasAudioFlagOverrided = false; this._hasAudio = false; this._hasVideoFlagOverrided = false; this._hasVideo = false; this._durationOverrided = false; this._duration = 0; this.pmtParsed = false; this._pmtId = -1; this.contiguous = false; this.avcSample = null; this.aacOverFlow = null; this.aacLastPTS = undefined; } /** * 解析 PAT表, 获取 PMT值 * @param data 要解析的Uint8Array 数据 * @param offset 偏移量 */ _parsePAT(data: Uint8Array, offset: number): number { // skip the PSI header and parse the first PMT entry return ((data[offset + 10] & 0x1f) << 8) | data[offset + 11]; // logger.log('PMT PID:' + this._pmtId); } /** * * @param data 解析的UInt8Array * @param offset 偏移数 * @param mpegSupported 浏览器是否支持MPEG回放 * @param isSampleAes 是否有解密程序 */ _parsePMT(data: Uint8Array, offset: number, mpegSupported: boolean, isSampleAes: boolean) { let pid; const result = { audio: -1, avc: -1, id3: -1, isAAC: true }; const sectionLength = ((data[offset + 1] & 0x0f) << 8) | data[offset + 2]; const tableEnd = offset + 3 + sectionLength - 4; // to determine where the table is, we have to figure out how // long the program info descriptors are const programInfoLength = ((data[offset + 10] & 0x0f) << 8) | data[offset + 11]; // advance the offset to the first entry in the mapping table offset += 12 + programInfoLength; while(offset < tableEnd) { pid = ((data[offset + 1] & 0x1f) << 8) | data[offset + 2]; switch(data[offset]) { case 0xcf: // SAMPLE-AES AAC if(!isSampleAes) { logger.log(TSDemuxer.Tag, `unkown stream type:${data[offset]}`); break; } /* falls through */ // ISO/IEC 13818-7 ADTS AAC (MPEG-2 lower bit-rate audio) // eslint-disable-next-line no-fallthrough case 0x0f: // logger.log('AAC PID:' + pid); if(result.audio === -1) { result.audio = pid; } break; // Packetized metadata (ID3) case 0x15: // logger.log('ID3 PID:' + pid); if(result.id3 === -1) { result.id3 = pid; } break; case 0xdb: // SAMPLE-AES AVC if(!isSampleAes) { logger.log(TSDemuxer.Tag, `unkown stream type:${data[offset]}`); break; } /* falls through */ // ITU-T Rec. H.264 and ISO/IEC 14496-10 (lower bit-rate video) // eslint-disable-next-line no-fallthrough case 0x1b: // logger.log('AVC PID:' + pid); if(result.avc === -1) { result.avc = pid; } break; // ISO/IEC 11172-3 (MPEG-1 audio) // or ISO/IEC 13818-3 (MPEG-2 halved sample rate audio) case 0x03: case 0x04: // logger.log('MPEG PID:' + pid); if(!mpegSupported) { logger.log( TSDemuxer.Tag, 'MPEG audio found, not supported in this browser for now' ); } else if(result.audio === -1) { result.audio = pid; result.isAAC = false; } break; case 0x24: logger.log(TSDemuxer.Tag, 'HEVC stream type found, not supported for now'); break; default: logger.log(TSDemuxer.Tag, `unkown stream type:${data[offset]}`); break; } // move to the next table entry // skip past the elementary stream descriptors, if present offset += (((data[offset + 3] & 0x0f) << 8) | data[offset + 4]) + 5; } return result; } /** * 解析PES包 * @param stream PES包 */ _parsePES(stream: pesData): parsedPesData | null { let i = 0; let frag; let pesFlags; let pesLen; let pesHdrLen; let pesData; let pesPts: number | undefined; let pesDts: number | undefined; let payloadStartOffset; const { data } = stream; // safety check if(!stream || stream.size === 0) { return null; } // we might need up to 19 bytes to read PES header // if first chunk of data is less than 19 bytes, let's merge it with following ones until we get 19 bytes // usually only one merge is needed (and this is rare ...) while(data[0].length < 19 && data.length > 1) { const newData = new Uint8Array(data[0].length + data[1].length); newData.set(data[0]); newData.set(data[1], data[0].length); data[0] = newData; data.splice(1, 1); } // retrieve PTS/DTS from first fragment frag = data[0]; const pesPrefix = (frag[0] << 16) + (frag[1] << 8) + frag[2]; if(pesPrefix === 1) { pesLen = (frag[4] << 8) + frag[5]; // if PES parsed length is not zero and greater than total received length, stop parsing. PES might be truncated // minus 6 : PES header size if(pesLen && pesLen > stream.size - 6) { return null; } pesFlags = frag[7]; if(pesFlags & 0xc0) { /* PES header described here : http://dvd.sourceforge.net/dvdinfo/pes-hdr.html as PTS / DTS is 33 bit we cannot use bitwise operator in JS, as Bitwise operators treat their operands as a sequence of 32 bits */ pesPts = (frag[9] & 0x0e) * 536870912 // 1 << 29 + (frag[10] & 0xff) * 4194304 // 1 << 22 + (frag[11] & 0xfe) * 16384 // 1 << 14 + (frag[12] & 0xff) * 128 // 1 << 7 + (frag[13] & 0xfe) / 2; // check if greater than 2^32 -1 if(pesPts > 4294967295) { // decrement 2^33 pesPts -= 8589934592; } if(pesFlags & 0x40) { pesDts = (frag[14] & 0x0e) * 536870912 // 1 << 29 + (frag[15] & 0xff) * 4194304 // 1 << 22 + (frag[16] & 0xfe) * 16384 // 1 << 14 + (frag[17] & 0xff) * 128 // 1 << 7 + (frag[18] & 0xfe) / 2; // check if greater than 2^32 -1 if(pesDts > 4294967295) { // decrement 2^33 pesDts -= 8589934592; } if(pesPts - pesDts > 60 * 90000) { logger.warn( TSDemuxer.Tag, `${Math.round( (pesPts - pesDts) / 90000 )}s delta between PTS and DTS, align them` ); pesPts = pesDts; } } else { pesDts = pesPts; } } pesHdrLen = frag[8]; // 9 bytes : 6 bytes for PES header + 3 bytes for PES extension payloadStartOffset = pesHdrLen + 9; stream.size -= payloadStartOffset; // reassemble PES packet pesData = new Uint8Array(stream.size); for(let j = 0, dataLen = data.length; j < dataLen; j++) { frag = data[j]; let len = frag.byteLength; if(payloadStartOffset) { if(payloadStartOffset > len) { // trim full frag if PES header bigger than frag payloadStartOffset -= len; continue; } else { // trim partial frag if PES header smaller than frag frag = frag.subarray(payloadStartOffset); len -= payloadStartOffset; payloadStartOffset = 0; } } pesData.set(frag, i); i += len; } if(pesLen) { // payload size : remove PES header + PES extension pesLen -= pesHdrLen + 3; } return { data: pesData, pts: pesPts, dts: pesDts, len: pesLen }; } return null; } pushAccesUnit(avcSample: avcSample, avcTrack: TSVideoTrack) { if(avcSample.units.length && avcSample.frame) { const { samples } = avcTrack; const nbSamples = samples.length; // only push AVC sample if starting with a keyframe is not mandatory OR // if keyframe already found in this fragment OR // keyframe found in last fragment (track.sps) AND // samples already appended (we already found a keyframe in this fragment) OR fragment is contiguous if( !this.config.forceKeyFrameOnDiscontinuity || avcSample.key === true || (avcTrack.sps && (nbSamples || this.contiguous)) ) { avcSample.id = nbSamples; samples.push(avcSample); } else { // dropped samples, track it avcTrack.dropped !== undefined && avcTrack.dropped++; } } if(avcSample.debug.length) { logger.info(TSDemuxer.Tag, `${avcSample.pts}/${avcSample.dts}:${avcSample.debug}`); } } /** * 解析AVC PES 数据 * @param pes 要解析的PES Data * @param last 是否为最后一个 */ _parseAVCPES(pes: parsedPesData, last?: boolean) { const track = this._avcTrack; const units = this._parseAVCNALu(pes.data); const debug = false; let expGolombDecoder; let { avcSample } = this; /** * 是否插入到units中 */ let push = false; /** * 是否找到SPS */ let spsfound = false; let i; const pushAccesUnit = this.pushAccesUnit.bind(this); // free pes.data to save up some memory delete pes.data; // if new NAL units found and last sample still there, let's push ... // this helps parsing streams with missing AUD (only do this if AUD never found) if(avcSample && units.length && !track.audFound) { pushAccesUnit(avcSample, track); this.avcSample = createAVCSample(false, pes.pts, pes.dts, ''); ({ avcSample } = this); } units.forEach((unit) => { switch(unit.type) { // eslint-disable-next-line no-case-declarations case 1: { // NDR push = true; if(!avcSample) { this.avcSample = createAVCSample(true, pes.pts, pes.dts, ''); ({ avcSample } = this); } if(!avcSample) { return; } if(debug) { avcSample.debug += 'NDR '; } avcSample.frame = true; const { data } = unit; // only check slice type to detect KF in case SPS found in same packet (any keyframe is preceded by SPS ...) if(spsfound && data.length > 4) { // retrieve slice type by parsing beginning of NAL unit (follow H264 spec, slice_header definition) to detect keyframe embedded in NDR const sliceType = new ExpGolomb(data).readSliceType(); // 2 : I slice, 4 : SI slice, 7 : I slice, 9: SI slice // SI slice : A slice that is coded using intra prediction only and using quantisation of the prediction samples. // An SI slice can be coded such that its decoded samples can be constructed identically to an SP slice. // I slice: A slice that is not an SI slice that is decoded using intra prediction only. // if (sliceType === 2 || sliceType === 7) { // https://www.cnblogs.com/pengkunfan/p/3945445.html SliceType 简介 if( sliceType === 2 || sliceType === 4 || sliceType === 7 || sliceType === 9 ) { avcSample.key = true; } } break; } // IDR case 5: push = true; // handle PES not starting with AUD if(!avcSample) { this.avcSample = createAVCSample(true, pes.pts, pes.dts, ''); ({ avcSample } = this); } if(!avcSample) { return; } if(debug) { avcSample.debug += 'IDR '; } avcSample.key = true; avcSample.frame = true; break; // SEI // eslint-disable-next-line no-case-declarations case 6: { push = true; if(debug && avcSample) { avcSample.debug += 'SEI '; } expGolombDecoder = new ExpGolomb(this.discardEPB(unit.data)); // skip frameType expGolombDecoder.readUByte(); let payloadType = 0; let payloadSize = 0; let endOfCaptions = false; let b = 0; while(!endOfCaptions && expGolombDecoder.bytesAvailable > 1) { payloadType = 0; do { b = expGolombDecoder.readUByte(); payloadType += b; } while(b === 0xff); // Parse payload size. payloadSize = 0; do { b = expGolombDecoder.readUByte(); payloadSize += b; } while(b === 0xff); // TODO: there can be more than one payload in an SEI packet... // TODO: need to read type and size in a while loop to get them all if(payloadType === 4 && expGolombDecoder.bytesAvailable !== 0) { endOfCaptions = true; const countryCode = expGolombDecoder.readUByte(); if(countryCode === 181) { const providerCode = expGolombDecoder.readUShort(); if(providerCode === 49) { const userStructure = expGolombDecoder.readUInt(); if(userStructure === 0x47413934) { const userDataType = expGolombDecoder.readUByte(); // Raw CEA-608 bytes wrapped in CEA-708 packet if(userDataType === 3) { const firstByte = expGolombDecoder.readUByte(); const secondByte = expGolombDecoder.readUByte(); const totalCCs = 31 & firstByte; const byteArray = [firstByte, secondByte]; for(i = 0; i < totalCCs; i++) { // 3 bytes per CC byteArray.push(expGolombDecoder.readUByte()); byteArray.push(expGolombDecoder.readUByte()); byteArray.push(expGolombDecoder.readUByte()); } this._insertSampleInOrder(this._txtTrack.samples, { type: 3, pts: <number>pes.pts, bytes: byteArray }); } } } } } else if(payloadType === 5 && expGolombDecoder.bytesAvailable !== 0) { endOfCaptions = true; if(payloadSize > 16) { const uuidStrArray = []; const userDataPayloadBytes = []; for(i = 0; i < 16; i++) { uuidStrArray.push(expGolombDecoder.readUByte().toString(16)); if(i === 3 || i === 5 || i === 7 || i === 9) { uuidStrArray.push('-'); } } for(i = 16; i < payloadSize; i++) { userDataPayloadBytes.push(expGolombDecoder.readUByte()); } this._insertSampleInOrder(this._txtTrack.samples, { pts: pes.pts, payloadType, uuid: uuidStrArray.join(''), userData: String.fromCharCode.apply(null, userDataPayloadBytes), userDataBytes: userDataPayloadBytes }); } } else if(payloadSize < expGolombDecoder.bytesAvailable) { for(i = 0; i < payloadSize; i++) { expGolombDecoder.readUByte(); } } } break; } // SPS // eslint-disable-next-line no-case-declarations case 7: push = true; spsfound = true; if(debug && avcSample) { avcSample.debug += 'SPS '; } if(!track.sps) { expGolombDecoder = new ExpGolomb(unit.data); const config = expGolombDecoder.readSPS(); track.width = config.width; track.height = config.height; track.pixelRatio = config.pixelRatio; track.sps = [unit.data]; track.duration = this._duration; const codecarray = unit.data.subarray(1, 4); let codecstring = 'avc1.'; for(i = 0; i < 3; i++) { let h = codecarray[i].toString(16); if(h.length < 2) { h = `0${h}`; } codecstring += h; } track.codec = codecstring; } break; // PPS case 8: push = true; if(debug && avcSample) { avcSample.debug += 'PPS '; } if(!track.pps) { track.pps = [unit.data]; } break; // AUD case 9: push = false; track.audFound = true; if(avcSample) { pushAccesUnit(avcSample, track); } this.avcSample = createAVCSample( false, pes.pts, pes.dts, debug ? 'AUD ' : '' ); ({ avcSample } = this); break; // Filler Data case 12: push = false; break; default: push = false; if(avcSample) { avcSample.debug += `unknown NAL ${unit.type} `; } break; } if(avcSample && push) { const { units } = avcSample; units.push(unit); } }); // if last PES packet, push samples if(last && avcSample) { pushAccesUnit(avcSample, track); this.avcSample = null; } } /** * 按照pts排序 插入一个sample * @param arr 被插入的avcsample * @param data 要排序的数据 */ _insertSampleInOrder(arr: Array<SampleLike>, data: SampleLike) { const len = arr.length; if(len > 0) { if(data.pts && data.pts >= <number>arr[len - 1].pts) { arr.push(data); } else { for(let pos = len - 1; pos >= 0; pos--) { if(data.pts && data.pts < <number>arr[pos].pts) { arr.splice(pos, 0, data); break; } } } } else { arr.push(data); } } _getLastNalUnit(): NALUnit | undefined { let { avcSample } = this; let lastUnit; // try to fallback to previous sample if current one is empty if(!avcSample || avcSample.units.length === 0) { const track = this._avcTrack; const { samples } = track; avcSample = samples[samples.length - 1]; } if(avcSample) { const { units } = avcSample; lastUnit = units[units.length - 1]; } return lastUnit; } /** * 解析AVC的NAL Unit, 返回 NAL Unit 的数组 * @param array 要解析的Uint8Array */ _parseAVCNALu(array: Uint8Array): Array<NALUnit> { let i = 0; const len = array.byteLength; let value; let overflow; const track = this._avcTrack; let state = track.naluState || 0; const lastState = state; const units = []; let unit: NALUnit; let unitType: number; let lastUnitStart = -1; let lastUnitType = 0; // logger.log('PES:' + Hex.hexDump(array)); if(state === -1) { // special use case where we fou