Skip to content

Commit

Permalink
Add a workaround for SegmentTimeline manifests for which there is no …
Browse files Browse the repository at this point in the history
…valid segment request to be found when seeking to the very end (#4658)
  • Loading branch information
dsilhavy authored Jan 8, 2025
1 parent 440ebd5 commit 0d62e13
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 20 deletions.
50 changes: 30 additions & 20 deletions src/streaming/StreamProcessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -473,30 +473,40 @@ function StreamProcessor(config) {
// If this statement is true we might be stuck. A static manifest does not change and we did not find a valid request for the target time
// There is no point in trying again. We need to adjust the time in order to find a valid request. This can happen if the user/app seeked into a gap.
// For dynamic manifests this can also happen especially if we jump over the gap in the previous period and are using SegmentTimeline and in case there is a positive eptDelta at the beginning of the period we are stuck.
if (settings.get().streaming.gaps.enableSeekFix && (shouldUseExplicitTimeForRequest || playbackController.getTime() === 0)) {
let adjustedTime;
if (!isDynamic) {
adjustedTime = dashHandler.getValidTimeAheadOfTargetTime(bufferingTime, currentMediaInfo, representation, settings.get().streaming.gaps.threshold);
} else if (isDynamic && representation.segmentInfoType === DashConstants.SEGMENT_TIMELINE) {
// If we find a valid request ahead of the current time then we are in a gap. Segments are only added at the end of the timeline
adjustedTime = dashHandler.getValidTimeAheadOfTargetTime(bufferingTime, currentMediaInfo, representation, settings.get().streaming.gaps.threshold);
}
if (!isNaN(adjustedTime) && adjustedTime !== bufferingTime) {
if (playbackController.isSeeking() || playbackController.getTime() === 0) {
// If we are seeking then playback is stalled. Do a seek to get out of this situation
logger.warn(`Adjusting playback time ${adjustedTime} because of gap in the manifest. Seeking by ${adjustedTime - bufferingTime}`);
playbackController.seek(adjustedTime, false, false);
} else {
// If we are not seeking we should still be playing but we cant find anything to buffer. So we adjust the buffering time and leave the gap jump to the GapController
logger.warn(`Adjusting buffering time ${adjustedTime} because of gap in the manifest. Adjusting time by ${adjustedTime - bufferingTime}`);
setExplicitBufferingTime(adjustedTime)
try {
if (settings.get().streaming.gaps.enableSeekFix && (shouldUseExplicitTimeForRequest || playbackController.getTime() === 0)) {
let adjustedTime;
if (!isDynamic) {
adjustedTime = dashHandler.getValidTimeAheadOfTargetTime(bufferingTime, currentMediaInfo, representation, settings.get().streaming.gaps.threshold);
if (isNaN(adjustedTime)) {
// If there is no valid target time ahead and the buffering time is within the duration of one segment we slightly adjust it
if (bufferingTime >= representation.adaptation.period.mpd.mediaPresentationDuration - representation.segmentDuration) {
adjustedTime = bufferingTime - 0.1;
}
}
} else if (isDynamic && representation.segmentInfoType === DashConstants.SEGMENT_TIMELINE) {
// If we find a valid request ahead of the current time then we are in a gap. Segments are only added at the end of the timeline
adjustedTime = dashHandler.getValidTimeAheadOfTargetTime(bufferingTime, currentMediaInfo, representation, settings.get().streaming.gaps.threshold);
}
if (!isNaN(adjustedTime) && adjustedTime !== bufferingTime) {
if (playbackController.isSeeking() || playbackController.getTime() === 0) {
// If we are seeking then playback is stalled. Do a seek to get out of this situation
logger.warn(`Adjusting playback time ${adjustedTime} because of gap in the manifest. Seeking by ${adjustedTime - bufferingTime}`);
playbackController.seek(adjustedTime, false, false);
} else {
// If we are not seeking we should still be playing but we cant find anything to buffer. So we adjust the buffering time and leave the gap jump to the GapController
logger.warn(`Adjusting buffering time ${adjustedTime} because of gap in the manifest. Adjusting time by ${adjustedTime - bufferingTime}`);
setExplicitBufferingTime(adjustedTime)

if (rescheduleIfNoRequest) {
_noValidRequest();
if (rescheduleIfNoRequest) {
_noValidRequest();
}
}
return;
}
return;
}
} catch (e) {
logger.error(e);
}

// Check if the media is finished. If so, no need to schedule another request
Expand Down
6 changes: 6 additions & 0 deletions test/functional/src/Constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ const TEST_INPUTS = {
ENDED: {
SEEK_END_OFFSET: 8,
},
SEEK_ENDED: {
SEEK_END_OFFSET: 0,
SEEK_BACK_OFFSET: 50,
EVENT_WAITING_TIME: 5000
},
NO_RELOAD_AFTER_SEEK: {
TIME_TO_REACH_FOR_REDUNDANT_SEGMENT_FETCH: 10,
TIME_TO_SEEK_BACK_FOR_REDUNDANT_SEGMENT_FETCH: 2,
Expand Down Expand Up @@ -167,6 +172,7 @@ TESTCASES.PLAYBACK.ENDED = TESTCASES.CATEGORIES.PLAYBACK + 'ended';
TESTCASES.PLAYBACK.PAUSE = TESTCASES.CATEGORIES.PLAYBACK + 'pause';
TESTCASES.PLAYBACK.PLAY = TESTCASES.CATEGORIES.PLAYBACK + 'play';
TESTCASES.PLAYBACK.SEEK = TESTCASES.CATEGORIES.PLAYBACK + 'seek';
TESTCASES.PLAYBACK.SEEK_ENDED = TESTCASES.CATEGORIES.PLAYBACK + 'seek-ended';
TESTCASES.PLAYBACK.SEEK_TO_PRESENTATION_TIME = TESTCASES.CATEGORIES.PLAYBACK + 'seek-to-presentation-time';

TESTCASES.PLAYBACK_ADVANCED.ATTACH_AT_NON_ZERO = TESTCASES.CATEGORIES.PLAYBACK_ADVANCED + 'attach-at-non-zero';
Expand Down
14 changes: 14 additions & 0 deletions test/functional/test/common/common.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Constants from '../../src/Constants.js';
import {expect} from 'chai';
import DashJsAdapter from '../../adapter/DashJsAdapter.js';
import MediaPlayerEvents from '../../../../src/streaming/MediaPlayerEvents.js';

export async function checkIsPlaying(playerAdapter, expectedState) {
const isPlaying = await playerAdapter.isInPlayingState(Constants.TEST_TIMEOUT_THRESHOLDS.IS_PLAYING);
Expand Down Expand Up @@ -29,6 +30,19 @@ export async function checkForEndedEvent(playerAdapter) {
expect(ended).to.be.true;
}

export async function seekAndEndedEvent(playerAdapter, seekOffset) {
let endedEventThrown = false;
const _endedCallback = () => {
endedEventThrown = true;
}
playerAdapter.registerEvent(MediaPlayerEvents.PLAYBACK_ENDED, _endedCallback);
const targetTime = playerAdapter.getDuration() + seekOffset;
playerAdapter.seek(targetTime);
await playerAdapter.sleep(Constants.TEST_INPUTS.SEEK_ENDED.EVENT_WAITING_TIME);
playerAdapter.unregisterEvent(MediaPlayerEvents.PLAYBACK_ENDED, _endedCallback);
expect(endedEventThrown).to.be.true;
}

export async function reachedTargetForwardBuffer(playerAdapter, targetBuffer, tolerance) {
const reachedBuffer = await playerAdapter.reachedTargetForwardBuffer(Constants.TEST_TIMEOUT_THRESHOLDS.TARGET_BUFFER_REACHED, targetBuffer, tolerance);
expect(reachedBuffer).to.be.true;
Expand Down
56 changes: 56 additions & 0 deletions test/functional/test/playback/seek-ended.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import Constants from '../../src/Constants.js';
import Utils from '../../src/Utils.js';
import {
checkIsPlaying,
checkIsProgressing,
checkNoCriticalErrors,
initializeDashJsAdapter, seekAndEndedEvent
} from '../common/common.js';

const TESTCASE = Constants.TESTCASES.PLAYBACK.SEEK_ENDED;

Utils.getTestvectorsForTestcase(TESTCASE).forEach((item) => {
const mpd = item.url;

describe(`${TESTCASE} - ${item.name} - ${mpd}`, () => {

let playerAdapter;

before(function () {
if (item.type === Constants.CONTENT_TYPES.LIVE) {
this.skip();
}
playerAdapter = initializeDashJsAdapter(item, mpd);
})

after(() => {
if (playerAdapter) {
playerAdapter.destroy();
}
})

it(`Checking playing state`, async () => {
await checkIsPlaying(playerAdapter, true);
})

it(`Checking progressing state`, async () => {
await checkIsProgressing(playerAdapter);
});

it(`Seek to time larger than duration and expect ended event to be thrown`, async () => {
await seekAndEndedEvent(playerAdapter, Constants.TEST_INPUTS.SEEK_ENDED.SEEK_END_OFFSET);
})

it(`Seek back and then seek to duration() and expect ended event to be thrown`, async () => {
const seekTime = Math.max(playerAdapter.getDuration() - Constants.TEST_INPUTS.SEEK_ENDED.SEEK_BACK_OFFSET, 0);
playerAdapter.seek(seekTime);
playerAdapter.play();
await checkIsProgressing(playerAdapter);
await seekAndEndedEvent(playerAdapter, 0);
})

it(`Expect no critical errors to be thrown`, () => {
checkNoCriticalErrors(playerAdapter);
})
})
})

0 comments on commit 0d62e13

Please sign in to comment.