Skip to content

Commit

Permalink
Merge branch 'upstream/master' into feature/v1.0.0
Browse files Browse the repository at this point in the history
* upstream/master:
  Disable flakey smooth switch test on stream with large start gap
  Require min cue duration of 0.25
  Package lock update.
  Minimal Logging on Functional Tests.
  Handle DTS wrapping in initial AVC CTS calculation
  Apply PTSNormalize to ID3 and Text samples to handle timestamp rollover
  Add method to find start pts #2930
  Fix PTS calculation on rollover.
  Bump netlify-cli from 2.58.0 to 2.59.0
  Use ES5 in JavaScript executed by webdriver (for IE11)
  Fix chart fragment rendering issue
  Do not flush audio on audio track switch when both tracks are main variant streams #2837
  Bump @types/mocha from 8.0.0 to 8.0.1
  Bump @babel/plugin-proposal-optional-chaining from 7.10.4 to 7.11.0
  Bump @babel/core from 7.10.5 to 7.11.0
  Improve demo video size menu
  Clear source buffers from chart when new ones are created
  Show multiple demo test tabs using modifier key
  Configure streams for audio and video buffer descrepency in buffer length test
  • Loading branch information
Rob Walch committed Aug 4, 2020
2 parents 2722e73 + 0476198 commit 929bba1
Show file tree
Hide file tree
Showing 10 changed files with 129 additions and 111 deletions.
2 changes: 1 addition & 1 deletion demo/chart/timeline-chart.ts
Expand Up @@ -274,7 +274,7 @@ export class TimelineChart {
return;
}
// eslint-disable-next-line no-restricted-properties
const fragData = arrayFind(levelDataSet.data, fragData => fragData.relurl === frag.relurl);
const fragData = arrayFind(levelDataSet.data, fragData => fragData.relurl === frag.relurl && fragData.sn === frag.sn);
if (fragData && fragData !== frag) {
Object.assign(fragData, frag);
}
Expand Down
1 change: 1 addition & 0 deletions demo/index.html
Expand Up @@ -169,6 +169,7 @@ <h3>
</p>
<p>
<span>
<button type="button" class="btn btn-xs btn-default" onclick="$('#streamSelect')[0].selectedIndex++;$('#streamSelect').change()">Next video</button>
<button type="button" class="btn btn-xs btn-default btn-dump" title="Save dumped audio mp4 appends" onclick="createfMP4('audio');">Create audio-fmp4</button>
<button type="button" class="btn btn-xs btn-default btn-dump" title="Save dumped video mp4 appends" onclick="createfMP4('video')">Create video-fmp4</button>
</span>
Expand Down
11 changes: 9 additions & 2 deletions demo/main.js
Expand Up @@ -58,14 +58,18 @@ $(document).ready(function () {

chart = setupTimelineChart();

Object.keys(testStreams).forEach((key) => {
Object.keys(testStreams).forEach((key, index) => {
const stream = testStreams[key];
const option = new Option(stream.description, key);
$('#streamSelect').append(option);
if (stream.url === sourceURL) {
$('#streamSelect')[0].selectedIndex = index + 1;
}
});

$('#streamSelect').change(function () {
selectedTestStream = testStreams[$('#streamSelect').val()];
const key = $('#streamSelect').val() || Object.keys(testStreams)[0];
selectedTestStream = testStreams[key];
const streamUrl = selectedTestStream.url;
$('#streamURL').val(streamUrl);
loadSelectedStream();
Expand Down Expand Up @@ -159,6 +163,9 @@ function setupGlobals () {
level: [],
bitrate: []
};
lastAudioTrackSwitchingIdx = undefined;
lastSeekingIdx = undefined;
bufferingIdx = -1;

// actual values, only on window
self.recoverDecodingErrorDate = null;
Expand Down
4 changes: 2 additions & 2 deletions src/controller/audio-stream-controller.ts
Expand Up @@ -99,7 +99,7 @@ class AudioStreamController extends BaseStreamController implements ComponentAPI
const cc = frag.cc;
this.initPTS[cc] = initPTS;
this.videoTrackCC = cc;
this.log(`InitPTS for cc: ${cc} found from video track: ${initPTS}`);
this.log(`InitPTS for cc: ${cc} found from main: ${initPTS}`);
// If we are waiting, tick immediately to unblock audio fragment transmuxing
if (this.state === State.WAITING_INIT_PTS) {
this.tick();
Expand Down Expand Up @@ -310,7 +310,7 @@ class AudioStreamController extends BaseStreamController implements ComponentAPI
return;
}

this.log(`Loading ${frag.sn}, cc: ${frag.cc} of [${trackDetails.startSN} ,${trackDetails.endSN}],track ${this.trackId}, load position: ${pos.toFixed(3)}-${loadPos.toFixed(3)}`);
this.log(`Loading ${frag.sn}, cc: ${frag.cc} of [${trackDetails.startSN}-${trackDetails.endSN}], track ${trackId}, load position: ${pos.toFixed(3)}-${loadPos.toFixed(3)}`);
this.loadFragment(frag);
}
}
Expand Down
11 changes: 6 additions & 5 deletions src/controller/id3-track-controller.ts
Expand Up @@ -11,6 +11,8 @@ declare global {
}
}

const MIN_CUE_DURATION = 0.25;

class ID3TrackController implements ComponentAPI {
private hls: Hls;
private id3Track: TextTrack | null = null;
Expand Down Expand Up @@ -99,11 +101,10 @@ class ID3TrackController implements ComponentAPI {
if (!endTime) {
endTime = fragment.start + fragment.duration;
}
if (startTime === endTime) {
// Give a slight bump to the endTime if it's equal to startTime to avoid a SyntaxError in IE
endTime += 0.0001;
} else if (startTime > endTime) {
endTime = startTime + 0.25;

const timeDiff = endTime - startTime;
if (timeDiff < MIN_CUE_DURATION) {
endTime += MIN_CUE_DURATION - timeDiff;
}

for (let j = 0; j < frames.length; j++) {
Expand Down
22 changes: 16 additions & 6 deletions src/controller/stream-controller.ts
Expand Up @@ -264,11 +264,13 @@ export default class StreamController extends BaseStreamController implements Ne
// We want to load the key if we're dealing with an identity key, because we will decrypt
// this content using the key we fetch. Other keys will be handled by the DRM CDM via EME.
if (frag.decryptdata?.keyFormat === 'identity' && !frag.decryptdata?.key) {
this.log(`Loading key for ${frag.sn} of [${levelDetails.startSN} ,${levelDetails.endSN}],level ${level}`);
this.log(`Loading key for ${frag.sn} of [${levelDetails.startSN}-${levelDetails.endSN}], level ${level}`);
this._loadKey(frag);
} else {
if (this.fragCurrent !== frag) {
this.log(`Loading fragment ${frag.sn} of [${levelDetails.startSN} ,${levelDetails.endSN}],level ${level}, currentTime:${pos.toFixed(3)},bufferEnd:${bufferInfo.end.toFixed(3)}`);
this.log(`Loading fragment ${frag.sn} of [${levelDetails.startSN}-${levelDetails.endSN}], level ${level}, ${
this.loadedmetadata ? 'currentTime' : 'nextLoadPosition'
}: ${parseFloat(pos.toFixed(3))}, bufferInfo.end: ${parseFloat(bufferInfo.end.toFixed(3))}`);
}
this._loadFragment(frag);
}
Expand Down Expand Up @@ -638,6 +640,7 @@ export default class StreamController extends BaseStreamController implements Ne

onAudioTrackSwitching (event: Events.AUDIO_TRACK_SWITCHING, data: AudioTrackSwitchingData) {
// if any URL found on new audio track, it is an alternate audio track
const fromAltAudio = this.altAudio;
const altAudio = !!data.url;
const trackId = data.id;
// if we switch on main audio, ensure that main fragment scheduling is synced with media.buffered
Expand All @@ -664,10 +667,17 @@ export default class StreamController extends BaseStreamController implements Ne
this.resetTransmuxer();
}
const hls = this.hls;
// switching to main audio, flush all audio and trigger track switched
hls.trigger(Events.BUFFER_FLUSHING, { startOffset: 0, endOffset: Number.POSITIVE_INFINITY, type: 'audio' });
hls.trigger(Events.AUDIO_TRACK_SWITCHED, { id: trackId });
this.altAudio = false;
// If switching from alt to main audio, flush all audio and trigger track switched
if (fromAltAudio) {
hls.trigger(Events.BUFFER_FLUSHING, {
startOffset: 0,
endOffset: Number.POSITIVE_INFINITY,
type: 'audio'
});
}
hls.trigger(Events.AUDIO_TRACK_SWITCHED, {
id: trackId
});
}
}

Expand Down
12 changes: 2 additions & 10 deletions src/demux/tsdemuxer.ts
Expand Up @@ -1154,22 +1154,14 @@ function parsePES (stream) {
(frag[11] & 0xFE) * 16384 +// 1 << 14
(frag[12] & 0xFF) * 128 +// 1 << 7
(frag[13] & 0xFE) / 2;
// check if greater than 2^32 -1
if (pesPts > 4294967295) {
// decrement 2^33
pesPts -= 8589934592;
}

if (pesFlags & 0x40) {
pesDts = (frag[14] & 0x0E) * 536870912 +// 1 << 29
(frag[15] & 0xFF) * 4194304 +// 1 << 22
(frag[16] & 0xFE) * 16384 +// 1 << 14
(frag[17] & 0xFF) * 128 +// 1 << 7
(frag[18] & 0xFE) / 2;
// check if greater than 2^32 -1
if (pesDts > 4294967295) {
// decrement 2^33
pesDts -= 8589934592;
}

if (pesPts - pesDts > 60 * 90000) {
logger.warn(`${Math.round((pesPts - pesDts) / 90000)}s delta between PTS and DTS, align them`);
pesPts = pesDts;
Expand Down
42 changes: 30 additions & 12 deletions src/remux/mp4-remuxer.ts
Expand Up @@ -72,6 +72,25 @@ export default class MP4Remuxer implements Remuxer {
this.ISGenerated = false;
}

getVideoStartPts (videoSamples) {
let rolloverDetected = false;
const startPTS = videoSamples.reduce((minPTS, sample) => {
const delta = sample.pts - minPTS;
if (delta < -4294967296) { // 2^32, see PTSNormalize for reasoning, but we're hitting a rollover here, and we don't want that to impact the timeOffset calculation
rolloverDetected = true;
return minPTS;
} else if (delta > 0) {
return minPTS;
} else {
return sample.pts;
}
}, videoSamples[0].pts);
if (rolloverDetected) {
logger.debug('PTS rollover detected');
}
return startPTS;
}

remux (audioTrack: DemuxedAudioTrack, videoTrack: DemuxedAvcTrack, id3Track: DemuxedTrack, textTrack: DemuxedTrack, timeOffset: number, accurateTimeOffset: boolean) : RemuxerResult {
let video;
let audio;
Expand Down Expand Up @@ -112,7 +131,7 @@ export default class MP4Remuxer implements Remuxer {
// if first audio DTS is not aligned with first video DTS then we need to take that into account
// when providing timeOffset to remuxAudio / remuxVideo. if we don't do that, there might be a permanent / small
// drift between audio and video streams
const startPTS = videoTrack.samples.reduce((minPTS, sample) => Math.min(minPTS, sample.pts), videoTrack.samples[0].pts);
const startPTS = this.getVideoStartPts(videoTrack.samples);
const tsDelta = audioTrack.samples[0].pts - startPTS;
const audiovideoTimestampDelta = tsDelta / videoTrack.inputTimeScale;
audioTimeOffset += Math.max(0, audiovideoTimestampDelta);
Expand Down Expand Up @@ -148,11 +167,11 @@ export default class MP4Remuxer implements Remuxer {

if (this.ISGenerated) {
if (id3Track.samples.length) {
id3 = this.remuxID3(id3Track);
id3 = this.remuxID3(id3Track, timeOffset);
}

if (textTrack.samples.length) {
text = this.remuxText(textTrack);
text = this.remuxText(textTrack, timeOffset);
}
}

Expand Down Expand Up @@ -225,7 +244,7 @@ export default class MP4Remuxer implements Remuxer {
}
};
if (computePTSDTS) {
const startPTS = videoSamples.reduce((minPTS, sample) => Math.min(minPTS, sample.pts), videoSamples[0].pts);
const startPTS = this.getVideoStartPts(videoSamples);
const startOffset = Math.round(inputTimeScale * timeOffset);
initDTS = Math.min(initDTS, videoSamples[0].dts - startOffset);
initPTS = Math.min(initPTS, startPTS - startOffset);
Expand Down Expand Up @@ -265,8 +284,7 @@ export default class MP4Remuxer implements Remuxer {
// if parsed fragment is contiguous with last one, let's use last DTS value as reference
if (!contiguous || nextAvcDts === null) {
const pts = timeOffset * timeScale;
// TODO: Handle case where pts value is wrapped, but dts is not
const cts = Math.max(0, inputSamples[0].pts - inputSamples[0].dts);
const cts = inputSamples[0].pts - PTSNormalize(inputSamples[0].dts, inputSamples[0].pts);
// if not contiguous, let's use target timeOffset
nextAvcDts = pts - cts;
}
Expand Down Expand Up @@ -754,7 +772,7 @@ export default class MP4Remuxer implements Remuxer {
return this.remuxAudio(track, timeOffset, contiguous, false);
}

remuxID3 (track: DemuxedTrack) : RemuxedMetadata | undefined {
remuxID3 (track: DemuxedTrack, timeOffset: number) : RemuxedMetadata | undefined {
const length = track.samples.length;
if (!length) {
return;
Expand All @@ -766,8 +784,8 @@ export default class MP4Remuxer implements Remuxer {
const sample = track.samples[index];
// setting id3 pts, dts to relative time
// using this._initPTS and this._initDTS to calculate relative time
sample.pts = ((sample.pts - initPTS) / inputTimeScale);
sample.dts = ((sample.dts - initDTS) / inputTimeScale);
sample.pts = PTSNormalize(sample.pts - initPTS, timeOffset * inputTimeScale) / inputTimeScale;
sample.dts = PTSNormalize(sample.dts - initDTS, timeOffset * inputTimeScale) / inputTimeScale;
}
const samples = track.samples;
track.samples = [];
Expand All @@ -776,21 +794,21 @@ export default class MP4Remuxer implements Remuxer {
};
}

remuxText (track: DemuxedTrack) : RemuxedUserdata | undefined {
remuxText (track: DemuxedTrack, timeOffset: number) : RemuxedUserdata | undefined {
const length = track.samples.length;
if (!length) {
return;
}
track.samples.sort((a, b) => a.pts - b.pts);

const inputTimeScale = track.inputTimeScale;
const initPTS = this._initPTS;
for (let index = 0; index < length; index++) {
const sample = track.samples[index];
// setting text pts, dts to relative time
// using this._initPTS and this._initDTS to calculate relative time
sample.pts = ((sample.pts - initPTS) / inputTimeScale);
sample.pts = PTSNormalize(sample.pts - initPTS, timeOffset * inputTimeScale) / inputTimeScale;
}
track.samples.sort((a, b) => a.pts - b.pts);
const samples = track.samples;
track.samples = [];
return {
Expand Down
40 changes: 23 additions & 17 deletions tests/functional/auto/setup.js
Expand Up @@ -22,6 +22,8 @@ const browserConfig = {
*/
let browser;
let stream;
const printDebugLogs = false;

// Setup browser config data from env vars
if (onTravis) {
const UA = process.env.UA;
Expand Down Expand Up @@ -118,20 +120,20 @@ async function testSmoothSwitch (url, config) {
const callback = arguments[arguments.length - 1];
self.startStream(url, config, callback);
const video = self.video;
self.hls.once(self.Hls.Events.FRAG_CHANGED, (event, data) => {
self.hls.once(self.Hls.Events.FRAG_CHANGED, function (eventName, data) {
console.log('[test] > ' + eventName + ' frag.level: ' + data.frag.level);
self.switchToHighestLevel('next');
});
self.hls.on(self.Hls.Events.LEVEL_SWITCHED, (event, data) => {
console.log(`[test] > level switched: ${data.level}`);
self.hls.on(self.Hls.Events.LEVEL_SWITCHED, function (eventName, data) {
console.log('[test] > ' + eventName + ' data.level: ' + data.level);
const currentTime = video.currentTime;
if (data.level === self.hls.levels.length - 1) {
console.log(`[test] > switched on level: ${data.level}`);
const highestLevel = (self.hls.levels.length - 1);
if (data.level === highestLevel) {
self.setTimeout(function () {
const newCurrentTime = video.currentTime;
console.log(
`[test] > currentTime delta : ${newCurrentTime - currentTime}`
);
console.log('[test] > currentTime delta: ' + (newCurrentTime - currentTime));
callback({
highestLevel: highestLevel,
currentTimeDelta: newCurrentTime - currentTime,
logs: self.logString
});
Expand Down Expand Up @@ -318,15 +320,17 @@ describe(`testing hls.js playback in the browser on "${browserDescription}"`, fu
beforeEach(async function () {
try {
await retry(async () => {
console.log('Loading test page...');
if (printDebugLogs) {
console.log('Loading test page...');
}
try {
await browser.get(`http://${hostname}:8000/tests/functional/auto/index.html`);
} catch (e) {
throw new Error('failed to open test page');
}
console.log('Test page loaded.');

console.log('Locating ID \'hlsjs-functional-tests\'');
if (printDebugLogs) {
console.log('Test page loaded.');
}
try {
await browser.wait(
until.elementLocated(By.css('body#hlsjs-functional-tests')),
Expand All @@ -338,18 +342,20 @@ describe(`testing hls.js playback in the browser on "${browserDescription}"`, fu
console.log(source);
throw e;
}
console.log('Located the ID, page confirmed loaded');
if (printDebugLogs) {
console.log('Test harness found, page confirmed loaded');
}
});
} catch (e) {
throw new Error(`error getting test page loaded: ${e}`);
}
});

afterEach(async function () {
// if (onTravis || (!onTravis && this.currentTest.isFailed())) {
const logString = await browser.executeScript('return logString');
console.log(`${onTravis ? 'travis_fold:start:debug_logs' : ''}\n${logString}\n${onTravis ? 'travis_fold:end:debug_logs' : ''}`);
// }
if (printDebugLogs || this.currentTest.isFailed()) {
const logString = await browser.executeScript('return logString');
console.log(`${onTravis ? 'travis_fold:start:debug_logs' : ''}\n${logString}\n${onTravis ? 'travis_fold:end:debug_logs' : ''}`);
}
});

after(async function () {
Expand Down

0 comments on commit 929bba1

Please sign in to comment.