Skip to content
This repository has been archived by the owner on Jan 12, 2019. It is now read-only.

In-Manifest WebVTT Support #1057

Merged
merged 50 commits into from
Mar 20, 2017
Merged

Conversation

mjneil
Copy link
Contributor

@mjneil mjneil commented Mar 14, 2017

Description

This adds support for in-manifest VTT

Specific Changes proposed

  • uses PlaylistLoader to continuously refresh subtitle playlist in live playback
  • Adds VTTSegmentLoader for loading vtt segments
  • updates m3u8-parser to v2.1.0
  • uses vtt.js to parse X-TIMESTAMP-MAP header from vtt segments (Requires gkatsev/vtt.js#4)
  • supports using an init segment via EXT-X-MAP tag in the playlist

Requirements Checklist

  • Feature implemented / Bug fixed
  • Unit Tests updated or fixed
  • Docs/guides updated
  • Example created (starter template on JSBin)
  • Reviewed by Two Core Contributors

}
};

this.setupSegmentLoaderListeners_();

this.masterPlaylistLoader_.start();
this.masterPlaylistLoader_.load();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From #1043
@imbcmdth: Just curious: why this was renamed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gesinger and I noticed that load calls start if start hasn't been called yet and is safe to call multiple times, but start should only be called once. Since this call is in the MasterPlaylistController constructor, theres no risk of running into that scenario, but we decided to just change any calls to start into load so we didn't have to concern ourselves with it. We can change it back if you think start makes more sense semantically

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me. 👍

* Call load on our SegmentLoaders
*/
load() {
this.mainSegmentLoader_.load();
if (this.audioPlaylistLoader_) {
this.audioSegmentLoader_.load();
}
if (this.subtitlePlaylistLoader_) {
this.subtitleSegmentLoader_.load();
Copy link
Contributor Author

@mjneil mjneil Mar 15, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from #1043
@imbcmdth: At some point I feel like it might just be nice to have an array of active segment loaders. Then we can just do this.segmentLoaders_.forEach((l) => l.load());... not necessary now but maybe a good idea to think about soon.

track.mode = 'disabled';
}

this.setupSubtitles();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from #1043
@imbcmdth: If handleSubtitleError_ calls setupSubtitles is there a possibility that an infinite loop of failure will result?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It shouldn't since just above we set the current active tracks mode to 'disabled'. This has the same effect as turning off subtitles through the UI.

}

// reset the segment loader
this.subtitleSegmentLoader_.resetEverything();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from #1043
@imbcmdth: Why not move this to where the SegmentLoader is paused above. It's easier to reason about the state of the loader if it is manipulated in one place. Also, shouldn't it be reset even if there is no track?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setupSubtitles gets called everytime the player's TextTrackList fires a change event. When changing subtitle tracks through the player ui, depending on which track was selected and which track you are changing to, this event will fire multiple times. (it also fires a change event if you select the already active track in the UI) Only one of these calls will actually reset the playlist loader and segment loader to start loading from a new track. By only resetting when we are actually changing tracks, we avoid reseting the segment loader after its already started from the previous call, or if the user selected the already active track, then all that happens is the segment loader is paused and unpaused. Also, yes it should be reset even if there is no track, but since we always reset before starting a new track, it will get reset when the user turns subtitles back on.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like a short version of this explanation needs to be in this function somewhere!

}

