UNPKG

hjplayer

Version:

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

776 lines (663 loc) 28.7 kB
import EventEmitter from 'eventemitter3'; import Log from '../../../Utils/Logger'; import Events from '../Events/index'; import MP4 from './mp4-generator'; import AAC from './aac-silent'; import Browser from './browser'; import { MediaSegmentInfo, MediaSegmentInfoList } from './media-segment-info'; import SampleInfo from '../../../Utils/SampleInfo'; import { track, audioTrack, videoTrack, avcSample, aacSample, AvcSampleData, DetailAacSample } from '../Interface'; // Fragmented mp4 remuxer class MP4Remuxer { TAG = 'MP4Remuxer' emitter: EventEmitter _config: Record<string, any> _isLive: boolean _dtsBase: number _dtsBaseInited: boolean _audioDtsBase: number _videoDtsBase: number _audioNextDts: number | undefined _videoNextDts: number | undefined _audioStashedLastSample: aacSample | null _videoStashedLastSample: AvcSampleData | null _audioMeta: track | null _videoMeta: track | null _audioSegmentInfoList: MediaSegmentInfoList | null _videoSegmentInfoList: MediaSegmentInfoList | null _onInitSegment: null _onMediaSegment: null // Workaround for chrome < 50: Always force first sample as a Random Access Point in media segment // see https://bugs.chromium.org/p/chromium/issues/detail?id=229412 _forceFirstIDR: boolean // Workaround for IE11/Edge: Fill silent aac frame after keyframe-seeking // Make audio beginDts equals with video beginDts, in order to fix seek freeze _fillSilentAfterSeek: boolean // While only FireFox supports 'audio/mp4, codecs="mp3"', use 'audio/mpeg' for chrome, safari, ... _mp3UseMpegAudio: boolean _fillAudioTimestampGap: boolean _audioNextRefDts: any; constructor(emitter: EventEmitter, config: Record<string, any>) { this.TAG = 'MP4Remuxer'; this.emitter = emitter; this._config = config; this._isLive = config.isLive === true; this._dtsBase = -1; this._dtsBaseInited = false; this._audioDtsBase = Infinity; this._videoDtsBase = Infinity; this._audioNextRefDts = undefined; this._audioNextDts = undefined; this._videoNextDts = undefined; this._audioStashedLastSample = null; this._videoStashedLastSample = null; this._audioMeta = null; this._videoMeta = null; this._audioSegmentInfoList = new MediaSegmentInfoList('audio'); this._videoSegmentInfoList = new MediaSegmentInfoList('video'); // Workaround for chrome < 50: Always force first sample as a Random Access Point in media segment // see https://bugs.chromium.org/p/chromium/issues/detail?id=229412 this._forceFirstIDR = (Browser.chrome && (Browser.version.major < 50 || (Browser.version.major === 50 && Browser.version.build < 2661))); // Workaround for IE11/Edge: Fill silent aac frame after keyframe-seeking // Make audio beginDts equals with video beginDts, in order to fix seek freeze this._fillSilentAfterSeek = Browser.msedge || Browser.msie; // While only FireFox supports 'audio/mp4, codecs="mp3"', use 'audio/mpeg' for chrome, safari, ... this._mp3UseMpegAudio = !Browser.firefox; this._fillAudioTimestampGap = this._config.fixAudioTimestampGap; } destroy() { this._dtsBase = -1; this._dtsBaseInited = false; this._audioMeta = null; this._videoMeta = null; this._audioSegmentInfoList && this._audioSegmentInfoList.clear(); this._audioSegmentInfoList = null; this._videoSegmentInfoList && this._videoSegmentInfoList.clear(); this._videoSegmentInfoList = null; } insertDiscontinuity() { this._videoNextDts = undefined; this._audioNextDts = undefined; } seek() { this._audioStashedLastSample = null; this._videoStashedLastSample = null; this._videoSegmentInfoList && this._videoSegmentInfoList.clear(); this._audioSegmentInfoList && this._audioSegmentInfoList.clear(); } remux(audioTrack: audioTrack, videoTrack: videoTrack) { if(!this._dtsBaseInited) { this._calculateDtsBase(audioTrack, videoTrack); } this._remuxVideo(videoTrack); this._remuxAudio(audioTrack); } _onTrackMetadataReceived(type: string, metadata: track) { let metabox = null; let container = 'mp4'; let { codec } = metadata; if(type === 'audio') { this._audioMeta = metadata; if(metadata.codec === 'mp3' && this._mp3UseMpegAudio) { // 'audio/mpeg' for MP3 audio track container = 'mpeg'; codec = ''; metabox = new Uint8Array(); } else { // 'audio/mp4, codecs="codec"' metabox = MP4.generateInitSegment(metadata); } } else if(type === 'video') { this._videoMeta = metadata; metabox = MP4.generateInitSegment(metadata); } else { return; } this.emitter.emit(Events.INIT_SEGMENT, type, { type, data: metabox.buffer, codec, container: `${type}/${container}`, mediaDuration: metadata.duration // in timescale 1000 (milliseconds) }); } /** * 从音频序列和视频序列中取他们第一个sample的dts比较, 最小的为dts基准值 * @param audioTrack 音频序列 * @param videoTrack 视频序列 */ _calculateDtsBase(audioTrack: audioTrack, videoTrack: videoTrack) { if(this._dtsBaseInited) { return; } if(audioTrack.samples && audioTrack.samples.length) { this._audioDtsBase = audioTrack.samples[0].dts; } if(videoTrack.samples && videoTrack.samples.length) { this._videoDtsBase = videoTrack.samples[0].dts; } this._dtsBase = Math.min(this._audioDtsBase, this._videoDtsBase); this._dtsBaseInited = true; } flushStashedSamples() { const videoSample = this._videoStashedLastSample; const audioSample = this._audioStashedLastSample; const videoTrack: videoTrack = { type: 'video', id: 1, sequenceNumber: 0, samples: [], length: 0 }; if(videoSample != null) { videoTrack.samples.push(videoSample); videoTrack.length = videoSample.length; } const audioTrack: audioTrack = { type: 'audio', id: 2, sequenceNumber: 0, samples: [], length: 0 }; if(audioSample != null) { audioTrack.samples.push(audioSample); audioTrack.length = audioSample.length; } this._videoStashedLastSample = null; this._audioStashedLastSample = null; this._remuxVideo(videoTrack, true); this._remuxAudio(audioTrack, true); } _remuxAudio(audioTrack: audioTrack, force?: boolean) { if(this._audioMeta == null) { return; } const track = audioTrack; const { samples } = track; let dtsCorrection; let firstDts = -1; let lastDts = -1; const lastPts = -1; const { refSampleDuration } = this._audioMeta; const mpegRawTrack = this._audioMeta.codec === 'mp3' && this._mp3UseMpegAudio; const firstSegmentAfterSeek = this._dtsBaseInited && this._audioNextDts === undefined; let insertPrefixSilentFrame = false; if(!samples || samples.length === 0) { return; } if(samples.length === 1 && !force) { // If [sample count in current batch] === 1 && (force != true) // Ignore and keep in demuxer's queue return; } // else if (force === true) do remux let offset = 0; let mdatbox = null; let mdatBytes = 0; // calculate initial mdat size if(mpegRawTrack) { // for raw mpeg buffer offset = 0; mdatBytes = track.length; } else { // for fmp4 mdat box offset = 8; // size + type mdatBytes = 8 + track.length; } let lastSample = null; // Pop the lastSample and waiting for stash if(samples.length > 1) { lastSample = samples.pop(); lastSample && (mdatBytes -= lastSample.length); } // Insert [stashed lastSample in the previous batch] to the front if(this._audioStashedLastSample != null) { const sample = this._audioStashedLastSample; this._audioStashedLastSample = null; samples.unshift(sample); mdatBytes += sample.length; } // Stash the lastSample of current batch, waiting for next batch if(lastSample != null) { this._audioStashedLastSample = lastSample; } const firstSampleOriginalDts = samples[0].dts - this._dtsBase; // calculate dtsCorrection if(this._audioNextDts) { dtsCorrection = firstSampleOriginalDts - this._audioNextDts; } else { // this._audioNextDts == undefined if(this._audioSegmentInfoList && this._audioSegmentInfoList.isEmpty()) { dtsCorrection = 0; if(this._fillSilentAfterSeek && this._videoSegmentInfoList && !this._videoSegmentInfoList.isEmpty()) { if(this._audioMeta.originalCodec !== 'mp3') { insertPrefixSilentFrame = true; } } } else { const lastSample = this._audioSegmentInfoList!.getLastSampleBefore(firstSampleOriginalDts); if(lastSample != null) { let distance = (firstSampleOriginalDts - (lastSample.originalDts + lastSample.duration)); if(distance <= 3) { distance = 0; } const expectedDts = lastSample.dts + lastSample.duration + distance; dtsCorrection = firstSampleOriginalDts - expectedDts; } else { // lastSample == null, cannot found dtsCorrection = 0; } } } if(insertPrefixSilentFrame) { // align audio segment beginDts to match with current video segment's beginDts const firstSampleDts = firstSampleOriginalDts - dtsCorrection; const videoSegment = this._videoSegmentInfoList!.getLastSegmentBefore(firstSampleOriginalDts); if(videoSegment != null && videoSegment.beginDts < firstSampleDts) { const silentUnit = AAC.getSilentFrame(this._audioMeta.originalCodec, this._audioMeta.channelCount); if(silentUnit) { const dts = videoSegment.beginDts; const silentFrameDuration = firstSampleDts - videoSegment.beginDts; Log.info(this.TAG, `InsertPrefixSilentAudio: dts: ${dts}, duration: ${silentFrameDuration}`); samples.unshift({ unit: silentUnit, dts, pts: dts, cts: 0, length: silentUnit.byteLength }); mdatBytes += silentUnit.byteLength; } // silentUnit == null: Cannot generate, skip } else { insertPrefixSilentFrame = false; } } let mp4Samples: Array<DetailAacSample> = []; // Correct dts for each sample, and calculate sample duration. Then output to mp4Samples for(let i = 0; i < samples.length; i++) { const sample = samples[i]; const { unit } = sample; const originalDts = sample.dts - this._dtsBase; let dts = originalDts; let needFillSilentFrames = false; let silentFrames = null; let sampleDuration = 0; if(originalDts < -0.001) { continue; // pass the first sample with the invalid dts } if(this._audioMeta.codec !== 'mp3') { // for AAC codec, we need to keep dts increase based on refSampleDuration let curRefDts = originalDts; const maxAudioFramesDrift = 3; if(this._audioNextRefDts) { curRefDts = this._audioNextRefDts; } const delta = originalDts - curRefDts; if(delta <= -maxAudioFramesDrift * refSampleDuration) { // If we're overlapping by more than maxAudioFramesDrift number of frame, drop this sample Log.warn(this.TAG, `Dropping 1 audio frame (originalDts: ${originalDts} ms ,curRefDts: ${curRefDts} ms) due to delta: ${delta} ms overlap.`); continue; } else if(delta >= maxAudioFramesDrift * refSampleDuration && this._fillAudioTimestampGap && !Browser.safari) { // Silent frame generation, if large timestamp gap detected && config.fixAudioTimestampGap needFillSilentFrames = true; // We need to insert silent frames to fill timestamp gap const frameCount = Math.floor(delta / refSampleDuration); Log.warn(this.TAG, `Large audio timestamp gap detected, may cause AV sync to drift. Silent frames will be generated to avoid unsync. originalDts: ${originalDts} ms, curRefDts: ${curRefDts} ms, delta: ${Math.round(delta)} ms, generate: ${frameCount} frames`); dts = Math.floor(curRefDts); sampleDuration = Math.floor(curRefDts + refSampleDuration) - dts; curRefDts += refSampleDuration; let silentUnit = AAC.getSilentFrame(this._audioMeta.originalCodec, this._audioMeta.channelCount); if(silentUnit == null) { Log.warn(this.TAG, `Unable to generate silent frame for ${this._audioMeta.originalCodec} with ${this._audioMeta.channelCount} channels, repeat last frame`); // Repeat last frame silentUnit = unit; } silentFrames = []; for(let j = 0; j < frameCount; j++) { const intDts = Math.floor(curRefDts); // change to integer const intDuration = Math.floor(curRefDts + refSampleDuration) - intDts; const frame = { dts: intDts, pts: intDts, cts: 0, unit: silentUnit, size: silentUnit.byteLength, duration: intDuration, // wait for next sample originalDts, flags: { isLeading: 0, dependsOn: 1, isDependedOn: 0, hasRedundancy: 0 } }; silentFrames.push(frame); mdatBytes += unit.byteLength; curRefDts += refSampleDuration; } this._audioNextRefDts = curRefDts + refSampleDuration; } else { dts = Math.floor(curRefDts); sampleDuration = Math.floor(curRefDts + refSampleDuration) - dts; this._audioNextRefDts = curRefDts + refSampleDuration; } } else { // keep the original dts calculate algorithm for mp3 dts = originalDts - dtsCorrection; if(i !== samples.length - 1) { const nextDts = samples[i + 1].dts - this._dtsBase - dtsCorrection; sampleDuration = nextDts - dts; } else { // the last sample if(lastSample != null) { // use stashed sample's dts to calculate sample duration const nextDts = lastSample.dts - this._dtsBase - dtsCorrection; sampleDuration = nextDts - dts; } else if(mp4Samples.length >= 1) { // use second last sample duration sampleDuration = mp4Samples[mp4Samples.length - 1].duration; } else { // the only one sample, use reference sample duration sampleDuration = Math.floor(refSampleDuration); } } } if(firstDts === -1) { firstDts = dts; } mp4Samples.push({ dts, pts: dts, cts: 0, unit: sample.unit, size: sample.unit.byteLength, duration: sampleDuration, originalDts, flags: { isLeading: 0, dependsOn: 1, isDependedOn: 0, hasRedundancy: 0 } }); if(needFillSilentFrames) { // Silent frames should be inserted after wrong-duration frame // mp4Samples.push.apply(mp4Samples, silentFrames); mp4Samples = [...mp4Samples, ...silentFrames]; } } if(mp4Samples.length === 0) { // no samples need to remux track.samples = []; track.length = 0; return; } // allocate mdatbox if(mpegRawTrack) { // allocate for raw mpeg buffer mdatbox = new Uint8Array(mdatBytes); } else { // allocate for fmp4 mdat box mdatbox = new Uint8Array(mdatBytes); // size field mdatbox[0] = (mdatBytes >>> 24) & 0xFF; mdatbox[1] = (mdatBytes >>> 16) & 0xFF; mdatbox[2] = (mdatBytes >>> 8) & 0xFF; mdatbox[3] = (mdatBytes) & 0xFF; // type field (fourCC) mdatbox.set(MP4.types.mdat, 4); } // Write samples into mdatbox for(let i = 0; i < mp4Samples.length; i++) { const { unit } = mp4Samples[i]; mdatbox.set(unit, offset); offset += unit.byteLength; } const latest = mp4Samples[mp4Samples.length - 1]; lastDts = latest.dts + latest.duration; this._audioNextDts = lastDts; // fill media segment info & add to info list const info = new MediaSegmentInfo(); info.beginDts = firstDts; info.endDts = lastDts; info.beginPts = firstDts; info.endPts = lastDts; info.originalBeginDts = mp4Samples[0].originalDts; info.originalEndDts = latest.originalDts + latest.duration; info.firstSample = new SampleInfo(mp4Samples[0].dts, mp4Samples[0].pts, mp4Samples[0].duration, mp4Samples[0].originalDts, false); info.lastSample = new SampleInfo(latest.dts, latest.pts, latest.duration, latest.originalDts, false); if(!this._isLive) { this._audioSegmentInfoList!.append(info); } track.samples = mp4Samples as any; track.sequenceNumber++; let moofbox = null; if(mpegRawTrack) { // Generate empty buffer, because useless for raw mpeg moofbox = new Uint8Array(); } else { // Generate moof for fmp4 segment moofbox = MP4.moof(track, firstDts); } track.samples = []; track.length = 0; let segment; if(mpegRawTrack && firstSegmentAfterSeek) { // For MPEG audio stream in MSE, if seeking occurred, before appending new buffer // We need explicitly set timestampOffset to the desired point in timeline for mpeg SourceBuffer. segment = { type: 'audio', data: this._mergeBoxes(moofbox, mdatbox).buffer, sampleCount: mp4Samples.length, info, timestampOffset: firstDts }; } else { segment = { type: 'audio', data: this._mergeBoxes(moofbox, mdatbox).buffer, sampleCount: mp4Samples.length, info, }; } this.emitter.emit(Events.MEDIA_SEGMENT, 'audio', segment); // this._onMediaSegment('audio', segment) } _remuxVideo(videoTrack: videoTrack, force?: boolean) { if(this._videoMeta == null) { return; } const track = videoTrack; const { samples } = track; let dtsCorrection; let firstDts = -1; let lastDts = -1; let firstPts = -1; let lastPts = -1; if(!samples || samples.length === 0) { return; } if(samples.length === 1 && !force) { // If [sample count in current batch] === 1 && (force != true) // Ignore and keep in demuxer's queue return; } // else if (force === true) do remux let offset = 8; let mdatbox = null; let mdatBytes = 8 + videoTrack.length; let lastSample = null; // Pop the lastSample and waiting for stash if(samples.length > 1) { lastSample = samples.pop(); lastSample && (mdatBytes -= lastSample.length); } // Insert [stashed lastSample in the previous batch] to the front if(this._videoStashedLastSample != null) { const sample = this._videoStashedLastSample; this._videoStashedLastSample = null; samples.unshift(sample); mdatBytes += sample.length; } // Stash the lastSample of current batch, waiting for next batch if(lastSample != null) { this._videoStashedLastSample = lastSample; } const firstSampleOriginalDts = samples[0].dts - this._dtsBase; // calculate dtsCorrection if(this._videoNextDts) { dtsCorrection = firstSampleOriginalDts - this._videoNextDts; } else { // this._videoNextDts == undefined if(this._videoSegmentInfoList && this._videoSegmentInfoList.isEmpty()) { dtsCorrection = 0; } else { const lastSample = this._videoSegmentInfoList && this._videoSegmentInfoList.getLastSampleBefore(firstSampleOriginalDts); if(lastSample != null) { let distance = firstSampleOriginalDts - (lastSample.originalDts + lastSample.duration); if(distance <= 3) { distance = 0; } const expectedDts = lastSample.dts + lastSample.duration + distance; dtsCorrection = firstSampleOriginalDts - expectedDts; } else { // lastSample == null, cannot found dtsCorrection = 0; } } } const info = new MediaSegmentInfo(); const mp4Samples = []; // Correct dts for each sample, and calculate sample duration. Then output to mp4Samples for(let i = 0; i < samples.length; i++) { const sample = samples[i]; const originalDts = sample.dts - this._dtsBase; const { isKeyframe } = sample; const dts = originalDts - dtsCorrection; const { cts } = sample; const pts = dts + cts; if(firstDts === -1) { firstDts = dts; firstPts = pts; } let sampleDuration = 0; if(i !== samples.length - 1) { const nextDts = samples[i + 1].dts - this._dtsBase - dtsCorrection; sampleDuration = nextDts - dts; } else { // the last sample if(lastSample != null) { // use stashed sample's dts to calculate sample duration const nextDts = lastSample.dts - this._dtsBase - dtsCorrection; sampleDuration = nextDts - dts; } else if(mp4Samples.length >= 1) { // use second last sample duration sampleDuration = mp4Samples[mp4Samples.length - 1].duration; } else { // the only one sample, use reference sample duration sampleDuration = Math.floor(this._videoMeta.refSampleDuration); } } if(isKeyframe) { const syncPoint = new SampleInfo(dts, pts, sampleDuration, sample.dts, true); syncPoint.fileposition = <number>sample.fileposition; info.appendSyncPoint(syncPoint); } mp4Samples.push({ dts, pts, cts, isKeyframe, originalDts, units: sample.units, size: sample.length, duration: sampleDuration, flags: { isLeading: 0, dependsOn: isKeyframe ? 2 : 1, isDependedOn: isKeyframe ? 1 : 0, hasRedundancy: 0, isNonSync: isKeyframe ? 0 : 1 } }); } // allocate mdatbox mdatbox = new Uint8Array(mdatBytes); mdatbox[0] = (mdatBytes >>> 24) & 0xff; mdatbox[1] = (mdatBytes >>> 16) & 0xff; mdatbox[2] = (mdatBytes >>> 8) & 0xff; mdatbox[3] = mdatBytes & 0xff; mdatbox.set(MP4.types.mdat, 4); // Write samples into mdatbox for(let i = 0; i < mp4Samples.length; i++) { const { units } = mp4Samples[i]; while(units.length) { const unit = units.shift(); const { data } = unit!; mdatbox.set(data, offset); offset += data.byteLength; } } const latest = mp4Samples[mp4Samples.length - 1]; lastDts = latest.dts + latest.duration; lastPts = latest.pts + latest.duration; this._videoNextDts = lastDts; // fill media segment info & add to info list info.beginDts = firstDts; info.endDts = lastDts; info.beginPts = firstPts; info.endPts = lastPts; info.originalBeginDts = mp4Samples[0].originalDts; info.originalEndDts = latest.originalDts + latest.duration; info.firstSample = new SampleInfo( mp4Samples[0].dts, mp4Samples[0].pts, mp4Samples[0].duration, mp4Samples[0].originalDts, mp4Samples[0].isKeyframe ); info.lastSample = new SampleInfo( latest.dts, latest.pts, latest.duration, latest.originalDts, latest.isKeyframe ); if(!this._isLive) { this._videoSegmentInfoList && this._videoSegmentInfoList.append(info); } track.samples = mp4Samples as Array<any>; track.sequenceNumber++; // workaround for chrome < 50: force first sample as a random access point // see https://bugs.chromium.org/p/chromium/issues/detail?id=229412 if(this._forceFirstIDR) { const { flags } = mp4Samples[0]; flags.dependsOn = 2; flags.isNonSync = 0; } const moofbox = MP4.moof(track, firstDts); track.samples = []; track.length = 0; this.emitter.emit(Events.MEDIA_SEGMENT, 'video', { type: 'video', data: this._mergeBoxes(moofbox, mdatbox).buffer, sampleCount: mp4Samples.length, info }); } /** * 将两个MP4 BOX合并成一个 Uint8Array 并返回 * @param moof moof 盒子 * @param mdat mdat盒子 */ _mergeBoxes(moof: Uint8Array, mdat: Uint8Array): Uint8Array { const result = new Uint8Array(moof.byteLength + mdat.byteLength); result.set(moof, 0); result.set(mdat, moof.byteLength); return result; } } export default MP4Remuxer;