webcodecs-flv
Version:
webcodecs decode flv
613 lines (585 loc) • 18.9 kB
JavaScript
import AMF from './amf-parser.js';
import * as EventModule from './event.js';
import { demuxerChange } from './event.js';
import SPSParser from './sps-parser.js';
function Swap16(src) {
return ((src >>> 8) & 0xff) | ((src & 0xff) << 8);
}
function Swap32(src) {
return (
((src & 0xff000000) >>> 24) |
((src & 0x00ff0000) >>> 8) |
((src & 0x0000ff00) << 8) |
((src & 0x000000ff) << 24)
);
}
function ReadBig32(array, index) {
return (
(array[index] << 24) |
(array[index + 1] << 16) |
(array[index + 2] << 8) |
array[index + 3]
);
}
class FLVDemuxer {
constructor() {
Object.assign(this, new EventModule.EventDispatcher());
this.FLVHeader = {
//header:占9个字节
streamInfo: {
//1个字节
hasVideo: null,
hasAudio: null,
},
version: 1, //1个字节
Type: 'flv', // 3个字节
headerSize: null, //4个字节整个header的长度,一般为9; 大于9表示下面还有扩展信息 (9)
offset: 0,
};
this.videoInfo = {
codec: null, //编解码
DataRate: null,
width: null,
height: null,
fps: null,
profile: null,
level: null,
refFrames: null,
chromaFormat: null,
sarNum: null,
sarDen: null,
data: null,
ts: null,
};
this.audioInfo = {
codec: null, //编解码
DataRate: null,
audioSampleRate: null,
audioChannelCount: null,
data: null,
ts: null,
};
this._firstParse = false;
this._littleEndian = (function () {
//小端
let buf = 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._flvSoundRateTable = [5500, 11025, 22050, 44100, 48000];
this._mpegSamplingRates = [
96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000,
11025, 8000, 7350,
];
this.noParseData = null;
this._naluLengthSize = 4;
this.chunkList = [];
this.isParse = false;
this.firstData = [];
this.isFirstData = true;
}
_parseHeader(buffer) {
let data = new Uint8Array(buffer);
if (
data[0] !== 0x46 ||
data[1] !== 0x4c ||
data[2] !== 0x56 ||
data[3] !== 0x01
) {
return false;
}
this.FLVHeader.streamInfo.hasAudio = (data[4] & 4) >>> 2 !== 0;
this.FLVHeader.streamInfo.hasVideo = (data[4] & 1) !== 0;
this.FLVHeader.offset = ReadBig32(data, 5); //获取header长度,一般为9
this.dispatchEvent(
new demuxerChange('demuxer', { event: 'flvHeader', data: this.FLVHeader })
);
}
parseChunks(chunk) {
if (!this.isParse) {
if (this.chunkList.length == 0) {
this._parseChunks(chunk);
} else {
let parseData = this.chunkList.splice(0, 1);
this._parseChunks(parseData);
}
} else {
this.chunkList.push(chunk);
}
}
_parseChunks(chunk, byteStart) {
let offset = 0;
let originChunk = chunk;
if (this.noParseData) {
chunk = this._concatUint8(this.noParseData, chunk);
} else {
chunk = originChunk;
}
if (!this._firstParse) {
this._firstParse = true;
this._parseHeader(chunk); //解析header
// 处理PreviousTagSize0 占4个字节,一般为0
if (
chunk[9] !== 0 ||
chunk[10] !== 0 ||
chunk[11] !== 0 ||
chunk[12] !== 0
) {
return 'First time parsing but chunk byteStart invalid!';
}
offset = this.FLVHeader.offset + 4; //第一个PreviousTag
}
this.isParse = true;
while (offset < chunk.byteLength) {
let v = new DataView(chunk.buffer, offset);
if (offset + 11 + 4 <= chunk.byteLength) {
this.noParseData = null;
}
if (offset + 11 + 4 > chunk.byteLength) {
// data not enough for parsing an flv tag
this.noParseData = new Uint8Array(chunk.buffer, offset);
break;
}
let tagType = v.getUint8(0);
let dataSize = v.getUint32(0, !this._littleEndian) & 0x00ffffff;
if (offset + 11 + dataSize + 4 <= chunk.byteLength) {
this.noParseData = null;
}
if (offset + 11 + dataSize + 4 > chunk.byteLength) {
this.noParseData = new Uint8Array(chunk.buffer, offset);
break;
}
if (tagType !== 8 && tagType !== 9 && tagType !== 18) {
offset += 11 + dataSize + 4;
continue;
}
let ts2 = v.getUint8(4);
let ts1 = v.getUint8(5);
let ts0 = v.getUint8(6);
let ts3 = v.getUint8(7); //高位8位
let timestamp = ts0 | (ts1 << 8) | (ts2 << 16) | (ts3 << 24);
let streamId = v.getUint32(7, !this._littleEndian) & 0x00ffffff;
// console.log(streamId, 'streamId');
let dataOffset = offset + 11; //body 中的tag header占11个字节
switch (tagType) {
case 8: // Audio
this._parseAudioData(chunk.buffer, dataOffset, dataSize, timestamp);
break;
case 9: // Video
this._parseVideoData(
chunk.buffer,
dataOffset,
dataSize,
timestamp,
byteStart + offset
);
break;
case 18: // ScriptDataObject
this._parseScriptData(chunk.buffer, dataOffset, dataSize);
break;
}
offset += 11 + dataSize + 4;
}
this.isParse = false;
}
_parseAudioData(arrayBuffer, dataOffset, dataSize, tagTimestamp) {
if (dataSize <= 1) {
return 'Flv: Invalid audio packet, missing SoundData payload!';
}
let le = this._littleEndian;
let v = new DataView(arrayBuffer, dataOffset, dataSize);
let soundSpec = v.getUint8(0);
let soundFormat = soundSpec >>> 4; //音频格式占4个字节
// console.log(soundFormat, 'soundFormat 10 is AAC');
let soundRateIndex = (soundSpec & 12) >>> 2; //占2个字节
// console.log(soundRateIndex, 'soundRateIndex 采样率'); // AAC 总是3
let soundRate = 0;
if (soundRateIndex >= 0 && soundRateIndex <= 4) {
soundRate = this._flvSoundRateTable[soundRateIndex];
} else {
return;
}
// console.log(soundRate, 'soundRate');
let soundSize = (soundSpec & 2) >>> 1; // 采样长度 0 = snd8Bit1 = snd16Bit 压缩过的音频都是16bit
// console.log(soundSize, 'soundSize 采样长度');
let soundType = soundSpec & 1; //音频类型 0 = sndMono 1 = sndStereo
// console.log(soundType, 'soundType 音频类型');
this.audioInfo.audioChannelCount = soundType === 0 ? 1 : 2; //声道数
this.audioInfo.audioSampleRate = soundRate; //采样率
if (soundFormat === 10) {
let aacData = this._parseAACAudioData(
arrayBuffer,
dataOffset + 1,
dataSize - 1,
tagTimestamp
);
// console.log(aacData, 'aacData');
}
}
_parseAACAudioData(arrayBuffer, dataOffset, dataSize, tagTimestamp) {
if (dataSize <= 1) {
return;
}
// let result = {};
let array = new Uint8Array(arrayBuffer, dataOffset, dataSize);
// console.log(array, 'audio array ');
// result.packetType = array[0];
if (array[0] === 0) {
this._parseAACAudioSpecificConfig(
arrayBuffer,
dataOffset + 1,
dataSize - 1
);
} else {
this.dispatchEvent(
new demuxerChange('demuxer', {
event: 'audioData',
data: { data: array.subarray(1), timestamp: tagTimestamp },
})
);
}
}
_parseAACAudioSpecificConfig(arrayBuffer, dataOffset, dataSize) {
let 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
audioObjectType = originalAudioObjectType = array[0] >>> 3;
// 4 bits
samplingIndex = ((array[0] & 0x07) << 1) | (array[1] >>> 7);
if (samplingIndex < 0 || samplingIndex >= this._mpegSamplingRates.length) {
this._onError(
DemuxErrors.FORMAT_ERROR,
'Flv: AAC invalid sampling frequency index!'
);
return;
}
let samplingFrequence = this._mpegSamplingRates[samplingIndex];
// 4 bits
let channelConfig = (array[1] & 0x78) >>> 3;
if (channelConfig < 0 || channelConfig >= 8) {
this._onError(
DemuxErrors.FORMAT_ERROR,
'Flv: AAC invalid channel configuration'
);
return;
}
if (audioObjectType === 5) {
// HE-AAC?
// 4 bits
extensionSamplingIndex = ((array[1] & 0x07) << 1) | (array[2] >>> 7);
// 5 bits
audioExtensionObjectType = (array[2] & 0x7c) >>> 2;
}
// workarounds for various browsers
let userAgent = self.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;
}
this.dispatchEvent(
new demuxerChange('demuxer', {
event: 'audioConfig',
data: {
config: config,
samplingRate: samplingFrequence,
channelCount: channelConfig,
codec: 'mp4a.40.' + audioObjectType,
originalCodec: 'mp4a.40.' + originalAudioObjectType,
},
})
);
}
_parseVideoData(
arrayBuffer,
dataOffset,
dataSize,
tagTimestamp,
tagPosition
) {
if (dataSize <= 1) {
console.log('Flv: Invalid video packet, missing VideoData payload!');
return;
}
let spec = new Uint8Array(arrayBuffer, dataOffset, dataSize)[0];
// console.log(spec, 'video spec');
// 1: keyframe (for AVC, a seekable frame)
// 2: inter frame (for AVC, a non-seekable frame)
// 3: disposable inter frame (H.263 only)
// 4: generated keyframe (reserved for server use only)
// 5: video info/command frame
let frameType = (spec & 240) >>> 4;
// 1: JPEG (currently unused)
// 2: Sorenson H.263
// 3: Screen video
// 4: On2 VP6
// 5: On2 VP6 with alpha channel
// 6: Screen video version 2
// 7: AVC
let codecId = spec & 15;
// console.log(frameType, 'video frameType 帧类型');
// console.log(codecId, 'video codecId 编码ID');
let array = new Uint8Array(arrayBuffer, dataOffset + 1, dataSize - 1);
// console.log(array, 'video data');
// this.dispatchEvent(
// new demuxerChange('demuxer', {
// event: 'videoData',
// data: { array, tagTimestamp, frameType, codecId },
// })
// );
this._parseAVCVideoPacket(
arrayBuffer,
dataOffset + 1,
dataSize - 1,
tagTimestamp,
tagPosition,
frameType
);
}
_parseAVCVideoPacket(
arrayBuffer,
dataOffset,
dataSize,
tagTimestamp,
tagPosition,
frameType
) {
let le = this._littleEndian;
let v = new DataView(arrayBuffer, dataOffset, dataSize);
let packetType = v.getUint8(0);
let cts_unsigned = v.getUint32(0, !le) & 0x00ffffff;
let cts = (cts_unsigned << 8) >> 8; // convert to 24-bit signed int
// console.log(packetType, 'video packetType');
// console.log(cts, 'video tag body cts');
if (packetType === 0) {
this._parseAVCDecoderConfigurationRecord(
arrayBuffer,
dataOffset + 4,
dataSize - 4
);
} else if (packetType === 1) {
this._parseAVCVideoData(
arrayBuffer,
dataOffset + 4,
dataSize - 4,
tagTimestamp,
tagPosition,
frameType,
cts
);
}
}
_parseAVCDecoderConfigurationRecord(arrayBuffer, dataOffset, dataSize) {
if (dataSize < 7) {
console.log('Flv: Invalid AVCDecoderConfigurationRecord, lack of data!');
return;
}
let le = this._littleEndian;
let v = new DataView(arrayBuffer, dataOffset, dataSize);
let version = v.getUint8(0); // configurationVersion
let avcProfile = v.getUint8(1); // avcProfileIndication
let profileCompatibility = v.getUint8(2); // profile_compatibility
let avcLevel = v.getUint8(3);
this._naluLengthSize = (v.getUint8(4) & 3) + 1; // lengthSizeMinusOne
if (this._naluLengthSize !== 3 && this._naluLengthSize !== 4) {
// holy shit!!
return;
}
let spsCount = v.getUint8(5) & 31; // numOfSequenceParameterSets
let offset = 6;
var config = {};
let codecString = 'avc1.';
for (let i = 0; i < spsCount; i++) {
let len = v.getUint16(offset, !le); // sequenceParameterSetLength
offset += 2;
if (len === 0) {
continue;
}
var sps = new Uint8Array(arrayBuffer, dataOffset + offset, len);
offset += len;
config = SPSParser.parseSPS(sps);
if (i !== 0) {
// ignore other sps's config
continue;
}
// console.log(config, 'config');
let codecArray = sps.subarray(1, 4);
for (let j = 0; j < 3; j++) {
let h = codecArray[j].toString(16);
if (h.length < 2) {
h = '0' + h;
}
codecString += h;
}
// console.log(codecString, 'codecString');
}
let ppsCount = v.getUint8(offset); // numOfPictureParameterSets
offset++;
for (let i = 0; i < ppsCount; i++) {
let len = v.getUint16(offset, !le); // pictureParameterSetLength
offset += 2;
if (len === 0) {
continue;
}
var pps = new Uint8Array(arrayBuffer, dataOffset + offset, len);
// pps is useless for extracting video information
offset += len;
}
this.firstData = this._concatUint8(
new Uint8Array([0, 0, 0, 1]),
sps,
new Uint8Array([0, 0, 0, 1]),
pps
);
this.dispatchEvent(
new demuxerChange('demuxer', {
event: 'videoConfig',
data: Object.assign(config, { codecString }),
})
);
}
_parseAVCVideoData(
arrayBuffer,
dataOffset,
dataSize,
tagTimestamp,
tagPosition,
frameType,
cts
) {
let le = this._littleEndian;
let v = new DataView(arrayBuffer, dataOffset, dataSize);
let v2 = new DataView(arrayBuffer);
let units = [],
length = 0;
let offset = 0;
const lengthSize = this._naluLengthSize;
let dts = 0 + tagTimestamp;
let keyframe = frameType === 1; // from FLV Frame Type constants
while (offset < dataSize) {
let naluSize = v.getUint32(offset, !le); // Big-Endian read
if (lengthSize === 3) {
naluSize >>>= 8;
}
let unitType = v.getUint8(offset + lengthSize) & 0x1f;
if (unitType === 5) {
// IDR
keyframe = true;
}
let data = new Uint8Array(
arrayBuffer,
dataOffset + offset,
lengthSize + naluSize
);
data[0] = 0;
data[1] = 0;
data[2] = 0;
data[3] = 1;
// console.log(data, 'data111111');
// console.log(dataOffset + offset);
v2.setInt8(dataOffset + offset, 0);
v2.setInt8(dataOffset + offset + 1, 0);
v2.setInt8(dataOffset + offset + 2, 0);
v2.setInt8(dataOffset + offset + 3, 1);
// console.log(v2);
let unit = { type: unitType, data: data };
units.push(unit);
length += data.byteLength;
offset += lengthSize + naluSize;
}
if (units.length) {
let avcSample = {
units: units,
length: length,
isKeyframe: keyframe,
dts: dts,
cts: cts,
pts: dts + cts,
data: this.isFirstData
? this._concatUint8(
this.firstData,
new Uint8Array(arrayBuffer, dataOffset, dataSize)
)
: new Uint8Array(arrayBuffer, dataOffset, dataSize),
};
// console.log(avcSample, 'avcSample');
this.isFirstData = false;
this.dispatchEvent(
new demuxerChange('demuxer', {
event: 'videoData',
data: avcSample,
})
);
}
}
_parseScriptData(arrayBuffer, dataOffset, dataSize) {
let scriptData = AMF.parseScriptData(arrayBuffer, dataOffset, dataSize);
// console.log(scriptData, 'scriptData');
this.dispatchEvent(
new demuxerChange('demuxer', { event: 'scriptData', data: scriptData })
);
}
_concatUint8(...args) {
const length = args.reduce((len, cur) => (len += cur.byteLength), 0);
const result = new Uint8Array(length);
let offset = 0;
args.forEach((uint8) => {
result.set(uint8, offset);
offset += uint8.byteLength;
});
return result;
}
}
export default FLVDemuxer;