timestampOffset() {
return this.syncController_.timestampOffsetForTimeline(this.currentTimeline_);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from #1043
@imbcmdth: Is this function used anywhere? If so, it needs a comment.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, is it? 😆

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the vtt-loader, no it isnt


combinedSegment.set(initSegment.bytes);
combinedSegment.set(vttLineTerminators, initSegment.bytes.byteLength);
segment.map.bytes = combinedSegment;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from #1043
@imbcmdth: Couple of thoughts on this whole block:

  1. We should pre-calculate the vttLineTerminators value because it's a typed array that never changes.
  2. I think we should do the entire append process when we first get the response for the initSegment so that we the object stored in this.initSegments_[initId] already has the value that would be in combinedSegment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with both, good ideas

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do it! 😃

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it will be better to do these changes when we integrate mediaSegmentRequest since that changes when the maps are stored in initSegments_

this.parseVTTCues_(segmentInfo);
} catch (e) {
this.error({
message: e.message
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from #1043
@imbcmdth: If error is fatal it should have some code set in addition to the message. The code is important for reporting reasons.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We decided not to do this now as we plan on revisiting how and when we emit errors at some point in the near future.

const midPoint = (firstStart + lastStart) / 2;

segment.start = midPoint - (segment.duration / 2);
segment.end = midPoint + (segment.duration / 2);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from #1043
@imbcmdth: Everything about this feels wrong and dirty! 😱

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We decided to try not storing segment start and end times and letting the conservative guess make it work since web vtt segments should be much smaller than video or audio segments.

One thing to be careful about is that the "resync" functionality needs to be removed since it will continuously reset the segment loader on poor guess instead of just walking forward.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the resync functionality actually checks for the existence of segment.end so we are safe leaving it here if we never set it

@@ -56,7 +56,7 @@
<br>
<label>
Player Options:
<input id="player-options" type="text" value='{}'>
<input id="player-options" type="text" value='{"vtt.js": "PATH/TO/vtt.js"}'>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reminder to self to revert back before releasing

this.mediaSource_.readyState === 'ended' &&
!this.seeking_()) {
this.syncController_.on('timestampoffset', checkTimestampOffset);
this.state = 'WAITING_ON_TIMELINE';
Copy link
Member

@imbcmdth imbcmdth Mar 15, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if we switch subtitles or seek while the state is 'WAITING_ON_TIMELINE'?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While the state is not WAITING any aborts will just clear pending segment (which at this point there isn't one yet) and does nothing else. The callback will set the state to ready and call monitorBuffer_ to restart the lifecycle. Since it starts back from the beginning, any seeks or subtitle changes that happened in the waiting time shouldn't have adverse effects

this.handleSegment_();
};

this.state = 'WAITING_ON_VTTJS';
Copy link
Member

@imbcmdth imbcmdth Mar 15, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if we switch subtitles or seek while the state is 'WAITING_ON_VTTJS'?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is basically the same as aborting while waiting in the DECRYPTING stage, the callback will call handleSegment_ again and if it was aborted while we were waiting, the first if block will reset the loader to READY

};

this.state = 'WAITING_ON_VTTJS';
this.subtitlesTrack_.tech_.on('vttjsloaded', loadHandler);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like the perfect case to use this.subtitlesTrack_.tech_.one('vttjsloaded', ...)

if (segmentInfo.mediaIndex === this.playlist_.segments.length - 1 &&
this.mediaSource_.readyState === 'ended' &&
!this.seeking_()) {
this.syncController_.on('timestampoffset', checkTimestampOffset);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like the perfect case to use this.syncController_.one('timestampoffset', ...)

@@ -403,7 +403,7 @@ QUnit.test('only appends one segment at a time', function(assert) {
assert.equal(loader.mediaRequests, 1, '1 request');
});

QUnit.test('adjusts the playlist offset if no buffering progress is made', function(assert) {
QUnit.skip('adjusts the playlist offset if no buffering progress is made', function(assert) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this test skipped?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is actually outdated from before simple-fetcher times when there was the offset correction variable to handle incorrect guesses, (now we just walk forward), it just wasnt skipped/removed previously because it happened to pass after segment loader changes. If you notice the other playlist offset tests are also skipped. I was planning on cleaning up skipped tests when the loaders get refactored

@imbcmdth imbcmdth merged commit 28bbd57 into videojs:feature/webvtt Mar 20, 2017
@mjneil mjneil mentioned this pull request Mar 21, 2017
imbcmdth added a commit that referenced this pull request Mar 31, 2017
* In-Manifest WebVTT Support (#1057)

* Minor loader fixes (#1065)
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants