Skip to content

Commit

Permalink
Simplify EOS check by using frag states, move logic into shared super…
Browse files Browse the repository at this point in the history
…class JW8-2500
  • Loading branch information
John Bartos committed Dec 11, 2018
1 parent 7fd542f commit 3d68f31
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 151 deletions.
60 changes: 7 additions & 53 deletions src/controller/audio-stream-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,15 @@ import TimeRanges from '../utils/time-ranges';
import { ErrorTypes, ErrorDetails } from '../errors';
import { logger } from '../utils/logger';
import { findFragWithCC } from '../utils/discontinuities';
import TaskLoop from '../task-loop';
import { FragmentState } from './fragment-tracker';
import Fragment from '../loader/fragment';
import { findFragmentByPTS } from './fragment-finders';

import BaseStreamController, { State } from './base-stream-controller';
const { performance } = window;

const State = {
STOPPED: 'STOPPED',
STARTING: 'STARTING',
IDLE: 'IDLE',
PAUSED: 'PAUSED',
KEY_LOADING: 'KEY_LOADING',
FRAG_LOADING: 'FRAG_LOADING',
FRAG_LOADING_WAITING_RETRY: 'FRAG_LOADING_WAITING_RETRY',
WAITING_TRACK: 'WAITING_TRACK',
PARSING: 'PARSING',
PARSED: 'PARSED',
BUFFER_FLUSHING: 'BUFFER_FLUSHING',
ENDED: 'ENDED',
ERROR: 'ERROR',
WAITING_INIT_PTS: 'WAITING_INIT_PTS'
};

const TICK_INTERVAL = 100; // how often to tick in ms

