Skip to content

Commit

Permalink
Merge pull request video-dev#1545 from azu/stream-controller-fragment…
Browse files Browse the repository at this point in the history
…-tracker

refactor StreamController and FragmentTracker logics
  • Loading branch information
azu authored Feb 21, 2018
2 parents 2ca1b8a + 8a4833d commit bbd69fc
Show file tree
Hide file tree
Showing 6 changed files with 285 additions and 122 deletions.
24 changes: 5 additions & 19 deletions src/controller/stream-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Demuxer from '../demux/demuxer';
import Event from '../events';
import {FragmentState} from '../helper/fragment-tracker';
import Fragment from '../loader/fragment';
import PlaylistLoader from '../loader/playlist-loader';
import * as LevelHelper from '../helper/level-helper';
import TimeRanges from '../utils/time-ranges';
import {ErrorTypes, ErrorDetails} from '../errors';
Expand Down Expand Up @@ -569,16 +570,8 @@ class StreamController extends TaskLoop {
return this._state;
}

// TODO: Move this functionality into fragment-tracker.js
getBufferedFrag(position) {
return BinarySearch.search(this._bufferedFrags, function(frag) {
if (position < frag.startPTS) {
return -1;
} else if (position > frag.endPTS) {
return 1;
}
return 0;
});
return this.fragmentTracker.getBufferedFrag(position, PlaylistLoader.LevelType.MAIN);
}

