mirror of
https://github.com/bytedance/xgplayer.git
synced 2025-04-05 03:05:02 +08:00
321 lines
10 KiB
JavaScript
321 lines
10 KiB
JavaScript
import Demuxer from './Demuxer';
|
||
import SPSParser from '../SPSParser';
|
||
import DataView4Read from '../../utils/DataView4Read';
|
||
import { EventTypes } from '../../constants/types';
|
||
import Buffer from '../../write/Buffer';
|
||
export default class VideoDemuxer extends Demuxer {
|
||
constructor (store) {
|
||
super(store);
|
||
this.CLASS_NAME = this.constructor.name;
|
||
this.readOffset = 0;
|
||
this.data = new Uint8Array(0);
|
||
this.currentTag = null;
|
||
this._store.videoMetaData = null;
|
||
}
|
||
|
||
resetStatus () {
|
||
this.readOffset = 0;
|
||
this.data = new Uint8Array(0);
|
||
this.currentTag = null;
|
||
}
|
||
|
||
resolve (tag) {
|
||
this.data = tag.body;
|
||
this.currentTag = tag;
|
||
const firstUI8 = this.readData(1)[0];
|
||
const frameType = (firstUI8 & 0xF0) >>> 4;
|
||
const codecId = firstUI8 & 0x0F;
|
||
if (codecId !== 7) {
|
||
|
||
/** 1: JPEG
|
||
* 2: H263
|
||
* 3: Screen video
|
||
* 4: On2 VP6
|
||
* 5: On2 VP6
|
||
* 6: Screen videoversion 2
|
||
* 7: AVC
|
||
*/
|
||
this.error(`unsupported codecId: ${codecId}`);
|
||
return;
|
||
}
|
||
this._parseAVCPacket(frameType);
|
||
|
||
this.resetStatus();
|
||
}
|
||
|
||
_parseAVCPacket (frameType) {
|
||
if (this.unreadLength < 4) {
|
||
this.error('Invalid Avc Tag');
|
||
}
|
||
const isLe = this._store.isLe;
|
||
const { buffer } = this.data;
|
||
const dv = new DataView(buffer, this.readOffset, this.unreadLength);
|
||
const packageType = dv.getUint8(0);
|
||
|
||
let cpsTime = dv.getUint32(0, !isLe) & 0x00FFFFFF;
|
||
cpsTime = (cpsTime << 8) >> 8;
|
||
this.readOffset += 4;
|
||
|
||
switch (packageType) {
|
||
case 0: {
|
||
const { position, tagSize } = this.currentTag;
|
||
|
||
this._store.metaEndPosition = position + Buffer.readAsInt(tagSize) + 4; // 缓存scriptTag结束的位置,replay使用
|
||
this._parseAVCDecoderConfigurationRecord();
|
||
break;
|
||
}
|
||
case 1: {
|
||
this._parseAVCVideoData(frameType, cpsTime);
|
||
break;
|
||
}
|
||
case 2: {
|
||
break;
|
||
}
|
||
default: {
|
||
// 报错
|
||
}
|
||
}
|
||
}
|
||
|
||
_parseAVCDecoderConfigurationRecord () {
|
||
if (this.unreadLength < 7) {
|
||
this.error('Invalid AVCDecoderConfigurationRecord, lack of data!');
|
||
return;
|
||
}
|
||
|
||
const { mediaInfo: mi } = this._store;
|
||
// stash offset&unreadSize before parsing sps&pps
|
||
const tempOffset = this.readOffset;
|
||
const tempUnreadLength = this.unreadLength;
|
||
|
||
|
||
const { _store: store } = this;
|
||
let meta = this._store.videoMetaData;
|
||
let track = this._store.videoTrack;
|
||
const dv = new DataView4Read(this.data.buffer, this);
|
||
if (meta) {
|
||
if (meta.avcc !== undefined) {
|
||
this.error('found another AVCDecoderConfigurationRecord!');
|
||
}
|
||
} else {
|
||
if (!store.state._hasVideo && !store.state.hasVideoFlagOverrided) {
|
||
store.state._hasVideo = true;
|
||
store._mediaInfo.hasVideo = true;
|
||
}
|
||
meta = store.videoMetaData = {};
|
||
meta.type = 'video';
|
||
meta.id = track.id;
|
||
meta.timeScale = store.videoTimeScale;
|
||
meta.duration = store.state.duration;
|
||
mi.timescale = store.videoTimeScale;
|
||
}
|
||
|
||
const version = dv.getUint8();
|
||
const avcProfile = dv.getUint8();
|
||
dv.getUint8();
|
||
dv.getUint8();
|
||
if (version !== 1 || avcProfile === 0) {
|
||
// 处理错误
|
||
return;
|
||
}
|
||
|
||
const naluLengthSize = store.state.naluLengthSize = dv.getUint(2, this.readOffset, false) + 1;
|
||
if (naluLengthSize !== 3 && naluLengthSize !== 4) {
|
||
// 处理错误
|
||
return;
|
||
}
|
||
|
||
const spsLength = dv.getUint(5, null, false);
|
||
if (spsLength === 0) {
|
||
// 处理错误
|
||
return;
|
||
} else if (spsLength > 1) {
|
||
this.warn('AVCDecoderConfigurationRecord: spsLength > 1');
|
||
}
|
||
let sps;
|
||
for (let i = 0; i < spsLength; i++) {
|
||
const len = dv.getUint16();
|
||
|
||
if (len === 0) {
|
||
continue;
|
||
}
|
||
sps = new Uint8Array(this.data.buffer, this.readOffset, len);
|
||
this.readOffset += len;
|
||
const spsConfig = SPSParser.parseSPS(sps);
|
||
|
||
if (i !== 0) {
|
||
continue;
|
||
}
|
||
|
||
const {
|
||
codecSize,
|
||
presentSize,
|
||
profileString,
|
||
levelString,
|
||
chromaFormat,
|
||
pixelRatio,
|
||
frameRate,
|
||
refFrames,
|
||
bitDepth,
|
||
} = spsConfig;
|
||
|
||
meta.width = codecSize.width;
|
||
meta.height = codecSize.height;
|
||
meta.presentWidth = presentSize.width;
|
||
meta.presentHeight = presentSize.height;
|
||
|
||
meta.profile = profileString;
|
||
meta.level = levelString;
|
||
// meta.profileCompatibility = profileCompatibility;
|
||
// meta.naluLengthSize = naluLengthSize;
|
||
|
||
meta.bitDepth = bitDepth;
|
||
meta.chromaFormat = chromaFormat;
|
||
meta.pixelRatio = pixelRatio;
|
||
meta.frameRate = frameRate;
|
||
|
||
if (!frameRate.fixed
|
||
|| frameRate.fpsNum === 0
|
||
|| frameRate.fpsDen === 0) {
|
||
meta.frameRate = store.referFrameRate;
|
||
}
|
||
|
||
let { fpsDen, fpsNum } = meta.frameRate;
|
||
meta.refSampleDuration = meta.timeScale * (fpsDen / fpsNum);
|
||
|
||
let codecArr = sps.subarray(1, 4);
|
||
let codecStr = 'avc1.';
|
||
for (let j = 0; j < 3; j++) {
|
||
let hex = codecArr[j].toString(16);
|
||
hex = hex.padStart(2, '0');
|
||
codecStr += hex;
|
||
}
|
||
|
||
meta.codec = codecStr;
|
||
|
||
const { mediaInfo: mi } = this._store;
|
||
mi.width = meta.width;
|
||
mi.height = meta.height;
|
||
mi.fps = meta.frameRate.fps;
|
||
mi.profile = meta.profile;
|
||
mi.level = meta.level;
|
||
mi.refFrames = refFrames;
|
||
mi.pixelRatio = pixelRatio;
|
||
mi.videoCodec = codecStr;
|
||
mi.chromaFormat = chromaFormat;
|
||
if (mi.hasAudio) {
|
||
if (mi.audioCodec) {
|
||
mi.mimeType = `video/x-flv; codecs="${mi.videoCodec},${mi.audioCodec}"`;
|
||
mi.codec = mi.mimeType.replace('x-flv', 'mp4');
|
||
}
|
||
} else {
|
||
mi.mimeType = `video/x-flv; codecs="${mi.videoCodec}"`;
|
||
mi.codec = mi.mimeType.replace('x-flv', 'mp4');
|
||
}
|
||
|
||
if (mi.isComplete) {
|
||
this.dispatch(EventTypes.MEDIA_INFO_READY, mi);
|
||
}
|
||
}
|
||
let pps;
|
||
const ppsCount = dv.getUint8();
|
||
if (!ppsCount) {
|
||
this.dispatch(EventTypes.ERROR, 'no pps in AVCDecoderConfigurationRecord');
|
||
return;
|
||
} else if (ppsCount > 1) {
|
||
this.warn(`AVCDecoderConfigurationRecord has ppsCount: ${ppsCount}`);
|
||
}
|
||
|
||
for (let i = 0; i < ppsCount; i++) {
|
||
let ppsSize = dv.getUint16();
|
||
|
||
if (!ppsSize) {
|
||
continue;
|
||
}
|
||
|
||
pps = new Uint8Array(this.data.buffer, this.readOffset, ppsSize);
|
||
this.readOffset += ppsSize;
|
||
}
|
||
|
||
mi.sps = meta.sps = sps;
|
||
mi.pps = meta.pps = pps;
|
||
if (store.hasInitialMetaDispatched) {
|
||
if (store.videoTrack.length || store.audioTrack.length) {
|
||
this.dispatch(EventTypes.DATA_READY, store.videoTrack, store.audioTrack);
|
||
}
|
||
} else {
|
||
store.state._videoInitialMetadataDispatched = true;
|
||
}
|
||
|
||
this.dispatch(EventTypes.META_DATA_READY, 'video', meta);
|
||
}
|
||
|
||
_parseAVCVideoData (frameType, cpsTime) {
|
||
let dv = new DataView4Read(this.data.buffer, this);
|
||
|
||
let naluList = [], dataLen = 0;
|
||
const { naluLengthSize: naluLenSize } = this._store.state;
|
||
let ts = this._store.state.timeStampBase + this.currentTag.getTime();
|
||
let isKeyframe = (frameType === 1);
|
||
while (this.unreadLength > 0) {
|
||
if (this.unreadLength < 4) {
|
||
this.warn('not enough data for parsing AVC');
|
||
break;
|
||
}
|
||
const tempReadOffset = this.readOffset;
|
||
let naluSize = naluLenSize === 4 ? dv.getUint32() : dv.getUint24();
|
||
if (naluSize > this.unreadLength) {
|
||
return;
|
||
}
|
||
|
||
let unitType = dv.getUint(5, this.readOffset, false);
|
||
|
||
if (unitType === 5) {
|
||
isKeyframe = true;
|
||
}
|
||
|
||
let data = new Uint8Array(this.data.buffer, tempReadOffset, naluLenSize + naluSize);
|
||
this.readOffset = tempReadOffset + naluLenSize + naluSize;
|
||
const naluUnit = {
|
||
type: unitType,
|
||
data,
|
||
};
|
||
naluList.push(naluUnit);
|
||
dataLen += data.byteLength;
|
||
}
|
||
dv = null;
|
||
if (naluList.length) {
|
||
const { videoTrack } = this._store;
|
||
const videoSample = {
|
||
units: naluList,
|
||
length: dataLen,
|
||
dts: ts,
|
||
cps: cpsTime,
|
||
pts: (ts + cpsTime),
|
||
isKeyframe,
|
||
position: isKeyframe ? this.currentTag.position : undefined,
|
||
};
|
||
videoTrack.samples.push(videoSample);
|
||
videoTrack.length += dataLen;
|
||
}
|
||
|
||
}
|
||
|
||
readData (num) {
|
||
const { data, readOffset } = this;
|
||
if (this.dataSize > readOffset + num) {
|
||
this.readOffset += num;
|
||
return data.slice(readOffset, num);
|
||
}
|
||
return [];
|
||
}
|
||
|
||
get dataSize () {
|
||
return this.data.length;
|
||
}
|
||
get unreadLength () {
|
||
return this.dataSize - this.readOffset;
|
||
}
|
||
}
|
||
|