hjplayer
Version:
hjplayer, a HTML5 Player, can play flv and hls by Media Source Extension, based on typescript;
1,380 lines (1,196 loc) • 46 kB
text/typescript
import EventEmitter from 'eventemitter3';
import {
ProbeData,
ScriptData,
FrameRate,
track,
audioTrack,
videoTrack,
KeyframesData,
AacAudioData,
AacAudioDataPacket,
Mp3AudioData,
AvcSampleData,
AudioMediaData,
VideoMediaData
} from '../Interface';
import AMF from './AmfParser';
import SPSParser from './SPSParser';
import Log from '../../../Utils/Logger';
import Events from '../Events/index';
import { ErrorTypes, ErrorDetails } from '../errors';
import MediaInfo from '../../../Utils/media-info';
import MP4Remuxer from '../Remuxer/mp4-remuxer';
import { NALUnit } from '../../TSCodec/TSCodecInterface';
import getGlobal from '../../../Utils/getGlobalObject';
const GG = getGlobal();
function ReadBig32(array: Uint8Array, index: number) {
return (
(array[index] << 24) | (array[index + 1] << 16) | (array[index + 2] << 8) | array[index + 3]
);
}
/**
* FLV 文件解码器总输出
*/
class FLVDemuxer {
Tag: string
type: string
_dataOffset: number | undefined
_littleEndian: boolean
_firstParse: boolean
_dispatch: boolean
_hasAudio: boolean | undefined
_hasVideo: boolean | undefined
_hasAudioFlagOverrided: boolean
_hasVideoFlagOverrided: boolean
_audioInitialMetadataDispatched: boolean
_videoInitialMetadataDispatched: boolean
_metadata: ScriptData | null
_mediaInfo: MediaInfo
_audioMetadata: track | null
_videoMetadata: track | null
_naluLengthSize: number
_timestampBase: number
_timescale: number
_duration: number
_durationOverrided: boolean
_referenceFrameRate: FrameRate
_flvSoundRateTable: number[]
_mpegSamplingRates: number[]
_mpegAudioV10SampleRateTable: number[]
_mpegAudioV20SampleRateTable: number[]
_mpegAudioV25SampleRateTable: number[]
_mpegAudioL1BitRateTable: number[]
_mpegAudioL2BitRateTable: number[]
_mpegAudioL3BitRateTable: number[]
_videoTrack: videoTrack
_audioTrack: audioTrack
_onError: Function | null
eventEmitter: EventEmitter
remuxer: MP4Remuxer
config: Record<string, any>
constructor(data: ArrayBuffer, emitter: EventEmitter, config: Record<string, any>) {
const probeData: ProbeData = FLVDemuxer.probe(data);
this.eventEmitter = emitter;
this.Tag = 'FLVCodec';
this.type = 'FLVCodec';
this.config = config;
this.remuxer = new MP4Remuxer(this.eventEmitter, this.config);
this._onError = null;
this._dataOffset = probeData.dataOffset;
this._firstParse = true;
this._dispatch = false;
this._hasAudio = probeData.hasAudioTrack;
this._hasVideo = probeData.hasVideoTrack;
this._hasAudioFlagOverrided = false;
this._hasVideoFlagOverrided = false;
this._audioInitialMetadataDispatched = false;
this._videoInitialMetadataDispatched = false;
this._mediaInfo = new MediaInfo();
this._mediaInfo.hasAudio = this._hasAudio;
this._mediaInfo.hasVideo = this._hasVideo;
this._metadata = null;
this._audioMetadata = null;
this._videoMetadata = null;
this._naluLengthSize = 4;
this._timestampBase = 0; // int32, in milliseconds
this._timescale = 1000;
this._duration = 0; // int32, in milliseconds
this._durationOverrided = false;
this._referenceFrameRate = {
fixed: true,
fps: 23.976,
fpsNum: 23976,
fpsDen: 1000
};
this._flvSoundRateTable = [5500, 11025, 22050, 44100, 48000];
this._mpegSamplingRates = [
96000,
88200,
64000,
48000,
44100,
32000,
24000,
22050,
16000,
12000,
11025,
8000,
7350
];
// 音频采样率解释 https://baike.baidu.com/item/%E9%9F%B3%E9%A2%91%E9%87%87%E6%A0%B7%E7%8E%87/9023551?fr=aladdin
this._mpegAudioV10SampleRateTable = [44100, 48000, 32000, 0];
this._mpegAudioV20SampleRateTable = [22050, 24000, 16000, 0];
this._mpegAudioV25SampleRateTable = [11025, 12000, 8000, 0];
this._mpegAudioL1BitRateTable = [
0,
32,
64,
96,
128,
160,
192,
224,
256,
288,
320,
352,
384,
416,
448,
-1
];
this._mpegAudioL2BitRateTable = [
0,
32,
48,
56,
64,
80,
96,
112,
128,
160,
192,
224,
256,
320,
384,
-1
];
this._mpegAudioL3BitRateTable = [
0,
32,
40,
48,
56,
64,
80,
96,
112,
128,
160,
192,
224,
256,
320,
-1
];
this._videoTrack = {
type: 'video', id: 1, sequenceNumber: 0, samples: [], length: 0
};
this._audioTrack = {
type: 'audio', id: 2, sequenceNumber: 0, samples: [], length: 0
};
const getLittleEndian = () => {
const buf: ArrayBuffer = new ArrayBuffer(2);
new DataView(buf).setInt16(0, 256, true); // little-endian write
return new Int16Array(buf)[0] === 256; // platform-spec read, if equal then LE
};
this._littleEndian = getLittleEndian();
}
/**
* 添加数据
* @param chunk loader给的数据
*/
parseChunks(chunk: ArrayBuffer, byteStart: number): number {
let offset = 0;
const le = this._littleEndian;
if(byteStart === 0) {
// FLV header 部分的 buffer
if(chunk.byteLength > 13) {
const probeData: ProbeData = FLVDemuxer.probe(chunk);
offset = probeData.dataOffset || 0;
} else {
return 0;
}
}
if(this._firstParse) {
// handle PreviousTagSize0 before Tag1
this._firstParse = false;
if(byteStart + offset !== this._dataOffset) {
Log.warn(this.Tag, 'First time parsing but chunk byteStart invalid!');
}
const v = new DataView(chunk, offset);
const prevTagSize0 = v.getUint32(0, !le);
if(prevTagSize0 !== 0) {
Log.warn(this.Tag, 'PrevTagSize0 !== 0 !!!');
}
offset += 4;
}
while(offset < chunk.byteLength) {
this._dispatch = true;
const v = new DataView(chunk, offset);
if(offset + 11 + 4 > chunk.byteLength) {
// 数据不全
break;
}
const tagType = v.getUint8(0);
const dataSize = v.getUint32(0, !le) & 0x00ffffff;
if(offset + 11 + dataSize + 4 > chunk.byteLength) {
// 数据不全
break;
}
if(tagType !== 8 && tagType !== 9 && tagType !== 18) {
// 没有需要的类型
Log.warn(this.Tag, `Unsupported tag type ${tagType}, skipped`);
offset += 11 + dataSize + 4;
continue;
}
const ts2 = v.getUint8(4);
const ts1 = v.getUint8(5);
const ts0 = v.getUint8(6);
const ts3 = v.getUint8(7);
const timestamp = ts0 | (ts1 << 8) | (ts2 << 16) | (ts3 << 24);
const streamId = v.getUint32(7, !le) & 0x00ffffff;
if(streamId !== 0) {
Log.warn(this.Tag, 'Meet tag which has StreamID != 0!');
}
const dataOffset = offset + 11;
switch(tagType) {
case 8: // Audio
this._parseAudioData(chunk, dataOffset, dataSize, timestamp);
break;
case 9: // Video
this._parseVideoData(chunk, dataOffset, dataSize, timestamp, byteStart + offset);
break;
case 18: // ScriptDataObject
this._parseScriptData(chunk, dataOffset, dataSize);
break;
default:
Log.warn(this.Tag, `Unsupported tag type ${tagType}, skipped`);
offset += 11 + dataSize + 4;
break;
}
const prevTagSize = v.getUint32(11 + dataSize, !le);
if(prevTagSize !== 11 + dataSize) {
Log.warn(this.Tag, `Invalid PrevTagSize ${prevTagSize}`);
}
offset += 11 + dataSize + 4; // tagBody + dataSize + prevTagSize
}
// 解析后的数据帧 传递给MP4编码器
if(this._isInitialMetadataDispatched()) {
if(this._dispatch && (this._audioTrack.length || this._videoTrack.length)) {
this.remuxer.remux(this._audioTrack, this._videoTrack);
}
}
return offset;
}
// script数据包解析
_parseScriptData(arrayBuffer: ArrayBuffer, dataOffset: number, dataSize: number) {
const scriptData = AMF.parseScriptData(arrayBuffer, dataOffset, dataSize);
if(scriptData.onMetaData !== undefined) {
if(scriptData.onMetaData == null || typeof scriptData.onMetaData !== 'object') {
Log.warn(this.Tag, 'Invalid onMetaData structure!');
return;
}
if(this._metadata) {
Log.warn(this.Tag, 'Found another onMetaData tag!');
}
this._metadata = scriptData;
const { onMetaData } = scriptData;
this.eventEmitter.emit(Events.MEDIA_DATA_ARRIVED, { ...onMetaData });
if(typeof onMetaData.hasAudio === 'boolean') {
// hasAudio
if(this._hasAudioFlagOverrided === false) {
this._hasAudio = onMetaData.hasAudio;
this._mediaInfo.hasAudio = this._hasAudio;
}
}
if(typeof onMetaData.hasVideo === 'boolean') {
// hasVideo
if(this._hasVideoFlagOverrided === false) {
this._hasVideo = onMetaData.hasVideo;
this._mediaInfo.hasVideo = this._hasVideo;
}
}
if(typeof onMetaData.audiodatarate === 'number') {
// audiodatarate
this._mediaInfo.audioDataRate = onMetaData.audiodatarate;
}
if(typeof onMetaData.videodatarate === 'number') {
// videodatarate
this._mediaInfo.videoDataRate = onMetaData.videodatarate;
}
if(typeof onMetaData.width === 'number') {
// width
this._mediaInfo.width = onMetaData.width;
}
if(typeof onMetaData.height === 'number') {
// height
this._mediaInfo.height = onMetaData.height;
}
if(typeof onMetaData.duration === 'number') {
// duration
if(!this._durationOverrided) {
const duration = Math.floor(onMetaData.duration * this._timescale);
this._duration = duration;
this._mediaInfo.duration = duration;
}
} else {
this._mediaInfo.duration = 0;
}
if(typeof onMetaData.framerate === 'number') {
// framerate
const fpsNum = Math.floor(onMetaData.framerate * 1000);
if(fpsNum > 0) {
const fps = fpsNum / 1000;
this._referenceFrameRate.fixed = true;
this._referenceFrameRate.fps = fps;
this._referenceFrameRate.fpsNum = fpsNum;
this._referenceFrameRate.fpsDen = 1000;
this._mediaInfo.fps = fps;
}
}
if(typeof onMetaData.keyframes === 'object') {
// keyframes
this._mediaInfo.hasKeyframesIndex = true;
const { keyframes } = onMetaData;
this._mediaInfo.keyframesIndex = this._parseKeyframesIndex(keyframes);
onMetaData.keyframes = null; // keyframes has been extracted, remove it
} else {
this._mediaInfo.hasKeyframesIndex = false;
}
this._dispatch = false;
this._mediaInfo.metadata = onMetaData;
Log.info(this.Tag, 'Parsed onMetaData');
if(this._mediaInfo.isComplete()) {
this.eventEmitter.emit(Events.MEDIA_INFO, this._mediaInfo);
}
}
if(Object.keys(scriptData).length > 0) {
this.eventEmitter.emit(Events.SCRIPT_DATA_ARRIVED, { ...scriptData });
}
}
_parseKeyframesIndex(keyframes: KeyframesData) {
const times = [];
const filepositions = [];
// ignore first keyframe which is actually AVC Sequence Header (AVCDecoderConfigurationRecord)
for(let i = 1; i < keyframes.times.length; i++) {
const time = this._timestampBase + Math.floor(keyframes.times[i] * 1000);
times.push(time);
filepositions.push(keyframes.filepositions[i]);
}
return { times, filepositions };
}
// audio数据包解析
_parseAudioData(
arrayBuffer: ArrayBuffer,
dataOffset: number,
dataSize: number,
tagTimestamp: number
) {
if(dataSize <= 1) {
Log.warn(this.Tag, 'Flv: 无效的数据包, missing SoundData payload!');
return;
}
if(this._hasAudioFlagOverrided === true && this._hasAudio === false) {
// If hasAudio: false indicated explicitly in MediaDataSource,
// 忽略所有音频数据包
return;
}
const le = this._littleEndian;
const v = new DataView(arrayBuffer, dataOffset, dataSize);
const soundSpec = v.getUint8(0);
const soundFormat = soundSpec >>> 4;
if(soundFormat !== 2 && soundFormat !== 10) {
// MP3 or AAC
this.eventEmitter.emit(Events.ERROR, {
type: ErrorTypes.MUX_ERROR,
details: ErrorDetails.CODEC_UNSUPPORTED,
fatal: false,
reason: `Flv: 不支持的类型 idx: ${soundFormat}`
});
return;
}
let soundRate = 0;
const soundRateIndex = (soundSpec & 12) >>> 2;
if(soundRateIndex >= 0 && soundRateIndex <= 4) {
soundRate = this._flvSoundRateTable[soundRateIndex];
} else {
this.eventEmitter.emit(Events.ERROR, {
type: ErrorTypes.MUX_ERROR,
details: ErrorDetails.FORMAT_ERROR,
fatal: false,
reason: `Flv: 音频采样率无效 idx: ${soundRateIndex}`
});
return;
}
const soundSize = (soundSpec & 2) >>> 1; // unused
const soundType = soundSpec & 1;
let meta: any = this._audioMetadata;
const track = this._audioTrack;
if(!meta) {
if(this._hasAudio === false && this._hasAudioFlagOverrided === false) {
this._hasAudio = true;
this._mediaInfo.hasAudio = true;
}
// initial metadata
this._audioMetadata = Object.create(null);
meta = this._audioMetadata;
meta.type = 'audio';
meta.id = track.id;
meta.timescale = this._timescale;
meta.duration = this._duration;
meta.audioSampleRate = soundRate;
meta.channelCount = soundType === 0 ? 1 : 2;
}
if(soundFormat === 10) {
// AAC 格式的音频
const aacData: AacAudioDataPacket | undefined = this._parseAACAudioData(
arrayBuffer,
dataOffset + 1,
dataSize - 1
);
if(aacData === undefined) {
return;
}
if(aacData.packetType === 0) {
// AAC sequence header (AudioSpecificConfig)
if(meta.config) {
Log.warn(this.Tag, 'Found another AudioSpecificConfig!');
}
if(aacData.type === 'object') {
const misc = <AacAudioData>aacData.data;
meta.audioSampleRate = misc.samplingRate;
meta.channelCount = misc.channelCount;
meta.codec = misc.codec;
meta.originalCodec = misc.originalCodec;
meta.config = misc.config;
// The decode result of an aac sample is 1024 PCM samples
const sampleRate = meta.audioSampleRate || 0;
const timescale = meta.timescale || 0;
meta.refSampleDuration = (1024 / sampleRate) * timescale;
Log.info(this.Tag, 'Parsed AudioSpecificConfig');
if(this._isInitialMetadataDispatched()) {
// Non-initial metadata, force dispatch (or flush) parsed frames to remuxer
if(
this._dispatch
&& (this._audioTrack.length || this._videoTrack.length)
) {
this.remuxer.remux(this._audioTrack, this._videoTrack);
}
} else {
this._audioInitialMetadataDispatched = true;
}
// then notify new metadata
this._dispatch = false;
this.remuxer._onTrackMetadataReceived('audio', meta);
const mi: MediaInfo = this._mediaInfo;
mi.audioCodec = meta.originalCodec;
mi.audioSampleRate = meta.audioSampleRate;
mi.audioChannelCount = meta.channelCount;
if(mi.hasVideo) {
if(mi.videoCodec != null) {
mi.mimeType = `video/x-flv; codecs="${mi.videoCodec},${mi.audioCodec}"`;
}
} else {
mi.mimeType = `video/x-flv; codecs="${mi.audioCodec}"`;
}
if(mi.isComplete()) {
this.eventEmitter.emit(Events.MEDIA_INFO, mi);
}
}
} else if(aacData.packetType === 1) {
// AAC raw frame data
const aacDataUnitArray = <Uint8Array>aacData.data;
const dts = this._timestampBase + tagTimestamp;
const aacSample = {
unit: aacDataUnitArray,
length: aacDataUnitArray.byteLength,
dts,
pts: dts,
cts: 0,
};
track.samples.push(aacSample);
track.length += aacDataUnitArray.length;
} else {
Log.error(this.Tag, `Flv: Unsupported AAC data type ${aacData.packetType}`);
}
} else if(soundFormat === 2) {
// 处理MP3格式的音频
if(!meta.codec) {
// We need metadata for mp3 audio track, extract info from frame header
const misc: Mp3AudioData | Uint8Array | undefined = this._parseMP3AudioData(
arrayBuffer,
dataOffset + 1,
dataSize - 1,
true
);
if(misc === undefined || misc instanceof Uint8Array) {
return;
}
meta.audioSampleRate = misc.samplingRate;
meta.channelCount = misc.channelCount;
meta.codec = misc.codec;
meta.originalCodec = misc.originalCodec;
// The decode result of an mp3 sample is 1152 PCM samples
const sampleRate2 = meta.audioSampleRate || 0;
const timescale2 = meta.timescale || 0;
meta.refSampleDuration = (1152 / sampleRate2) * timescale2;
Log.info(this.Tag, 'Parsed MPEG Audio Frame Header');
this._audioInitialMetadataDispatched = true;
this.remuxer._onTrackMetadataReceived('audio', meta);
const mi = this._mediaInfo;
mi.audioCodec = meta.codec;
mi.audioSampleRate = meta.audioSampleRate;
mi.audioChannelCount = meta.channelCount;
mi.audioDataRate = misc.bitRate;
if(mi.hasVideo) {
if(mi.videoCodec != null) {
mi.mimeType = `video/x-flv; codecs="${mi.videoCodec},${mi.audioCodec}"`;
}
} else {
mi.mimeType = `video/x-flv; codecs="${mi.audioCodec}"`;
}
if(mi.isComplete()) {
this.eventEmitter.emit(Events.MEDIA_INFO, mi);
}
}
// This packet is always a valid audio packet, extract it
const data: Mp3AudioData | Uint8Array | undefined = this._parseMP3AudioData(
arrayBuffer,
dataOffset + 1,
dataSize - 1,
false
);
if(data === undefined || !(data instanceof Uint8Array)) {
return;
}
const dts = this._timestampBase + tagTimestamp;
const mp3Sample = {
unit: data, length: data.byteLength, dts, pts: dts, cts: 0,
};
track.samples.push(mp3Sample);
track.length += data.length;
}
}
_parseAACAudioData(
arrayBuffer: ArrayBuffer,
dataOffset: number,
dataSize: number
): AacAudioDataPacket | undefined {
if(dataSize <= 1) {
Log.warn(this.Tag, 'Flv: Invalid AAC packet, missing AACPacketType or/and Data!');
return;
}
const result = Object.create(null);
const array = new Uint8Array(arrayBuffer, dataOffset, dataSize);
result.packetType = array[0];
if(array[0] === 0) {
result.type = 'object';
result.data = this._parseAACAudioSpecificConfig(
arrayBuffer,
dataOffset + 1,
dataSize - 1
);
} else {
result.type = 'Unit8Array';
result.data = array.subarray(1);
}
return result;
}
_parseAACAudioSpecificConfig(arrayBuffer: ArrayBuffer, dataOffset: number, dataSize: number) {
const array = new Uint8Array(arrayBuffer, dataOffset, dataSize);
let config = null;
/* Audio Object Type:
0: Null
1: AAC Main
2: AAC LC
3: AAC SSR (Scalable Sample Rate)
4: AAC LTP (Long Term Prediction)
5: HE-AAC / SBR (Spectral Band Replication)
6: AAC Scalable
*/
let audioObjectType = 0;
let originalAudioObjectType = 0;
// let audioExtensionObjectType = null;
let samplingIndex = 0;
let extensionSamplingIndex = null;
// 5 bits
originalAudioObjectType = array[0] >>> 3;
audioObjectType = originalAudioObjectType;
// 4 bits
samplingIndex = ((array[0] & 0x07) << 1) | (array[1] >>> 7);
if(samplingIndex < 0 || samplingIndex >= this._mpegSamplingRates.length) {
this.eventEmitter.emit(Events.ERROR, {
type: ErrorTypes.MUX_ERROR,
details: ErrorDetails.FORMAT_ERROR,
fatal: false,
reason: 'Flv: AAC invalid sampling frequency index!'
});
return;
}
const samplingFrequence = this._mpegSamplingRates[samplingIndex];
// 4 bits
const channelConfig = (array[1] & 0x78) >>> 3;
if(channelConfig < 0 || channelConfig >= 8) {
this.eventEmitter.emit(Events.ERROR, {
type: ErrorTypes.MUX_ERROR,
details: ErrorDetails.FORMAT_ERROR,
fatal: false,
reason: 'Flv: AAC invalid channel configuration'
});
return;
}
if(audioObjectType === 5) {
// HE-AAC?
// 4 bits
extensionSamplingIndex = ((array[1] & 0x07) << 1) | (array[2] >>> 7);
// 5 bits
// const audioExtensionObjectType = (array[2] & 0x7c) >>> 2
}
// workarounds for various browsers
const userAgent = GG.navigator.userAgent.toLowerCase();
if(userAgent.indexOf('firefox') !== -1) {
// firefox: use SBR (HE-AAC) if freq less than 24kHz
if(samplingIndex >= 6) {
audioObjectType = 5;
config = new Array(4);
extensionSamplingIndex = samplingIndex - 3;
} else {
// use LC-AAC
audioObjectType = 2;
config = new Array(2);
extensionSamplingIndex = samplingIndex;
}
} else if(userAgent.indexOf('android') !== -1) {
// android: always use LC-AAC
audioObjectType = 2;
config = new Array(2);
extensionSamplingIndex = samplingIndex;
} else {
// for other browsers, e.g. chrome...
// Always use HE-AAC to make it easier to switch aac codec profile
audioObjectType = 5;
extensionSamplingIndex = samplingIndex;
config = new Array(4);
if(samplingIndex >= 6) {
extensionSamplingIndex = samplingIndex - 3;
} else if(channelConfig === 1) {
// Mono channel
audioObjectType = 2;
config = new Array(2);
extensionSamplingIndex = samplingIndex;
}
}
config[0] = audioObjectType << 3;
config[0] |= (samplingIndex & 0x0f) >>> 1;
config[1] = (samplingIndex & 0x0f) << 7;
config[1] |= (channelConfig & 0x0f) << 3;
if(audioObjectType === 5) {
config[1] |= (extensionSamplingIndex & 0x0f) >>> 1;
config[2] = (extensionSamplingIndex & 0x01) << 7;
// extended audio object type: force to 2 (LC-AAC)
config[2] |= 2 << 2;
config[3] = 0;
}
return {
config,
samplingRate: samplingFrequence,
channelCount: channelConfig,
codec: `mp4a.40.${audioObjectType}`,
originalCodec: `mp4a.40.${originalAudioObjectType}`
};
}
_parseMP3AudioData(
arrayBuffer: ArrayBuffer,
dataOffset: number,
dataSize: number,
requestHeader: boolean
): Mp3AudioData | Uint8Array | undefined {
if(dataSize < 4) {
Log.warn(this.Tag, 'Flv: Invalid MP3 packet, header missing!');
return;
}
const le = this._littleEndian;
const array = new Uint8Array(arrayBuffer, dataOffset, dataSize);
let result = null;
if(requestHeader) {
if(array[0] !== 0xff) {
return;
}
const ver = (array[1] >>> 3) & 0x03;
const layer = (array[1] & 0x06) >> 1;
const bitrateIndex = (array[2] & 0xf0) >>> 4;
const samplingFreqIndex = (array[2] & 0x0c) >>> 2;
const channelMode = (array[3] >>> 6) & 0x03;
const channelCount = channelMode !== 3 ? 2 : 1;
let sampleRate = 0;
let bitRate = 0;
let objectType = 34; // Layer-3, listed in MPEG-4 Audio Object Types
const codec = 'mp3';
switch(ver) {
case 0: // MPEG 2.5
sampleRate = this._mpegAudioV25SampleRateTable[samplingFreqIndex];
break;
case 2: // MPEG 2
sampleRate = this._mpegAudioV20SampleRateTable[samplingFreqIndex];
break;
case 3: // MPEG 1
sampleRate = this._mpegAudioV10SampleRateTable[samplingFreqIndex];
break;
default:
sampleRate = this._mpegAudioV25SampleRateTable[samplingFreqIndex];
break;
}
switch(layer) {
case 1: // Layer 3
objectType = 34;
if(bitrateIndex < this._mpegAudioL3BitRateTable.length) {
bitRate = this._mpegAudioL3BitRateTable[bitrateIndex];
}
break;
case 2: // Layer 2
objectType = 33;
if(bitrateIndex < this._mpegAudioL2BitRateTable.length) {
bitRate = this._mpegAudioL2BitRateTable[bitrateIndex];
}
break;
case 3: // Layer 1
objectType = 32;
if(bitrateIndex < this._mpegAudioL1BitRateTable.length) {
bitRate = this._mpegAudioL1BitRateTable[bitrateIndex];
}
break;
default:
objectType = 34;
if(bitrateIndex < this._mpegAudioL3BitRateTable.length) {
bitRate = this._mpegAudioL3BitRateTable[bitrateIndex];
}
break;
}
result = {
bitRate,
samplingRate: sampleRate,
channelCount,
codec,
originalCodec: codec
};
} else {
result = array;
}
return result;
}
_parseVideoData(
arrayBuffer: ArrayBuffer,
dataOffset: number,
dataSize: number,
tagTimestamp: number,
tagPosition: number
) {
if(dataSize <= 1) {
Log.warn(this.Tag, 'Flv: Invalid video packet, missing VideoData payload!');
return;
}
if(this._hasVideoFlagOverrided === true && this._hasVideo === false) {
// If hasVideo: false indicated explicitly in MediaDataSource,
// Ignore all the video packets
return;
}
const spec = new Uint8Array(arrayBuffer, dataOffset, dataSize)[0];
const frameType = (spec & 240) >>> 4;
const codecId = spec & 15;
if(codecId !== 7) {
this.eventEmitter.emit(Events.ERROR, ErrorTypes.MUX_ERROR, {
type: ErrorTypes.MUX_ERROR,
code: -1,
details: ErrorDetails.CODEC_UNSUPPORTED,
fatal: false,
reason: `Flv: Unsupported codec in video frame: ${codecId}`,
});
return;
}
this._parseAVCVideoPacket(
arrayBuffer,
dataOffset + 1,
dataSize - 1,
tagTimestamp,
tagPosition,
frameType
);
}
_parseAVCVideoPacket(
arrayBuffer: ArrayBuffer,
dataOffset: number,
dataSize: number,
tagTimestamp: number,
tagPosition: number,
frameType: number
) {
if(dataSize < 4) {
Log.warn(
this.Tag,
'Flv: Invalid AVC packet, missing AVCPacketType or/and CompositionTime'
);
return;
}
const le = this._littleEndian;
const v = new DataView(arrayBuffer, dataOffset, dataSize);
const packetType = v.getUint8(0);
const ctsUnsigned = v.getUint32(0, !le) & 0x00ffffff;
const cts = (ctsUnsigned << 8) >> 8; // convert to 24-bit signed int
if(packetType === 0) {
// AVCDecoderConfigurationRecord
this._parseAVCDecoderConfigurationRecord(arrayBuffer, dataOffset + 4, dataSize - 4);
} else if(packetType === 1) {
// One or more Nalus
this._parseAVCVideoData(
arrayBuffer,
dataOffset + 4,
dataSize - 4,
tagTimestamp,
tagPosition,
frameType,
cts
);
} else if(packetType === 2) {
// empty, AVC end of sequence
} else {
this.eventEmitter.emit(Events.ERROR, {
type: ErrorTypes.MUX_ERROR,
details: ErrorDetails.FORMAT_ERROR,
fatal: false,
reason: `Flv: Invalid video packet type ${packetType}`
});
}
}
_parseAVCDecoderConfigurationRecord(
arrayBuffer: ArrayBuffer,
dataOffset: number,
dataSize: number
) {
if(dataSize < 7) {
Log.warn(this.Tag, 'Flv: Invalid AVCDecoderConfigurationRecord, lack of data!');
return;
}
let meta: any = this._videoMetadata;
const track = this._videoTrack;
const le = this._littleEndian;
const v = new DataView(arrayBuffer, dataOffset, dataSize);
if(!meta) {
if(this._hasVideo === false && this._hasVideoFlagOverrided === false) {
this._hasVideo = true;
this._mediaInfo.hasVideo = true;
}
this._videoMetadata = Object.create(null);
meta = this._videoMetadata;
meta.type = 'video';
meta.id = track.id;
meta.timescale = this._timescale;
meta.duration = this._duration;
} else if(typeof meta.avcc !== 'undefined') {
Log.warn(this.Tag, 'Found another AVCDecoderConfigurationRecord!');
}
const version = v.getUint8(0); // configurationVersion
const avcProfile = v.getUint8(1); // avcProfileIndication
const profileCompatibility = v.getUint8(2); // profile_compatibility
const avcLevel = v.getUint8(3); // AVCLevelIndication
if(version !== 1 || avcProfile === 0) {
this.eventEmitter.emit(Events.ERROR, {
type: ErrorTypes.MUX_ERROR,
details: ErrorDetails.FORMAT_ERROR,
fatal: false,
reason: 'Flv: Invalid AVCDecoderConfigurationRecord'
});
return;
}
this._naluLengthSize = (v.getUint8(4) & 3) + 1; // lengthSizeMinusOne
if(this._naluLengthSize !== 3 && this._naluLengthSize !== 4) {
// holy shit!!!
this.eventEmitter.emit(Events.ERROR, {
type: ErrorTypes.MUX_ERROR,
details: ErrorDetails.FORMAT_ERROR,
fatal: false,
reason: `Flv: Strange NaluLengthSizeMinusOne: ${this._naluLengthSize - 1}`
});
return;
}
const spsCount = v.getUint8(5) & 31; // numOfSequenceParameterSets
if(spsCount === 0) {
this.eventEmitter.emit(Events.ERROR, {
type: ErrorTypes.MUX_ERROR,
details: ErrorDetails.FORMAT_ERROR,
fatal: false,
reason: 'Flv: Invalid AVCDecoderConfigurationRecord: No SPS'
});
return;
} if(spsCount > 1) {
Log.warn(
this.Tag,
`Flv: Strange AVCDecoderConfigurationRecord: SPS Count = ${spsCount}`
);
}
let offset = 6;
for(let i = 0; i < spsCount; i++) {
const len = v.getUint16(offset, !le); // sequenceParameterSetLength
offset += 2;
if(len === 0) {
continue;
}
// Notice: Nalu without startcode header (00 00 00 01)
const sps = new Uint8Array(arrayBuffer, dataOffset + offset, len);
offset += len;
const config = SPSParser.parseSPS(sps);
if(i !== 0) {
// ignore other sps's config
continue;
}
meta.codecWidth = config.codecSize.width;
meta.codecHeight = config.codecSize.height;
meta.presentWidth = config.presentSize.width;
meta.presentHeight = config.presentSize.height;
meta.profile = config.profileString;
meta.level = config.levelString;
meta.bitDepth = config.bitDepth;
meta.chromaFormat = config.chromaFormat;
meta.sarRatio = config.sarRatio;
meta.frameRate = config.frameRate;
if(
config.frameRate.fixed === false
|| config.frameRate.fpsNum === 0
|| config.frameRate.fpsDen === 0
) {
meta.frameRate = this._referenceFrameRate;
}
const { frameRate } = meta;
const { fpsDen } = frameRate;
const { fpsNum } = frameRate;
meta.refSampleDuration = meta.timescale * (fpsDen / fpsNum);
const codecArray = sps.subarray(1, 4);
let codecString = 'avc1.';
for(let j = 0; j < 3; j++) {
let h = codecArray[j].toString(16);
if(h.length < 2) {
h = `0${h}`;
}
codecString += h;
}
meta.codec = codecString;
const mi = this._mediaInfo;
mi.width = meta.codecWidth;
mi.height = meta.codecHeight;
mi.fps = meta.frameRate.fps;
mi.profile = meta.profile;
mi.level = meta.level;
mi.refFrames = config.refFrames;
mi.chromaFormat = config.chromaFormatString;
const { sarRatio } = meta;
mi.sarNum = sarRatio.width;
mi.sarDen = sarRatio.height;
mi.videoCodec = codecString;
if(mi.hasAudio) {
if(mi.audioCodec != null) {
mi.mimeType = `video/x-flv; codecs="${mi.videoCodec},${mi.audioCodec}"`;
}
} else {
mi.mimeType = `video/x-flv; codecs="${mi.videoCodec}"`;
}
if(mi.isComplete()) {
this.eventEmitter.emit(Events.MEDIA_INFO, mi);
}
}
const ppsCount = v.getUint8(offset); // numOfPictureParameterSets
if(ppsCount === 0) {
this.eventEmitter.emit(Events.ERROR, {
type: ErrorTypes.MUX_ERROR,
details: ErrorDetails.FORMAT_ERROR,
fatal: false,
reason: 'Flv: Invalid AVCDecoderConfigurationRecord: No PPS'
});
return;
} if(ppsCount > 1) {
Log.warn(
this.Tag,
`Flv: Strange AVCDecoderConfigurationRecord: PPS Count = ${ppsCount}`
);
}
offset++;
for(let i = 0; i < ppsCount; i++) {
const len = v.getUint16(offset, !le); // pictureParameterSetLength
offset += 2;
if(len === 0) {
continue;
}
// pps is useless for extracting video information
offset += len;
}
meta.avcc = new Uint8Array(dataSize);
meta.avcc.set(new Uint8Array(arrayBuffer, dataOffset, dataSize), 0);
Log.info(this.Tag, 'Parsed AVCDecoderConfigurationRecord');
if(this._isInitialMetadataDispatched()) {
// flush parsed frames
if(this._dispatch && (this._audioTrack.length || this._videoTrack.length)) {
this.remuxer.remux(this._audioTrack, this._videoTrack);
}
} else {
this._videoInitialMetadataDispatched = true;
}
// notify new metadata
this._dispatch = false;
this.remuxer._onTrackMetadataReceived('video', meta);
}
// 解析NALU
_parseAVCVideoData(
arrayBuffer: ArrayBuffer,
dataOffset: number,
dataSize: number,
tagTimestamp: number,
tagPosition: number,
frameType: number,
cts: number
) {
const le = this._littleEndian;
const v = new DataView(arrayBuffer, dataOffset, dataSize);
const units: Array<NALUnit> = [];
let length = 0;
let offset = 0;
const lengthSize = this._naluLengthSize;
const dts = this._timestampBase + tagTimestamp;
let isKeyframe = frameType === 1; // from FLV Frame Type constants
while(offset < dataSize) {
if(offset + 4 >= dataSize) {
Log.warn(
this.Tag,
`Malformed Nalu near timestamp ${dts}, offset = ${offset}, dataSize = ${dataSize}`
);
break; // data not enough for next Nalu
}
// Nalu with length-header (AVC1)
let naluSize = v.getUint32(offset, !le); // Big-Endian read
if(lengthSize === 3) {
naluSize >>>= 8;
}
if(naluSize > dataSize - lengthSize) {
Log.warn(this.Tag, `Malformed Nalus near timestamp ${dts}, NaluSize > DataSize!`);
return;
}
const unitType = v.getUint8(offset + lengthSize) & 0x1f;
if(unitType === 5) {
// IDR
isKeyframe = true;
}
const data = new Uint8Array(arrayBuffer, dataOffset + offset, lengthSize + naluSize);
const unit: NALUnit = { type: unitType, data };
if(unit.type === 6) {
// 获取到SEI信息
try {
const unitArray: Uint8Array = data.subarray(lengthSize);
this.eventEmitter.emit(Events.GET_SEI_INFO, unitArray);
} catch (e) {
Log.log(this.Tag, 'parse sei info error!');
}
}
units.push(unit);
length += data.byteLength;
offset += lengthSize + naluSize;
}
if(units.length) {
const track = this._videoTrack;
const avcSample: AvcSampleData = {
units,
length,
isKeyframe,
dts,
cts,
pts: dts + cts
};
if(isKeyframe) {
avcSample.fileposition = tagPosition;
}
track.samples.push(avcSample);
track.length += length;
}
}
// 探测数据是否支持解码
static probe(data: ArrayBuffer) {
const info: Uint8Array = new Uint8Array(data);
const mismatch: { match: boolean } = { match: false };
if(info[0] !== 0x46 || info[1] !== 0x4c || info[2] !== 0x56 || info[3] !== 0x01) {
return mismatch;
}
const hasAudio = (info[4] & 4) >>> 2 !== 0;
const hasVideo = (info[4] & 1) !== 0;
const offset = ReadBig32(info, 5);
if(offset < 9) {
return mismatch;
}
return {
match: true,
consumed: offset,
dataOffset: offset,
hasAudioTrack: hasAudio,
hasVideoTrack: hasVideo
};
}
on(eventName: string, callback: EventEmitter.ListenerFn) {
this.eventEmitter.on(eventName, callback);
}
once(eventName: string, callback: EventEmitter.ListenerFn) {
this.eventEmitter.once(eventName, callback);
}
off(eventName: string, callback?: EventEmitter.ListenerFn) {
this.eventEmitter.off(eventName, callback);
}
destroy() {
this.eventEmitter.removeAllListeners();
delete this.eventEmitter;
}
// timestamp base for output samples, must be in milliseconds
get timestampBase() {
return this._timestampBase;
}
set timestampBase(base: number) {
this._timestampBase = base;
}
get overridedDuration() {
return this._duration;
}
// Force-override media duration. Must be in milliseconds, int32
set overridedDuration(duration: number) {
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;
}
resetMediaInfo() {
this._mediaInfo = new MediaInfo();
}
_isInitialMetadataDispatched() {
if(this._hasAudio && this._hasVideo) {
// both audio & video
return this._audioInitialMetadataDispatched && this._videoInitialMetadataDispatched;
}
if(this._hasAudio && !this._hasVideo) {
// audio only
return this._audioInitialMetadataDispatched;
}
if(!this._hasAudio && this._hasVideo) {
// video only
return this._videoInitialMetadataDispatched;
}
return false;
}
insertDiscontinuity() {
this.remuxer.insertDiscontinuity();
}
seek() {
this.remuxer.seek();
}
flushStashedSamples() {
this.remuxer.flushStashedSamples();
}
}
export default FLVDemuxer;