get currentLevel() {
Expand Down Expand Up @@ -875,7 +868,7 @@ class StreamController extends TaskLoop {
// reset buffer on manifest loading
logger.log('trigger BUFFER_RESET');
this.hls.trigger(Event.BUFFER_RESET);
this._bufferedFrags = [];
this.fragmentTracker.removeAllFragments();
this.stalled = false;
this.startPosition = this.lastCurrentTime = 0;
}
Expand Down Expand Up @@ -1304,14 +1297,6 @@ class StreamController extends TaskLoop {
if (frag) {
const media = this.mediaBuffer ? this.mediaBuffer : this.media;
logger.log(`main buffered : ${TimeRanges.toString(media.buffered)}`);
// filter fragments potentially evicted from buffer. this is to avoid memleak on live streams
let bufferedFrags = BufferHelper.filterEvictedFragments(this._bufferedFrags, media);
// push new range
bufferedFrags.push(frag);
// sort frags, as we use BinarySearch for lookup in getBufferedFrag ...
this._bufferedFrags = bufferedFrags.sort(function(a, b) {
return (a.startPTS - b.startPTS);
});
this.fragPrevious = frag;
const stats = this.stats;
stats.tbuffered = performance.now();
Expand Down Expand Up @@ -1536,7 +1521,8 @@ _checkBuffer() {
use mediaBuffered instead of media (so that we will check against video.buffered ranges in case of alt audio track)
*/
const media = this.mediaBuffer ? this.mediaBuffer : this.media;
this._bufferedFrags = this._bufferedFrags.filter(frag => {return BufferHelper.isBuffered(media,(frag.startPTS + frag.endPTS) / 2);});
// filter fragments potentially evicted from buffer. this is to avoid memleak on live streams
this.fragmentTracker.detectEvictedFragments(Fragment.ElementaryStreamTypes.VIDEO, media.buffered);

// move to IDLE once flush complete. this should trigger new fragment loading
this.state = State.IDLE;
Expand Down
30 changes: 0 additions & 30 deletions src/helper/buffer-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,6 @@
*/

const BufferHelper = {
/**
* filter fragments potentially evicted from buffer.
*/
filterEvictedFragments: function(bufferedFrags, media) {
try {
if (media) {
// Cache `media.buffered` at first for performance
// accessing `media.buffered` have a cost
const mediaBuffered = media.buffered;
// accessing MediaElement property through a function call should be quite expensive
const bufferedPositions = [];
for (let i = 0; i < mediaBuffered.length; i++) {
bufferedPositions.push({ start: mediaBuffered.start(i), end: mediaBuffered.end(i) });
}
return bufferedFrags.filter(frag => {
const position = (frag.startPTS + frag.endPTS) / 2;
for (let i = 0; i < bufferedPositions.length; i++) {
if (position >= bufferedPositions[i].start && position <= bufferedPositions[i].end) {
return true;
}
}
return false;
});
}
} catch (error) {
// InvalidStateError: Failed to read the 'buffered' property from 'SourceBuffer':
// This SourceBuffer has been removed from the parent media source
}
return [];
},
/**
* Return true if `media`'s buffered include `position`
* @param {HTMLMediaElement|SourceBuffer} media
Expand Down
49 changes: 48 additions & 1 deletion src/helper/fragment-tracker.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,42 @@ export class FragmentTracker extends EventHandler {
super.destroy();
}


/**
* Return a Fragment that match the position and levelType.
* If not found any Fragment, return null
* @param {number} position
* @param {LevelType} levelType
* @returns {Fragment|null}
*/
getBufferedFrag(position, levelType) {
const fragments = this.fragments;
const bufferedFrags = Object.keys(fragments).filter(key => {
const fragmentEntity = fragments[key];
if(fragmentEntity.body.type !== levelType){
return false;
}
if(!fragmentEntity.buffered){
return false;
}
const frag = fragmentEntity.body;
return frag.startPTS <= position && position <= frag.endPTS;
});
if (bufferedFrags.length === 0) {
return null;
} else {
// https://github.com/video-dev/hls.js/pull/1545#discussion_r166229566
const bufferedFragKey = bufferedFrags.pop();
return fragments[bufferedFragKey].body;
}
}

/**
* Partial fragments effected by coded frame eviction will be removed
* The browser will unload parts of the buffer to free up memory for new buffer data
* Fragments will need to be reloaded when the buffer is freed up, removing partial fragments will allow them to reload(since there might be parts that are still playable)
* @param {String} elementaryStream The elementaryStream of media this is (eg. video/audio)
* @param {Object} timeRange TimeRange object from a sourceBuffer
* @param {TimeRanges} timeRange TimeRange object from a sourceBuffer
*/
detectEvictedFragments(elementaryStream, timeRange) {
let fragmentTimes, time;
Expand Down Expand Up @@ -227,6 +257,16 @@ export class FragmentTracker extends EventHandler {
this.detectPartialFragments(e.frag);
}

/**
* Return true if fragment tracker has the fragment.
* @param {Object} fragment
* @returns {boolean}
*/
hasFragment(fragment) {
const fragKey = this.getFragmentKey(fragment);
return this.fragments[fragKey] !== undefined;
}

/**
* Remove a fragment from fragment tracker until it is loaded again
* @param {Object} fragment The fragment to remove
Expand All @@ -235,4 +275,11 @@ export class FragmentTracker extends EventHandler {
let fragKey = this.getFragmentKey(fragment);
delete this.fragments[fragKey];
}

/**
* Remove all fragments from fragment tracker.
*/
removeAllFragments(){
this.fragments = Object.create(null);
}
}
24 changes: 21 additions & 3 deletions src/loader/playlist-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ const ContextType = {
SUBTITLE_TRACK: 'subtitleTrack'
};

/**
* @enum {string}
*/
const LevelType = {
MAIN: 'main',
AUDIO: 'audio',
SUBTITLE: 'subtitle'
};

/**
* @constructor
*/
Expand All @@ -53,6 +62,10 @@ class PlaylistLoader extends EventHandler {
return ContextType;
}

static get LevelType() {
return LevelType;
}

/**
* @param {ContextType} type
* @returns {boolean}
Expand All @@ -62,16 +75,21 @@ class PlaylistLoader extends EventHandler {
type !== ContextType.SUBTITLE_TRACK);
}

/**
* Map context.type to LevelType
* @param {{type: ContextType}} context
* @returns {LevelType}
*/
static mapContextToLevelType(context) {
const {type} = context;

switch(type) {
case ContextType.AUDIO_TRACK:
return 'audio';
return LevelType.AUDIO;
case ContextType.SUBTITLE_TRACK:
return 'subtitle';
return LevelType.SUBTITLE;
default:
return 'main';
return LevelType.MAIN;
}
}

Expand Down
68 changes: 0 additions & 68 deletions tests/unit/helper/buffer-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,74 +10,6 @@ function createMockBuffer(buffered) {
}

describe('BufferHelper', function() {
describe('filterEvictedFragments', function() {
it("should return empty array if the media is invalid", () => {
const invalidMedia = {
get buffered() {
throw new Error("InvalidStateError");
}
};
const fragments = [
{
startPTS: 0,
endPTS: 0.5
},
{
startPTS: 1,
endPTS: 2.0
}
];
const filteredFragments = BufferHelper.filterEvictedFragments(fragments, invalidMedia);
assert.equal(filteredFragments.length, 0);
});
it("should return fragments that are not evicted", () => {
// |__________|//////////|//////////|__________|
// 0 1.0 2.0 3.0 4.0
const media = {
get buffered() {
return createMockBuffer([
{
startPTS: 1,
endPTS: 2.0
},
{
startPTS: 2.0,
endPTS: 3.0
}
]);
}
};
// |////|/////|//////////|//////////|//////////|
// 0 1.0 2.0 3.0 4.0
const fragments = [
// ↓ out of buffer ↓
{
startPTS: 0,
endPTS: 0.5
},
{
startPTS: 0.5,
endPTS: 1.0
},
// ↑ out of buffer ↑
{
startPTS: 1.0,
endPTS: 2.0
},
{
startPTS: 2.0,
endPTS: 3.0
},
// ↓ out of buffer ↓
{
startPTS: 3.0,
endPTS: 4.0
}
];
const filteredFragments = BufferHelper.filterEvictedFragments(fragments, media);
assert.deepEqual(filteredFragments, [fragments[2], fragments[3]]);
});
});
describe('isBuffered', function() {
// |////////|__________|////////////////|
// 0 0.5 1 2.0
Expand Down
Loading

0 comments on commit bbd69fc

Please sign in to comment.