src/controller/base-stream-controller.js
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',
WAITING_LEVEL: 'WAITING_LEVEL'
};
export default class BaseStreamController extends TaskLoop {
doTick () {}
startLoad () {}
stopLoad () {
let frag = this.fragCurrent;
if (frag) {
if (frag.loader) {
frag.loader.abort();
}
this.fragmentTracker.removeFragment(frag);
}
if (this.demuxer) {
this.demuxer.destroy();
this.demuxer = null;
}
this.fragCurrent = null;
this.fragPrevious = null;
this.clearInterval();
this.clearNextTick();
this.state = State.STOPPED;
}
_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;
}
onHandlerDestroying () {
this.stopLoad();
super.onHandlerDestroying();
}
onHandlerDestroyed () {
this.state = State.STOPPED;
this.fragmentTracker = null;
}
computeLivePosition (sliding, levelDetails) {
let targetLatency = this.config.liveSyncDuration !== undefined ? this.config.liveSyncDuration : this.config.liveSyncDurationCount * levelDetails.targetduration;
return sliding + Math.max(0, levelDetails.totalduration - targetLatency);
}
}