class AudioStreamController extends TaskLoop {
class AudioStreamController extends BaseStreamController {
constructor (hls, fragmentTracker) {
super(hls,
Event.MEDIA_ATTACHED,
Expand Down Expand Up @@ -209,20 +191,10 @@ class AudioStreamController extends TaskLoop {
break;
}

// check if we need to finalize media stream
// we just got done loading the final fragment and there is no other buffered range after ...
// rationale is that in case there are any buffered ranges after, it means that there are unbuffered portion in between
// so we should not switch to ENDED in that case, to be able to buffer them
if (!audioSwitch && !trackDetails.live && fragPrevious && fragPrevious.sn === trackDetails.endSN && !bufferInfo.nextStart) {
// if we are not seeking or if we are seeking but everything (almost) til the end is buffered, let's signal eos
// we don't compare exactly media.duration === bufferInfo.end as there could be some subtle media duration difference when switching
// between different renditions. using half frag duration should help cope with these cases.
if (!this.media.seeking || (this.media.duration - bufferEnd) < fragPrevious.duration / 2) {
// Finalize the media stream
this.hls.trigger(Event.BUFFER_EOS, { type: 'audio' });
this.state = State.ENDED;
break;
}
if (!audioSwitch && this._streamEnded(bufferInfo, trackDetails)) {
this.hls.trigger(Event.BUFFER_EOS, { type: 'audio' });
this.state = State.ENDED;
return;
}

// find fragment index, contiguous with end of buffer position
Expand Down Expand Up @@ -305,8 +277,8 @@ class AudioStreamController extends TaskLoop {
logger.log(`Loading ${frag.sn}, cc: ${frag.cc} of [${trackDetails.startSN} ,${trackDetails.endSN}],track ${trackId}, currentTime:${pos},bufferEnd:${bufferEnd.toFixed(3)}`);
// only load if fragment is not loaded or if in audio switch
// we force a frag loading in audio switch as fragment tracker might not have evicted previous frags in case of quick audio switch
this.fragCurrent = frag;
if (audioSwitch || this.fragmentTracker.getState(frag) === FragmentState.NOT_LOADED) {
this.fragCurrent = frag;
this.startFragRequested = true;
if (Number.isFinite(frag.sn)) {
this.nextLoadPosition = frag.start + frag.duration;
Expand Down Expand Up @@ -406,24 +378,6 @@ class AudioStreamController extends TaskLoop {
this.stopLoad();
}

onMediaSeeking () {
if (this.state === State.ENDED) {
// switch to IDLE state to check for potential new fragment
this.state = State.IDLE;
}
if (this.media) {
this.lastCurrentTime = this.media.currentTime;
}

// tick to speed up processing
this.tick();
}

onMediaEnded () {
// reset startPosition and lastCurrentTime to restart playback @ stream beginning
this.startPosition = this.lastCurrentTime = 0;
}

onAudioTracksUpdated (data) {
logger.log('audio tracks updated');
this.tracks = data.audioTracks;
Expand Down
96 changes: 96 additions & 0 deletions src/controller/base-stream-controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import TaskLoop from '../task-loop';
import { FragmentState } from './fragment-tracker';
import { BufferHelper } from '../utils/buffer-helper';
import { logger } from '../utils/logger';

export const State = {
STOPPED: 'STOPPED',
STARTING: 'STARTING',
IDLE: 'IDLE',
PAUSED: 'PAUSED',
KEY_LOADING: 'KEY_LOADING',
FRAG_LOADING: 'FRAG_LOADING',
FRAG_LOADING_WAITING_RETRY: 'FRAG_LOADING_WAITING_RETRY',
WAITING_TRACK: 'WAITING_TRACK',
PARSING: 'PARSING',
PARSED: 'PARSED',
BUFFER_FLUSHING: 'BUFFER_FLUSHING',
ENDED: 'ENDED',
ERROR: 'ERROR',
WAITING_INIT_PTS: 'WAITING_INIT_PTS'
};

export default class BaseStreamController extends TaskLoop {
doTick () {}

_streamEnded (bufferInfo, levelDetails) {
const { fragCurrent, fragmentTracker } = this;
// we just got done loading the final fragment and there is no other buffered range after ...
// rationale is that in case there are any buffered ranges after, it means that there are unbuffered portion in between
// so we should not switch to ENDED in that case, to be able to buffer them
// dont switch to ENDED if we need to backtrack last fragment
if (!levelDetails.live && fragCurrent && !fragCurrent.backtracked && fragCurrent.sn === levelDetails.endSN && !bufferInfo.nextStart) {
const fragState = fragmentTracker.getState(fragCurrent);
return fragState === FragmentState.PARTIAL || fragState === FragmentState.OK;
}
return false;
}

onMediaSeeking () {
const { config, media, mediaBuffer, state } = this;
const currentTime = media ? media.currentTime : null;
const bufferInfo = BufferHelper.bufferInfo(mediaBuffer || media, currentTime, this.config.maxBufferHole);

if (Number.isFinite(currentTime)) {
logger.log(`media seeking to ${currentTime.toFixed(3)}`);
}

if (state === State.FRAG_LOADING) {
let fragCurrent = this.fragCurrent;
// check if we are seeking to a unbuffered area AND if frag loading is in progress
if (bufferInfo.len === 0 && fragCurrent) {
const tolerance = config.maxFragLookUpTolerance;
const fragStartOffset = fragCurrent.start - tolerance;
const fragEndOffset = fragCurrent.start + fragCurrent.duration + tolerance;
// check if we seek position will be out of currently loaded frag range : if out cancel frag load, if in, don't do anything
if (currentTime < fragStartOffset || currentTime > fragEndOffset) {
if (fragCurrent.loader) {
logger.log('seeking outside of buffer while fragment load in progress, cancel fragment load');
fragCurrent.loader.abort();
}
this.fragCurrent = null;
this.fragPrevious = null;
// switch to IDLE state to load new fragment
this.state = State.IDLE;
} else {
logger.log('seeking outside of buffer but within currently loaded fragment range');
}
}
} else if (state === State.ENDED) {
// if seeking to unbuffered area, clean up fragPrevious
if (bufferInfo.len === 0) {
this.fragPrevious = null;
this.fragCurrent = null;
}

// switch to IDLE state to check for potential new fragment
this.state = State.IDLE;
}
if (media) {
this.lastCurrentTime = currentTime;
}

// in case seeking occurs although no media buffered, adjust startPosition and nextLoadPosition to seek target
if (!this.loadedmetadata) {
this.nextLoadPosition = this.startPosition = currentTime;
}

// tick to speed up processing
this.tick();
}

onMediaEnded () {
// reset startPosition and lastCurrentTime to restart playback @ stream beginning
this.startPosition = this.lastCurrentTime = 0;
}
}
107 changes: 10 additions & 97 deletions src/controller/stream-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,13 @@ import TimeRanges from '../utils/time-ranges';
import { ErrorDetails } from '../errors';
import { logger } from '../utils/logger';
import { alignStream } from '../utils/discontinuities';
import TaskLoop from '../task-loop';
import { findFragmentByPDT, findFragmentByPTS } from './fragment-finders';
import GapController from './gap-controller';

export const State = {
STOPPED: 'STOPPED',
IDLE: 'IDLE',
KEY_LOADING: 'KEY_LOADING',
FRAG_LOADING: 'FRAG_LOADING',
FRAG_LOADING_WAITING_RETRY: 'FRAG_LOADING_WAITING_RETRY',
WAITING_LEVEL: 'WAITING_LEVEL',
PARSING: 'PARSING',
PARSED: 'PARSED',
BUFFER_FLUSHING: 'BUFFER_FLUSHING',
ENDED: 'ENDED',
ERROR: 'ERROR'
};
import BaseStreamController, { State } from './base-stream-controller';

const TICK_INTERVAL = 100; // how often to tick in ms

class StreamController extends TaskLoop {
class StreamController extends BaseStreamController {
constructor (hls, fragmentTracker) {
super(hls,
Event.MEDIA_ATTACHED,
Expand Down Expand Up @@ -246,32 +232,16 @@ class StreamController extends TaskLoop {
return;
}

// we just got done loading the final fragment and there is no other buffered range after ...
// rationale is that in case there are any buffered ranges after, it means that there are unbuffered portion in between
// so we should not switch to ENDED in that case, to be able to buffer them
// dont switch to ENDED if we need to backtrack last fragment
let fragPrevious = this.fragPrevious;
if (!levelDetails.live && fragPrevious && !fragPrevious.backtracked && fragPrevious.sn === levelDetails.endSN && !bufferInfo.nextStart) {
// fragPrevious is last fragment. retrieve level duration using last frag start offset + duration
// real duration might be lower than initial duration if there are drifts between real frag duration and playlist signaling
const duration = Math.min(media.duration, fragPrevious.start + fragPrevious.duration);
// if everything (almost) til the end is buffered, let's signal eos
// we don't compare exactly media.duration === bufferInfo.end as there could be some subtle media duration difference (audio/video offsets...)
// tolerate up to one frag duration to cope with these cases.
// also cope with almost zero last frag duration (max last frag duration with 200ms) refer to https://github.com/video-dev/hls.js/pull/657
if (duration - Math.max(bufferInfo.end, fragPrevious.start) <= Math.max(0.2, fragPrevious.duration)) {
// Finalize the media stream
let data = {};
if (this.altAudio) {
data.type = 'video';
}

this.hls.trigger(Event.BUFFER_EOS, data);
this.state = State.ENDED;
return;
if (this._streamEnded(bufferInfo, levelDetails)) {
const data = {};
if (this.altAudio) {
data.type = 'video';
}
}

this.hls.trigger(Event.BUFFER_EOS, data);
this.state = State.ENDED;
return;
}
// if we have the levelDetails for the selected variant, lets continue enrichen our stream (load keys/fragments or trigger EOS, etc..)
this._fetchPayloadOrEos(pos, bufferInfo, levelDetails);
}
Expand Down Expand Up @@ -757,57 +727,6 @@ class StreamController extends TaskLoop {
this.stopLoad();
}

onMediaSeeking () {
let media = this.media, currentTime = media ? media.currentTime : undefined, config = this.config;
if (Number.isFinite(currentTime)) {
logger.log(`media seeking to ${currentTime.toFixed(3)}`);
}

let mediaBuffer = this.mediaBuffer ? this.mediaBuffer : media;
let bufferInfo = BufferHelper.bufferInfo(mediaBuffer, currentTime, this.config.maxBufferHole);
if (this.state === State.FRAG_LOADING) {
let fragCurrent = this.fragCurrent;
// check if we are seeking to a unbuffered area AND if frag loading is in progress
if (bufferInfo.len === 0 && fragCurrent) {
let tolerance = config.maxFragLookUpTolerance,
fragStartOffset = fragCurrent.start - tolerance,
fragEndOffset = fragCurrent.start + fragCurrent.duration + tolerance;
// check if we seek position will be out of currently loaded frag range : if out cancel frag load, if in, don't do anything
if (currentTime < fragStartOffset || currentTime > fragEndOffset) {
if (fragCurrent.loader) {
logger.log('seeking outside of buffer while fragment load in progress, cancel fragment load');
fragCurrent.loader.abort();
}
this.fragCurrent = null;
this.fragPrevious = null;
// switch to IDLE state to load new fragment
this.state = State.IDLE;
} else {
logger.log('seeking outside of buffer but within currently loaded fragment range');
}
}
} else if (this.state === State.ENDED) {
// if seeking to unbuffered area, clean up fragPrevious
if (bufferInfo.len === 0) {
this.fragPrevious = 0;
}

// switch to IDLE state to check for potential new fragment
this.state = State.IDLE;
}
if (media) {
this.lastCurrentTime = currentTime;
}

// in case seeking occurs although no media buffered, adjust startPosition and nextLoadPosition to seek target
if (!this.loadedmetadata) {
this.nextLoadPosition = this.startPosition = currentTime;
}

// tick to speed up processing
this.tick();
}

onMediaSeeked () {
const media = this.media, currentTime = media ? media.currentTime : undefined;
if (Number.isFinite(currentTime)) {
Expand All @@ -818,12 +737,6 @@ class StreamController extends TaskLoop {
this.tick();
}

onMediaEnded () {
logger.log('media ended');
// reset startPosition and lastCurrentTime to restart playback @ stream beginning
this.startPosition = this.lastCurrentTime = 0;
}

onManifestLoading () {
// reset buffer on manifest loading
logger.log('trigger BUFFER_RESET');
Expand Down
Loading

0 comments on commit 3d68f31

Please sign in to comment.