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
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
4220a0b
added vtt playlist loader
mjneil Feb 23, 2017
29d7487
start of vtt-segment-loader, request segment and parse cues
mjneil Feb 24, 2017
0a5fc1e
Add cues when handling subtitle segment and clear cues when changing …
gesinger Feb 24, 2017
75bf022
Don't call endOfStream from vtt segment loader
gesinger Feb 24, 2017
0c788ba
Move creation of new subtitles playlist loader on URI change
gesinger Feb 24, 2017
06a6c65
Reset subtitle segment loader only when using new subtitles
gesinger Feb 24, 2017
5fdf933
Fill out buffered function for vtt segment loader
gesinger Feb 24, 2017
7b156bc
Wait for video timestamp offsets in vtt segment loader
gesinger Feb 24, 2017
69b5e5e
Fix some loads and disposes for subtitle segment loader
gesinger Feb 24, 2017
739ba90
Don't use a default subtitle track
gesinger Feb 25, 2017
39741d0
Fix some subtitle playlist and segment loader pausing and clearing
gesinger Feb 28, 2017
e7103e3
Fill out vtt segment loader's remove function
gesinger Feb 28, 2017
836c5b4
Fix cue time references in vtt buffered check
gesinger Mar 1, 2017
d2dc66c
fix delay when switching subtitle tracks
mjneil Mar 1, 2017
d91da5e
unpause subtitle segment loader when selecting active track
mjneil Mar 1, 2017
840c709
Reset to subtitles off when subtitle playlist errors
gesinger Mar 1, 2017
0d150f1
Remove unnecessary nullification of subtitle playlist loader on segme…
gesinger Mar 1, 2017
9ba9bc7
Handle subtitle errors generically
gesinger Mar 1, 2017
36a4099
Remove commented out subtitle code for handling forced subtitles
gesinger Mar 1, 2017
3ecaa71
Move warn log to handleSubtitleError and dispose subtitle segment loa…
gesinger Mar 1, 2017
f3939fe
add decryption for vtt loader
mjneil Mar 1, 2017
5483ea6
Fix linting errors
gesinger Mar 2, 2017
21885c2
add support for vtt init segment
mjneil Mar 2, 2017
50f88a8
append line terminators to init segment instead of segment
mjneil Mar 2, 2017
ea37686
require track before loading can begin, start tests
mjneil Mar 3, 2017
ffcc4bc
fix vtt loader tests
mjneil Mar 6, 2017
fa021d4
fix vtt loader waiting on timeline and vttjs
mjneil Mar 7, 2017
1791581
test vtt segment time mapping
mjneil Mar 7, 2017
5a5cfe5
add vttjs error handling
mjneil Mar 7, 2017
bb96ff8
Removed unused subtitleupdate event
gesinger Mar 2, 2017
3907bc9
Add test for adding subtitle tracks when media playlist is loaded
gesinger Mar 2, 2017
9941345
Add test for switching off subtitles on subtitle errors
gesinger Mar 6, 2017
ab2fbfc
Add test for pausing subtitle loader on tech error
gesinger Mar 6, 2017
379bfc4
Add test for disposing subtitle segment and playlist loaders
gesinger Mar 6, 2017
32dd51c
Add test for resetting subtitle segment loader on seeks
gesinger Mar 6, 2017
9b78a9b
Add test for getting active subtitle group
gesinger Mar 7, 2017
443be25
Add test for getting active subtitle track
gesinger Mar 7, 2017
1afd470
Add test for handling subtitle errors from master playlist controller
gesinger Mar 7, 2017
5102875
Add test for subtitles setup
gesinger Mar 7, 2017
178fef2
update readme
mjneil Mar 7, 2017
46ffa3e
remove forced logging from vtt-loader
mjneil Mar 8, 2017
f850fdf
Add master-subtitles.m3u8 manifest for tests
gesinger Mar 8, 2017
47fcb24
ignore: copy segment-loader files for diffing
mjneil Mar 14, 2017
85aba5e
merge fixes
mjneil Mar 15, 2017
f66f3f1
prevent infinitely requesting same segment when segment has no subtit…
mjneil Mar 16, 2017
047083c
Fix MasterPlaylistController subtitle tests by considering segment-me…
gesinger Mar 16, 2017
514b06b
ignore: fix missing semicolon
mjneil Mar 16, 2017
4dbe477
ignore: rebase fix
mjneil Mar 20, 2017
fed4daa
put init segment getting/setting into function
mjneil Mar 20, 2017
ebba86d
remove path to vttjs
mjneil Mar 20, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Maintenance Status: Stable
- [Events](#events)
- [loadedmetadata](#loadedmetadata)
- [In-Band Metadata](#in-band-metadata)
- [Segment Metadata](#segment-metadata)
- [Hosting Considerations](#hosting-considerations)
- [Known Issues](#known-issues)
- [IE10 and Below](#ie10-and-below)
Expand Down Expand Up @@ -146,6 +147,8 @@ are some highlights:
- AES-128 segment encryption
- CEA-608 captions are automatically translated into standard HTML5
[caption text tracks][0]
- In-Manifest WebVTT subtitles are automatically translated into standard HTML5
subtitle tracks
- Timed ID3 Metadata is automatically translated into HTML5 metedata
text tracks
- Highly customizable adaptive bitrate selection
Expand Down
207 changes: 203 additions & 4 deletions src/master-playlist-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/
import PlaylistLoader from './playlist-loader';
import SegmentLoader from './segment-loader';
import VTTSegmentLoader from './vtt-segment-loader';
import Ranges from './ranges';
import videojs from 'video.js';
import AdCueTags from './ad-cue-tags';
Expand Down Expand Up @@ -231,13 +232,13 @@ export class MasterPlaylistController extends videojs.EventTarget {
this.cueTagsTrack_.inBandMetadataTrackDispatchType = '';
}

this.audioTracks_ = [];
this.requestOptions_ = {
withCredentials: this.withCredentials,
timeout: null
};

this.audioGroups_ = {};
this.subtitleGroups_ = { groups: {}, tracks: {} };

this.mediaSource = new videojs.MediaSource({ mode });
this.audioinfo_ = null;
Expand Down Expand Up @@ -274,6 +275,7 @@ export class MasterPlaylistController extends videojs.EventTarget {
this.masterPlaylistLoader_ = new PlaylistLoader(url, this.hls_, this.withCredentials);
this.setupMasterPlaylistLoaderListeners_();
this.audioPlaylistLoader_ = null;
this.subtitlePlaylistLoader_ = null;

// setup segment loaders
// combined audio/video or just video when alternate audio track is selected
Expand All @@ -287,14 +289,18 @@ export class MasterPlaylistController extends videojs.EventTarget {
loaderType: 'audio'
}));

this.setupSegmentLoaderListeners_();
this.subtitleSegmentLoader_ = new VTTSegmentLoader(videojs.mergeOptions(segmentLoaderOptions, {
loaderType: 'vtt'
}));

this.masterPlaylistLoader_.start();
this.setupSegmentLoaderListeners_();

// Create SegmentLoader stat-getters
loaderStats.forEach((stat) => {
this[stat + '_'] = sumLoaderStat.bind(this, stat);
});

this.masterPlaylistLoader_.load();
}

/**
Expand Down Expand Up @@ -326,6 +332,9 @@ export class MasterPlaylistController extends videojs.EventTarget {
this.fillAudioTracks_();
this.setupAudio();

this.fillSubtitleTracks_();
this.setupSubtitles();

try {
this.setupSourceBuffers_();
} catch (e) {
Expand Down Expand Up @@ -422,6 +431,7 @@ export class MasterPlaylistController extends videojs.EventTarget {
this.setupAudio();
this.trigger('audioupdate');
}
this.setupSubtitles();

this.tech_.trigger({
type: 'mediachange',
Expand Down Expand Up @@ -465,6 +475,8 @@ export class MasterPlaylistController extends videojs.EventTarget {
this.audioPlaylistLoader_ = null;
this.setupAudio();
});

this.subtitleSegmentLoader_.on('error', this.handleSubtitleError_.bind(this));
}

handleAudioinfoUpdate_(event) {
Expand Down Expand Up @@ -576,6 +588,45 @@ export class MasterPlaylistController extends videojs.EventTarget {

return kind;
}
/**
* fill our internal list of Subtitle Tracks with data from
* the master playlist or use a default
*
* @private
*/
fillSubtitleTracks_() {
let master = this.master();
let mediaGroups = master.mediaGroups || {};

for (let mediaGroup in mediaGroups.SUBTITLES) {
if (!this.subtitleGroups_.groups[mediaGroup]) {
this.subtitleGroups_.groups[mediaGroup] = [];
}

for (let label in mediaGroups.SUBTITLES[mediaGroup]) {
let properties = mediaGroups.SUBTITLES[mediaGroup][label];

if (!properties.forced) {
this.subtitleGroups_.groups[mediaGroup].push(
videojs.mergeOptions({ id: label }, properties));

if (typeof this.subtitleGroups_.tracks[label] === 'undefined') {
let track = this.tech_.addRemoteTextTrack({
id: label,
kind: 'subtitles',
enabled: false,
language: properties.language,
label
}, true).track;

this.subtitleGroups_.tracks[label] = track;
}
}
}
}

// Do not enable a default subtitle track. Wait for user interaction instead.
}

/**
* Call load on our SegmentLoaders
Expand All @@ -585,6 +636,9 @@ export class MasterPlaylistController extends videojs.EventTarget {
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.

}
}

/**
Expand All @@ -602,6 +656,50 @@ export class MasterPlaylistController extends videojs.EventTarget {
return result || this.audioGroups_.main;
}

/**
* Returns the subtitle group for the currently active primary
* media playlist.
*/
activeSubtitleGroup_() {
let videoPlaylist = this.masterPlaylistLoader_.media();
let result;

if (!videoPlaylist) {
return null;
}

if (videoPlaylist.attributes && videoPlaylist.attributes.SUBTITLES) {
result = this.subtitleGroups_.groups[videoPlaylist.attributes.SUBTITLES];
}

return result || this.subtitleGroups_.groups.main;
}

activeSubtitleTrack_() {
for (let trackName in this.subtitleGroups_.tracks) {
if (this.subtitleGroups_.tracks[trackName].mode === 'showing') {
return this.subtitleGroups_.tracks[trackName];
}
}

return null;
}

handleSubtitleError_() {
videojs.log.warn('Problem encountered loading the subtitle track' +
'. Switching back to default.');

this.subtitleSegmentLoader_.abort();

let track = this.activeSubtitleTrack_();

if (track) {
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.

}

/**
* Determine the correct audio rendition based on the active
* AudioTrack and initialize a PlaylistLoader and SegmentLoader if
Expand Down Expand Up @@ -642,7 +740,7 @@ export class MasterPlaylistController extends videojs.EventTarget {
this.audioPlaylistLoader_ = new PlaylistLoader(track.properties_.resolvedUri,
this.hls_,
this.withCredentials);
this.audioPlaylistLoader_.start();
this.audioPlaylistLoader_.load();

this.audioPlaylistLoader_.on('loadedmetadata', () => {
let audioPlaylist = this.audioPlaylistLoader_.media();
Expand Down Expand Up @@ -686,6 +784,93 @@ export class MasterPlaylistController extends videojs.EventTarget {
});
}

/**
* Determine the correct subtitle playlist based on the active
* SubtitleTrack and initialize a PlaylistLoader and SegmentLoader if
* necessary. This method is called once automatically before
* playback begins to enable the default subtitle track and should be
* invoked again if the track is changed.
*/
setupSubtitles() {
let subtitleGroup = this.activeSubtitleGroup_();
let track = this.activeSubtitleTrack_();

this.subtitleSegmentLoader_.pause();

if (!track) {
// stop playlist and segment loading for subtitles
if (this.subtitlePlaylistLoader_) {
this.subtitlePlaylistLoader_.dispose();
this.subtitlePlaylistLoader_ = null;
}
return;
}

let properties = subtitleGroup.filter((subtitleProperties) => {
return subtitleProperties.id === track.id;
})[0];

// startup playlist and segment loaders for the enabled subtitle track
if (!this.subtitlePlaylistLoader_ ||
// if the media hasn't loaded yet, we don't have the URI to check, so it is
// easiest to simply recreate the playlist loader
!this.subtitlePlaylistLoader_.media() ||
this.subtitlePlaylistLoader_.media().resolvedUri !== properties.resolvedUri) {

if (this.subtitlePlaylistLoader_) {
this.subtitlePlaylistLoader_.dispose();
}

// reset the segment loader only when the subtitle playlist is changed instead of
// every time setupSubtitles is called since switching subtitle tracks fires
// multiple `change` events on the TextTrackList
this.subtitleSegmentLoader_.resetEverything();

// can't reuse playlistloader because we're only using single renditions and not a
// proper master
this.subtitlePlaylistLoader_ = new PlaylistLoader(properties.resolvedUri,
this.hls_,
this.withCredentials);

this.subtitlePlaylistLoader_.on('loadedmetadata', () => {
let subtitlePlaylist = this.subtitlePlaylistLoader_.media();

this.subtitleSegmentLoader_.playlist(subtitlePlaylist, this.requestOptions_);
this.subtitleSegmentLoader_.track(this.activeSubtitleTrack_());

// if the video is already playing, or if this isn't a live video and preload
// permits, start downloading segments
if (!this.tech_.paused() ||
(subtitlePlaylist.endList && this.tech_.preload() !== 'none')) {
this.subtitleSegmentLoader_.load();
}
});

this.subtitlePlaylistLoader_.on('loadedplaylist', () => {
let updatedPlaylist;

if (this.subtitlePlaylistLoader_) {
updatedPlaylist = this.subtitlePlaylistLoader_.media();
}

if (!updatedPlaylist) {
return;
}

this.subtitleSegmentLoader_.playlist(updatedPlaylist, this.requestOptions_);
});

this.subtitlePlaylistLoader_.on('error', this.handleSubtitleError_.bind(this));
}

if (this.subtitlePlaylistLoader_.media() &&
this.subtitlePlaylistLoader_.media().resolvedUri === properties.resolvedUri) {
this.subtitleSegmentLoader_.load();
} else {
this.subtitlePlaylistLoader_.load();
}
}

/**
* Re-tune playback quality level for the current player
* conditions. This method may perform destructive actions, like
Expand Down Expand Up @@ -847,6 +1032,9 @@ export class MasterPlaylistController extends videojs.EventTarget {
if (this.audioPlaylistLoader_) {
this.audioSegmentLoader_.pause();
}
if (this.subtitlePlaylistLoader_) {
this.subtitleSegmentLoader_.pause();
}
}

/**
Expand Down Expand Up @@ -888,12 +1076,19 @@ export class MasterPlaylistController extends videojs.EventTarget {
this.audioSegmentLoader_.resetEverything();
this.audioSegmentLoader_.abort();
}
if (this.subtitlePlaylistLoader_) {
this.subtitleSegmentLoader_.resetEverything();
this.subtitleSegmentLoader_.abort();
}

if (!this.tech_.paused()) {
this.mainSegmentLoader_.load();
if (this.audioPlaylistLoader_) {
this.audioSegmentLoader_.load();
}
if (this.subtitlePlaylistLoader_) {
this.subtitleSegmentLoader_.load();
}
}
}

Expand Down Expand Up @@ -1011,7 +1206,11 @@ export class MasterPlaylistController extends videojs.EventTarget {
if (this.audioPlaylistLoader_) {
this.audioPlaylistLoader_.dispose();
}
if (this.subtitlePlaylistLoader_) {
this.subtitlePlaylistLoader_.dispose();
}
this.audioSegmentLoader_.dispose();
this.subtitleSegmentLoader_.dispose();
}

/**
Expand Down
Loading