Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: In-manifest VTT iOS MSE issue #1360

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 18 additions & 1 deletion src/playlist-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,24 @@ export class PlaylistController extends videojs.EventTarget {
this.subtitleSegmentLoader_ =
new VTTSegmentLoader(merge(segmentLoaderSettings, {
loaderType: 'vtt',
featuresNativeTextTracks: this.tech_.featuresNativeTextTracks
featuresNativeTextTracks: this.tech_.featuresNativeTextTracks,
loadVttJs: () => new Promise((resolve, reject) => {
function onLoad() {
tech.off('vttjserror', onError);
resolve();
}

function onError() {
tech.off('vttjsloaded', onLoad);
reject();
}

tech.one('vttjsloaded', onLoad);
tech.one('vttjserror', onError);

// safe to call multiple times, script will be loaded only once:
tech.addWebVttScript_();
})
}), options);

this.setupSegmentLoaderListeners_();
Expand Down
23 changes: 16 additions & 7 deletions src/videojs-http-streaming.js
Original file line number Diff line number Diff line change
Expand Up @@ -1223,16 +1223,25 @@ const VhsSourceHandler = {
tech.vhs.src(source.src, source.type);
return tech.vhs;
},
canPlayType(type, options = {}) {
const {
vhs: { overrideNative = !videojs.browser.IS_ANY_SAFARI } = {}
} = merge(videojs.options, options);
canPlayType(type, options) {
const simpleType = simpleTypeFromSourceType(type);

const supportedType = simpleTypeFromSourceType(type);
const canUseMsePlayback = supportedType &&
(!Vhs.supportsTypeNatively(supportedType) || overrideNative);
if (!simpleType) {
return '';
}

const overrideNative = VhsSourceHandler.getOverrideNative(options);
const supportsTypeNatively = Vhs.supportsTypeNatively(simpleType);
const canUseMsePlayback = !supportsTypeNatively || overrideNative;

return canUseMsePlayback ? 'maybe' : '';
},
getOverrideNative(options = {}) {
const { vhs = {} } = options;
const defaultOverrideNative = !(videojs.browser.IS_ANY_SAFARI || videojs.browser.IS_IOS);
const { overrideNative = defaultOverrideNative } = vhs;

return overrideNative;
}
};

Expand Down
46 changes: 24 additions & 22 deletions src/vtt-segment-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ import {createTimeRanges} from './util/vjs-compat';
const VTT_LINE_TERMINATORS =
new Uint8Array('\n\n'.split('').map(char => char.charCodeAt(0)));

class NoVttJsError extends Error {
constructor() {
super('Trying to parse received VTT cues, but there is no WebVTT. Make sure vtt.js is loaded.');
}
}

/**
* An object that manages segment loading and appending.
*
Expand All @@ -35,6 +41,8 @@ export default class VTTSegmentLoader extends SegmentLoader {

this.featuresNativeTextTracks_ = settings.featuresNativeTextTracks;

this.loadVttJs = settings.loadVttJs;

// The VTT segment will have its own time mappings. Saving VTT segment timing info in
// the sync controller leads to improper behavior.
this.shouldSaveSegmentTimingInfo_ = false;
Expand Down Expand Up @@ -298,29 +306,16 @@ export default class VTTSegmentLoader extends SegmentLoader {
}
segmentInfo.bytes = simpleSegment.bytes;

// Make sure that vttjs has loaded, otherwise, wait till it finished loading
if (typeof window.WebVTT !== 'function' &&
this.subtitlesTrack_ &&
this.subtitlesTrack_.tech_) {

let loadHandler;
const errorHandler = () => {
this.subtitlesTrack_.tech_.off('vttjsloaded', loadHandler);
this.stopForError({
message: 'Error loading vtt.js'
});
return;
};

loadHandler = () => {
this.subtitlesTrack_.tech_.off('vttjserror', errorHandler);
this.segmentRequestFinished_(error, simpleSegment, result);
};

// Make sure that vttjs has loaded, otherwise, load it and wait till it finished loading
if (typeof window.WebVTT !== 'function' && typeof this.loadVttJs === 'function') {
this.state = 'WAITING_ON_VTTJS';
this.subtitlesTrack_.tech_.one('vttjsloaded', loadHandler);
this.subtitlesTrack_.tech_.one('vttjserror', errorHandler);

// should be fine to call multiple times
// script will be loaded once but multiple listeners will be added to the queue, which is expected.
this.loadVttJs()
.then(
() => this.segmentRequestFinished_(error, simpleSegment, result),
() => this.stopForError({ message: 'Error loading vtt.js' })
);
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice tidy-up job here 🙌

return;
}

Expand Down Expand Up @@ -392,6 +387,8 @@ export default class VTTSegmentLoader extends SegmentLoader {
/**
* Uses the WebVTT parser to parse the segment response
*
* @throws NoVttJsError
*
* @param {Object} segmentInfo
* a segment info object that describes the current segment
* @private
Expand All @@ -400,6 +397,11 @@ export default class VTTSegmentLoader extends SegmentLoader {
let decoder;
let decodeBytesToString = false;

if (typeof window.WebVTT !== 'function') {
// caller is responsible for exception handling.
throw new NoVttJsError();
}

if (typeof window.TextDecoder === 'function') {
decoder = new window.TextDecoder('utf8');
} else {
Expand Down
19 changes: 19 additions & 0 deletions test/playlist-controller.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import QUnit from 'qunit';
import sinon from 'sinon';
import videojs from 'video.js';
import window from 'global/window';
import {
Expand Down Expand Up @@ -591,6 +592,24 @@ QUnit.test('resets everything for a fast quality change', function(assert) {
assert.deepEqual(removeFuncArgs, {start: 0, end: 60}, 'remove() called with correct arguments if media is changed');
});

QUnit.test('loadVttJs should be passed to the vttSegmentLoader and resolved on vttjsloaded', function(assert) {
const stub = sinon.stub(this.player.tech_, 'addWebVttScript_').callsFake(() => this.player.tech_.trigger('vttjsloaded'));
const controller = new PlaylistController({ src: 'test', tech: this.player.tech_});

controller.subtitleSegmentLoader_.loadVttJs().then(() => {
assert.equal(stub.callCount, 1, 'tech addWebVttScript called once');
});
});

QUnit.test('loadVttJs should be passed to the vttSegmentLoader and rejected on vttjserror', function(assert) {
const stub = sinon.stub(this.player.tech_, 'addWebVttScript_').callsFake(() => this.player.tech_.trigger('vttjserror'));
const controller = new PlaylistController({ src: 'test', tech: this.player.tech_});

controller.subtitleSegmentLoader_.loadVttJs().catch(() => {
assert.equal(stub.callCount, 1, 'tech addWebVttScript called once');
});
});

QUnit.test('seeks in place for fast quality switch on non-IE/Edge browsers', function(assert) {
let seeks = 0;

Expand Down
7 changes: 5 additions & 2 deletions test/videojs-http-streaming.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2980,12 +2980,14 @@ QUnit.test('has no effect if native HLS is available and browser is Safari', fun
videojs.browser.IS_ANY_SAFARI = origIsAnySafari;
});

QUnit.test('loads if native HLS is available but browser is not Safari', function(assert) {
QUnit.test('has no effect if native HLS is available and browser is any non-safari browser on ios', function(assert) {
const Html5 = videojs.getTech('Html5');
const oldHtml5CanPlaySource = Html5.canPlaySource;
const origIsAnySafari = videojs.browser.IS_ANY_SAFARI;
const originalIsIos = videojs.browser.IS_IOS;

videojs.browser.IS_ANY_SAFARI = false;
videojs.browser.IS_IOS = true;
Html5.canPlaySource = () => true;
Vhs.supportsNativeHls = true;
const player = createPlayer();
Expand All @@ -2997,10 +2999,11 @@ QUnit.test('loads if native HLS is available but browser is not Safari', functio

this.clock.tick(1);

assert.ok(player.tech_.vhs, 'loaded VHS tech');
assert.ok(!player.tech_.vhs, 'did not load vhs tech');
player.dispose();
Html5.canPlaySource = oldHtml5CanPlaySource;
videojs.browser.IS_ANY_SAFARI = origIsAnySafari;
videojs.browser.IS_IOS = originalIsIos;
});

QUnit.test(
Expand Down
121 changes: 83 additions & 38 deletions test/vtt-segment-loader.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from './loader-common.js';
import { encryptionKey, subtitlesEncrypted } from 'create-test-data!segments';
import {merge, createTimeRanges} from '../src/util/vjs-compat';
import sinon from 'sinon';

const oldVTT = window.WebVTT;

Expand Down Expand Up @@ -308,6 +309,19 @@ QUnit.module('VTTSegmentLoader', function(hooks) {
QUnit.test(
'waits for vtt.js to be loaded before attempting to parse cues',
function(assert) {
let promiseLoadVttJs; let resolveLoadVttJs;

loader = new VTTSegmentLoader(LoaderCommonSettings.call(this, {
loaderType: 'vtt',
loadVttJs: () => {
promiseLoadVttJs = new Promise((resolve) => {
resolveLoadVttJs = resolve;
});

return promiseLoadVttJs;
}
}), {});

const vttjs = window.WebVTT;
const playlist = playlistWithDuration(40);
let parsedCues = false;
Expand All @@ -319,22 +333,6 @@ QUnit.module('VTTSegmentLoader', function(hooks) {
loader.state = 'READY';
};

let vttjsCallback = () => {};

this.track.tech_ = {
one(event, callback) {
if (event === 'vttjsloaded') {
vttjsCallback = callback;
}
},
trigger(event) {
if (event === 'vttjsloaded') {
vttjsCallback();
}
},
off() {}
};

loader.playlist(playlist);
loader.track(this.track);
loader.load();
Expand All @@ -361,10 +359,58 @@ QUnit.module('VTTSegmentLoader', function(hooks) {

window.WebVTT = vttjs;

loader.subtitlesTrack_.tech_.trigger('vttjsloaded');
promiseLoadVttJs.then(() => {
assert.equal(loader.state, 'READY', 'loader is ready to load next segment');
assert.ok(parsedCues, 'parsed cues');
});

assert.equal(loader.state, 'READY', 'loader is ready to load next segment');
assert.ok(parsedCues, 'parsed cues');
resolveLoadVttJs();
}
);

QUnit.test(
'parse should throw if no vtt.js is loaded for any reason',
function(assert) {
const vttjs = window.WebVTT;
const playlist = playlistWithDuration(40);
let errors = 0;

const originalParse = loader.parseVTTCues_.bind(loader);

loader.parseVTTCues_ = (...args) => {
delete window.WebVTT;
return originalParse(...args);
};

const spy = sinon.spy(loader, 'error');

loader.on('error', () => errors++);

loader.playlist(playlist);
loader.track(this.track);
loader.load();

assert.equal(errors, 0, 'no error at loader start');

this.clock.tick(1);

// state WAITING for segment response
this.requests[0].responseType = 'arraybuffer';
this.requests.shift().respond(200, null, new Uint8Array(10).buffer);

this.clock.tick(1);

assert.equal(errors, 1, 'triggered error when parser emmitts fatal error');
assert.ok(loader.paused(), 'loader paused when encountering fatal error');
assert.equal(loader.state, 'READY', 'loader reset after error');
assert.ok(
spy.withArgs(sinon.match({
message: 'Trying to parse received VTT cues, but there is no WebVTT. Make sure vtt.js is loaded.'
})).calledOnce,
'error method called once with instance of NoVttJsError'
);

window.WebVTT = vttjs;
}
);

Expand Down Expand Up @@ -748,25 +794,22 @@ QUnit.module('VTTSegmentLoader', function(hooks) {
});

QUnit.test('loader triggers error event when vtt.js fails to load', function(assert) {
let promiseLoadVttJs; let rejectLoadVttJs;

loader = new VTTSegmentLoader(LoaderCommonSettings.call(this, {
loaderType: 'vtt',
loadVttJs: () => {
promiseLoadVttJs = new Promise((resolve, reject) => {
rejectLoadVttJs = reject;
});

return promiseLoadVttJs;
}
}), {});
const playlist = playlistWithDuration(40);
let errors = 0;

delete window.WebVTT;
let vttjsCallback = () => {};

this.track.tech_ = {
one(event, callback) {
if (event === 'vttjserror') {
vttjsCallback = callback;
}
},
trigger(event) {
if (event === 'vttjserror') {
vttjsCallback();
}
},
off() {}
};

loader.on('error', () => errors++);

Expand Down Expand Up @@ -794,11 +837,13 @@ QUnit.module('VTTSegmentLoader', function(hooks) {
);
assert.equal(errors, 0, 'no errors yet');

loader.subtitlesTrack_.tech_.trigger('vttjserror');
promiseLoadVttJs.catch(() => {
assert.equal(loader.state, 'READY', 'loader is reset to ready');
assert.ok(loader.paused(), 'loader is paused after error');
assert.equal(errors, 1, 'loader triggered error when vtt.js load triggers error');
});

assert.equal(loader.state, 'READY', 'loader is reset to ready');
assert.ok(loader.paused(), 'loader is paused after error');
assert.equal(errors, 1, 'loader triggered error when vtt.js load triggers error');
rejectLoadVttJs();
});

QUnit.test('does not save segment timing info', function(assert) {
Expand Down