mirror of
https://github.com/video-dev/hls.js.git
synced 2026-05-17 13:30:38 +00:00
Workaround Chrome silent coded frame eviction by removing later segments from buffer when PTS overlap is detected
Resolves #6777
This commit is contained in:
@@ -2038,7 +2038,7 @@ export class FragmentTracker implements ComponentAPI {
|
||||
addAsGap(frag: MediaFragment): void;
|
||||
// (undocumented)
|
||||
destroy(): void;
|
||||
detectEvictedFragments(elementaryStream: SourceBufferName, timeRange: TimeRanges, playlistType: PlaylistLevelType, appendedPart?: Part | null, removeAppending?: boolean): void;
|
||||
detectEvictedFragments(elementaryStream: SourceBufferName, timeRange: TimeRanges, playlistType: PlaylistLevelType, appendedFrag?: MediaFragment | null, appendedPart?: Part | null, removeAppending?: boolean): void;
|
||||
detectPartialFragments(data: FragBufferedData): void;
|
||||
// (undocumented)
|
||||
fragBuffered(frag: MediaFragment, force?: true): void;
|
||||
|
||||
@@ -419,6 +419,7 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe
|
||||
bufferedTimeRanges,
|
||||
playlistType,
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
type MediaFragment,
|
||||
type Part,
|
||||
} from '../loader/fragment';
|
||||
import { userAgentChromeVersion } from '../utils/user-agent';
|
||||
import type Hls from '../hls';
|
||||
import type { SourceBufferName } from '../types/buffer';
|
||||
import type { ComponentAPI } from '../types/component-api';
|
||||
@@ -154,6 +155,7 @@ export class FragmentTracker implements ComponentAPI {
|
||||
elementaryStream: SourceBufferName,
|
||||
timeRange: TimeRanges,
|
||||
playlistType: PlaylistLevelType,
|
||||
appendedFrag?: MediaFragment | null,
|
||||
appendedPart?: Part | null,
|
||||
removeAppending?: boolean,
|
||||
) {
|
||||
@@ -165,18 +167,19 @@ export class FragmentTracker implements ComponentAPI {
|
||||
const appendedPartSn = appendedPart?.fragment.sn || -1;
|
||||
Object.keys(this.fragments).forEach((key) => {
|
||||
const fragmentEntity = this.fragments[key];
|
||||
if (!fragmentEntity) {
|
||||
if (!fragmentEntity || !this.hls) {
|
||||
return;
|
||||
}
|
||||
if (appendedPartSn >= fragmentEntity.body.sn) {
|
||||
const frag = fragmentEntity.body;
|
||||
if (appendedPartSn >= frag.sn) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!fragmentEntity.buffered &&
|
||||
(!fragmentEntity.loaded || removeAppending)
|
||||
) {
|
||||
if (fragmentEntity.body.type === playlistType) {
|
||||
this.removeFragment(fragmentEntity.body);
|
||||
if (frag.type === playlistType) {
|
||||
this.removeFragment(frag);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -185,7 +188,7 @@ export class FragmentTracker implements ComponentAPI {
|
||||
return;
|
||||
}
|
||||
if (esData.time.length === 0) {
|
||||
this.removeFragment(fragmentEntity.body);
|
||||
this.removeFragment(frag);
|
||||
return;
|
||||
}
|
||||
esData.time.some((time: FragmentTimeRange) => {
|
||||
@@ -196,10 +199,29 @@ export class FragmentTracker implements ComponentAPI {
|
||||
);
|
||||
if (isNotBuffered) {
|
||||
// Unregister partial fragment as it needs to load again to be reused
|
||||
this.removeFragment(fragmentEntity.body);
|
||||
this.removeFragment(frag);
|
||||
}
|
||||
return isNotBuffered;
|
||||
});
|
||||
// Flush forward buffer in Chrome when PTS overlap detected
|
||||
// Workaround https://github.com/video-dev/hls.js/issues/6777 (https://issues.chromium.org/u/1/issues/336839131)
|
||||
if (appendedFrag && appendedFrag !== frag && userAgentChromeVersion()) {
|
||||
const endPTS = appendedFrag.endPTS;
|
||||
const otherStartPTS = frag.startPTS;
|
||||
if (endPTS && otherStartPTS && fragmentEntity.range.video) {
|
||||
const diff = otherStartPTS - endPTS;
|
||||
// overlap is no more than 1/10s
|
||||
if (diff < 0 && diff > -0.1) {
|
||||
this.removeFragment(frag);
|
||||
this.hls.trigger(Events.BUFFER_FLUSHING, {
|
||||
// pad removal start to avoid accedental removal of appendedFrag
|
||||
startOffset: endPTS + 0.004,
|
||||
endOffset: Infinity,
|
||||
type: 'video',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -236,6 +258,7 @@ export class FragmentTracker implements ComponentAPI {
|
||||
elementaryStream,
|
||||
timeRange,
|
||||
playlistType,
|
||||
frag,
|
||||
part,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -16,6 +16,10 @@ import {
|
||||
removeEventListener,
|
||||
} from '../utils/event-listener-helper';
|
||||
import { useAlternateAudio } from '../utils/rendition-helper';
|
||||
import {
|
||||
userAgentFirefoxVersion,
|
||||
userAgentIsAndroidLike,
|
||||
} from '../utils/user-agent';
|
||||
import type { FragmentTracker } from './fragment-tracker';
|
||||
import type Hls from '../hls';
|
||||
import type { Fragment, MediaFragment } from '../loader/fragment';
|
||||
@@ -1428,7 +1432,6 @@ export default class StreamController
|
||||
audioCodec = 'mp4a.40.5';
|
||||
}
|
||||
// Handle `audioCodecSwitch`
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
if (this.audioCodecSwitch) {
|
||||
if (audioCodec) {
|
||||
if (audioCodec.indexOf('mp4a.40.5') !== -1) {
|
||||
@@ -1445,7 +1448,7 @@ export default class StreamController
|
||||
audioMetadata &&
|
||||
'channelCount' in audioMetadata &&
|
||||
(audioMetadata.channelCount || 1) !== 1 &&
|
||||
ua.indexOf('firefox') === -1
|
||||
!userAgentFirefoxVersion()
|
||||
) {
|
||||
audioCodec = 'mp4a.40.5';
|
||||
}
|
||||
@@ -1454,7 +1457,7 @@ export default class StreamController
|
||||
if (
|
||||
audioCodec &&
|
||||
audioCodec.indexOf('mp4a.40.5') !== -1 &&
|
||||
ua.indexOf('android') !== -1 &&
|
||||
userAgentIsAndroidLike() &&
|
||||
audio.container !== 'audio/mpeg'
|
||||
) {
|
||||
// Exclude mpeg audio
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
/**
|
||||
* MPEG parser helper
|
||||
*/
|
||||
import { userAgentChromeVersion } from '../../utils/user-agent';
|
||||
import type { DemuxedAudioTrack } from '../../types/demuxer';
|
||||
|
||||
let chromeVersion: number | null = null;
|
||||
|
||||
const BitratesMap = [
|
||||
32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 32, 48, 56,
|
||||
64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 32, 40, 48, 56, 64, 80,
|
||||
@@ -115,11 +114,7 @@ export function parseHeader(data: Uint8Array, offset: number) {
|
||||
Math.floor((sampleCoefficient * bitRate) / sampleRate + paddingBit) *
|
||||
bytesInSlot;
|
||||
|
||||
if (chromeVersion === null) {
|
||||
const userAgent = navigator.userAgent || '';
|
||||
const result = userAgent.match(/Chrome\/(\d+)/i);
|
||||
chromeVersion = result ? parseInt(result[1]) : 0;
|
||||
}
|
||||
const chromeVersion = userAgentChromeVersion();
|
||||
const needChromeFix = !!chromeVersion && chromeVersion <= 87;
|
||||
|
||||
if (
|
||||
|
||||
+14
-16
@@ -9,6 +9,10 @@ import {
|
||||
timestampToString,
|
||||
toMsFromMpegTsClock,
|
||||
} from '../utils/timescale-conversion';
|
||||
import {
|
||||
userAgentChromeVersion,
|
||||
userAgentSafariVersion,
|
||||
} from '../utils/user-agent';
|
||||
import type { HlsConfig } from '../config';
|
||||
import type { HlsEventEmitter } from '../events';
|
||||
import type { ChunkMetadata } from '../hls';
|
||||
@@ -43,9 +47,6 @@ const MPEG_AUDIO_SAMPLE_PER_FRAME = 1152;
|
||||
const AC3_SAMPLES_PER_FRAME = 1536;
|
||||
const MPEG_TS_PTS_ROLLOVER = 8589934592; // 2^33
|
||||
|
||||
let chromeVersion: number | null = null;
|
||||
let safariWebkitVersion: number | null = null;
|
||||
|
||||
function createMp4Sample(
|
||||
isKeyframe: boolean,
|
||||
duration: number,
|
||||
@@ -96,16 +97,6 @@ export default class MP4Remuxer extends Logger implements Remuxer {
|
||||
this.config = config;
|
||||
this.typeSupported = typeSupported;
|
||||
this.ISGenerated = false;
|
||||
|
||||
if (chromeVersion === null) {
|
||||
const userAgent = navigator.userAgent || '';
|
||||
const result = userAgent.match(/Chrome\/(\d+)/i);
|
||||
chromeVersion = result ? parseInt(result[1]) : 0;
|
||||
}
|
||||
if (safariWebkitVersion === null) {
|
||||
const result = navigator.userAgent.match(/Safari\/(\d+)/i);
|
||||
safariWebkitVersion = result ? parseInt(result[1]) : 0;
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
@@ -563,7 +554,7 @@ export default class MP4Remuxer extends Logger implements Remuxer {
|
||||
inputSamples[0].pts -
|
||||
normalizePts(inputSamples[0].dts, inputSamples[0].pts);
|
||||
if (
|
||||
chromeVersion &&
|
||||
userAgentChromeVersion() &&
|
||||
nextVideoTs !== null &&
|
||||
Math.abs(pts - cts - (nextVideoTs + initTime)) < 15000
|
||||
) {
|
||||
@@ -635,7 +626,7 @@ export default class MP4Remuxer extends Logger implements Remuxer {
|
||||
if (
|
||||
!foundOverlap ||
|
||||
nextVideoPts >= inputSamples[0].pts ||
|
||||
chromeVersion
|
||||
userAgentChromeVersion()
|
||||
) {
|
||||
firstDTS = nextVideoPts;
|
||||
const firstPTS = inputSamples[0].pts - delta;
|
||||
@@ -832,6 +823,7 @@ export default class MP4Remuxer extends Logger implements Remuxer {
|
||||
}
|
||||
|
||||
if (outputSamples.length) {
|
||||
const chromeVersion = userAgentChromeVersion();
|
||||
if (chromeVersion) {
|
||||
if (chromeVersion < 70) {
|
||||
// Chrome workaround, mark first sample as being a Random Access Point (keyframe) to avoid sourcebuffer append issue
|
||||
@@ -840,7 +832,7 @@ export default class MP4Remuxer extends Logger implements Remuxer {
|
||||
flags.dependsOn = 2;
|
||||
flags.isNonSync = 0;
|
||||
}
|
||||
} else if (safariWebkitVersion) {
|
||||
} else if (userAgentSafariVersion()) {
|
||||
// Fix for "CNN special report, with CC" in test-streams (Safari browser only)
|
||||
// Ignore DTS when frame durations are irregular. Safari MSE does not handle this leading to gaps.
|
||||
if (
|
||||
@@ -913,6 +905,12 @@ export default class MP4Remuxer extends Logger implements Remuxer {
|
||||
nb: outputSamples.length,
|
||||
dropped: track.dropped,
|
||||
};
|
||||
// For troubleshooting duplicates of https://github.com/video-dev/hls.js/issues/6374
|
||||
// console.log(
|
||||
// `#6374 segment ${chunkMeta.sn} timeOffset: ${timeOffset} dts: ${firstDTS}-${endDTS} pts: ${minPTS}-${maxPTS} cts[0]=${
|
||||
// outputSamples[0].cts
|
||||
// } cts[${outputSamples.length - 1}]=${outputSamples[outputSamples.length - 1].cts} sample-duration: ${mp4SampleDuration} timescale: ${timeScale}`,
|
||||
// );
|
||||
track.samples = [];
|
||||
track.dropped = 0;
|
||||
return data;
|
||||
|
||||
@@ -390,11 +390,29 @@ class PassThroughRemuxer extends Logger implements Remuxer {
|
||||
initSegment.trackId = initPTS.trackId;
|
||||
}
|
||||
|
||||
const startTime = decodeTime - initPTS.baseTime / initPTS.timescale;
|
||||
const endTime = startTime + duration;
|
||||
const startDTS = decodeTime - initPTS.baseTime / initPTS.timescale;
|
||||
const endDTS = startDTS + duration;
|
||||
const startPTS =
|
||||
baseOffsetSamples?.ptsMin !== undefined
|
||||
? baseOffsetSamples.ptsMin / baseOffsetSamples.timescale -
|
||||
initPTS.baseTime / initPTS.timescale
|
||||
: startDTS;
|
||||
const endPTS = baseOffsetSamples?.ptsMax
|
||||
? baseOffsetSamples.ptsMax / baseOffsetSamples.timescale -
|
||||
initPTS.baseTime / initPTS.timescale
|
||||
: endDTS;
|
||||
|
||||
// For troubleshooting duplicates of https://github.com/video-dev/hls.js/issues/6777
|
||||
// if (videoSampleTimestamps) {
|
||||
// console.log(
|
||||
// `#6777 segment ${chunkMeta.sn}: dts: ${videoSampleTimestamps.start}-${videoSampleTimestamps.start + videoSampleTimestamps.duration} pts: ${videoSampleTimestamps.ptsMin}-${
|
||||
// videoSampleTimestamps.ptsMax
|
||||
// }`,
|
||||
// );
|
||||
// }
|
||||
|
||||
if (duration > 0) {
|
||||
this.lastEndTime = endTime;
|
||||
this.lastEndTime = endDTS;
|
||||
} else {
|
||||
this.warn('Duration parsed from mp4 should be greater than zero');
|
||||
this.resetNextTimestamp();
|
||||
@@ -407,10 +425,10 @@ class PassThroughRemuxer extends Logger implements Remuxer {
|
||||
const track: RemuxedTrack = {
|
||||
data1,
|
||||
data2,
|
||||
startPTS: startTime,
|
||||
startDTS: startTime,
|
||||
endPTS: endTime,
|
||||
endDTS: endTime,
|
||||
startPTS,
|
||||
startDTS,
|
||||
endPTS,
|
||||
endDTS,
|
||||
type,
|
||||
hasAudio,
|
||||
hasVideo,
|
||||
@@ -421,13 +439,14 @@ class PassThroughRemuxer extends Logger implements Remuxer {
|
||||
|
||||
result.audio = hasAudio && !hasVideo ? track : undefined;
|
||||
result.video = hasVideo ? track : undefined;
|
||||
const isVideoContiguous = this.isVideoContiguous;
|
||||
const videoSampleCount = videoSampleTimestamps?.sampleCount;
|
||||
if (videoSampleCount) {
|
||||
const firstKeyFrame = videoSampleTimestamps.keyFrameIndex;
|
||||
const independent = firstKeyFrame !== -1;
|
||||
track.nb = videoSampleCount;
|
||||
track.dropped =
|
||||
firstKeyFrame === 0 || this.isVideoContiguous
|
||||
firstKeyFrame === 0 || isVideoContiguous
|
||||
? 0
|
||||
: independent
|
||||
? firstKeyFrame
|
||||
@@ -439,7 +458,7 @@ class PassThroughRemuxer extends Logger implements Remuxer {
|
||||
(videoSampleTimestamps.keyFrameStart - initPTS.baseTime) /
|
||||
initPTS.timescale;
|
||||
}
|
||||
if (!this.isVideoContiguous) {
|
||||
if (!isVideoContiguous) {
|
||||
result.independent = independent;
|
||||
}
|
||||
this.isVideoContiguous ||= independent;
|
||||
|
||||
+1
-4
@@ -1,9 +1,6 @@
|
||||
import { getMediaSource } from './mediasource-helper';
|
||||
import { isHEVC } from './mp4-tools';
|
||||
|
||||
export const userAgentHevcSupportIsInaccurate = () => {
|
||||
return /\(Windows.+Firefox\//i.test(navigator.userAgent);
|
||||
};
|
||||
import { userAgentHevcSupportIsInaccurate } from './user-agent';
|
||||
|
||||
// from http://mp4ra.org/codecs.html
|
||||
// values indicate codec selection preference (lower is higher priority)
|
||||
|
||||
@@ -2,9 +2,9 @@ import {
|
||||
fillInMissingAV01Params,
|
||||
getCodecsForMimeType,
|
||||
mimeTypeForCodec,
|
||||
userAgentHevcSupportIsInaccurate,
|
||||
} from './codecs';
|
||||
import { isHEVC } from './mp4-tools';
|
||||
import { userAgentHevcSupportIsInaccurate } from './user-agent';
|
||||
import type { AudioTracksByGroup } from './rendition-helper';
|
||||
import type { Level, VideoRange } from '../types/level';
|
||||
import type { AudioSelectionOption } from '../types/media-playlist';
|
||||
|
||||
+31
-14
@@ -712,10 +712,11 @@ type TrackFragmentRun = {
|
||||
samples: TrackFragmentRunSample[];
|
||||
};
|
||||
export type TrackTimes = {
|
||||
cts?: number;
|
||||
duration: number;
|
||||
keyFrameIndex?: number;
|
||||
keyFrameStart?: number;
|
||||
ptsMin?: number;
|
||||
ptsMax?: number;
|
||||
start: number;
|
||||
sampleCount: number;
|
||||
trun: TrackFragmentRun[];
|
||||
@@ -852,10 +853,8 @@ export function getSampleData(
|
||||
};
|
||||
trackTimes.trun.push(fragRun);
|
||||
}
|
||||
let size;
|
||||
for (let ix = 0; ix < sampleCount; ix++) {
|
||||
const sample: TrackFragmentRunSample = {
|
||||
size: 0,
|
||||
};
|
||||
if (sampleDurationPresent) {
|
||||
sampleDuration = readUint32(trun, offset);
|
||||
offset += 4;
|
||||
@@ -863,17 +862,18 @@ export function getSampleData(
|
||||
sampleDuration = defaultSampleDuration;
|
||||
}
|
||||
if (sampleSizePresent) {
|
||||
sample.size = readUint32(trun, offset);
|
||||
size = readUint32(trun, offset);
|
||||
offset += 4;
|
||||
} else {
|
||||
sample.size = defaultSampleSize;
|
||||
size = defaultSampleSize;
|
||||
}
|
||||
sampleOffset += sample.size;
|
||||
sampleOffset += size;
|
||||
if (sampleOffset <= eof) {
|
||||
samples[ix] = sample;
|
||||
let flags;
|
||||
let cts = 0;
|
||||
if (sampleFlagsPresent) {
|
||||
const isNonSyncSample = trun[offset + 1] & 0x01;
|
||||
sample.flags = {
|
||||
flags = {
|
||||
isNonSync: isNonSyncSample ? 1 : 0,
|
||||
dependsOn: (trun[offset] & 0x03) === 1 ? 1 : 2,
|
||||
};
|
||||
@@ -887,13 +887,30 @@ export function getSampleData(
|
||||
}
|
||||
if (sampleCompositionTimeOffsetPresent) {
|
||||
const version = trun[0];
|
||||
if (version === 0) {
|
||||
sample.cts = readUint32(trun, offset);
|
||||
} else {
|
||||
sample.cts = readSint32(trun, offset);
|
||||
}
|
||||
cts =
|
||||
version === 0
|
||||
? readUint32(trun, offset)
|
||||
: readSint32(trun, offset);
|
||||
offset += 4;
|
||||
}
|
||||
const pts = sampleDTS + cts;
|
||||
if (
|
||||
!Number.isFinite(trackTimes.ptsMin) ||
|
||||
pts < trackTimes.ptsMin!
|
||||
) {
|
||||
trackTimes.ptsMin = pts;
|
||||
}
|
||||
if (
|
||||
!Number.isFinite(trackTimes.ptsMax) ||
|
||||
pts + sampleDuration > trackTimes.ptsMax!
|
||||
) {
|
||||
trackTimes.ptsMax = pts + sampleDuration;
|
||||
}
|
||||
samples[ix] = {
|
||||
cts,
|
||||
flags,
|
||||
size,
|
||||
};
|
||||
}
|
||||
sampleDTS += sampleDuration;
|
||||
rawDuration += sampleDuration;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { logger } from './logger';
|
||||
import { userAgentIsIOSLike } from './user-agent';
|
||||
import type { MediaPlaylist } from '../hls';
|
||||
|
||||
// This is a replacement of the native addTextTrack method.
|
||||
@@ -28,11 +29,7 @@ export function createTrackNode(
|
||||
}
|
||||
|
||||
export function getTrackKind(track: MediaPlaylist): TextTrackKind | 'forced' {
|
||||
if (
|
||||
track.forced &&
|
||||
(navigator.vendor.includes('Apple') ||
|
||||
/iPhone|iPad|iPod/.test(navigator.userAgent))
|
||||
) {
|
||||
if (track.forced && userAgentIsIOSLike()) {
|
||||
return 'forced';
|
||||
} else if (
|
||||
track.characteristics &&
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
const ua = getNavigator('userAgent');
|
||||
const vendor = getNavigator('vendor');
|
||||
|
||||
let chromeVersion: number | undefined;
|
||||
let firefoxVersion: number | undefined;
|
||||
let safariVersion: number | undefined;
|
||||
|
||||
function getNavigator(field: string) {
|
||||
try {
|
||||
return navigator[field] || '';
|
||||
} catch (e) {
|
||||
/* no-op */
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export function userAgentSafariVersion(): number {
|
||||
if (safariVersion === undefined) {
|
||||
const result = ua.match(/Safari\/(\d+)/i);
|
||||
safariVersion = result ? parseInt(result[1]) : 0;
|
||||
}
|
||||
return safariVersion;
|
||||
}
|
||||
|
||||
export function userAgentChromeVersion(): number {
|
||||
if (chromeVersion === undefined) {
|
||||
const result = ua.match(/Chrome\/(\d+)/i);
|
||||
chromeVersion = result ? parseInt(result[1]) : 0;
|
||||
}
|
||||
return chromeVersion;
|
||||
}
|
||||
|
||||
export function userAgentFirefoxVersion(): number {
|
||||
if (firefoxVersion === undefined) {
|
||||
const result = ua.match(/Firefox\/(\d+)/i);
|
||||
firefoxVersion = result ? parseInt(result[1]) : 0;
|
||||
}
|
||||
return firefoxVersion;
|
||||
}
|
||||
|
||||
export function userAgentHevcSupportIsInaccurate() {
|
||||
return /\(Windows.+Firefox\//i.test(ua);
|
||||
}
|
||||
|
||||
export function userAgentIsIOSLike() {
|
||||
return vendor.indexOf('Apple') > -1 || /iPhone|iPad|iPod/.test(ua);
|
||||
}
|
||||
|
||||
export function userAgentIsAndroidLike() {
|
||||
return /Android/.test(ua);
|
||||
}
|
||||
Reference in New Issue
Block a user