diff --git a/README.md b/README.md
index 88dee8b06..564c43195 100644
--- a/README.md
+++ b/README.md
@@ -39,6 +39,7 @@ Video.js Compatibility: 7.x, 8.x
- [List](#list)
- [withCredentials](#withcredentials)
- [useCueTags](#usecuetags)
+ - [useSegmentMetadataTrack](#usesegmentmetadatatrack)
- [parse708captions](#parse708captions)
- [overrideNative](#overridenative)
- [experimentalUseMMS](#experimentalusemms)
@@ -317,6 +318,30 @@ cuesTrack.addEventListener('cuechange', function() {
});
```
+##### useSegmentMetadataTrack
+* Type: `boolean`
+* Default: `true`
+* can be used as an initialization option or a source option
+
+When set to `false`, VHS does not create the `segment-metadata` text track.
+Segment metadata cues and `cuechange` events from that track are unavailable.
+Other text tracks, including subtitles and in-band metadata, are not affected.
+
+This can be used to work around playback stalls on affected Safari/iOS versions
+when using `overrideNative` with native text tracks. See [#1600](https://github.com/videojs/http-streaming/issues/1600).
+Applications that need the segment metadata track should leave this option enabled.
+
+```javascript
+var player = videojs('playerId', {
+ html5: {
+ vhs: {
+ overrideNative: true,
+ useSegmentMetadataTrack: false
+ }
+ }
+});
+```
+
##### parse708captions
* Type: `boolean`
* Default: `true`
diff --git a/index.html b/index.html
index d8f42d7e5..f1e7f0021 100644
--- a/index.html
+++ b/index.html
@@ -202,6 +202,11 @@
Use native text tracks (reloads player)
+
+
+ Use segment metadata track (reloads player)
+
+
Preload (reloads player)
diff --git a/scripts/index.js b/scripts/index.js
index a191fe03c..5740cbe46 100644
--- a/scripts/index.js
+++ b/scripts/index.js
@@ -476,7 +476,8 @@
'preload',
'mirror-source',
'forced-subtitles',
- 'native-text-tracks'
+ 'native-text-tracks',
+ 'segment-metadata-track'
].forEach(function(name) {
stateEls[name] = document.getElementById(name);
});
@@ -532,7 +533,8 @@
'dts-offset',
'exact-manifest-timings',
'forced-subtitles',
- 'native-text-tracks'
+ 'native-text-tracks',
+ 'segment-metadata-track'
].forEach(function(name) {
stateEls[name].addEventListener('change', function(event) {
saveState();
@@ -618,6 +620,7 @@
useNetworkInformationApi: getInputValue(stateEls['network-info']),
useDtsForTimestampOffset: getInputValue(stateEls['dts-offset']),
useForcedSubtitles: getInputValue(stateEls['forced-subtitles']),
+ useSegmentMetadataTrack: getInputValue(stateEls['segment-metadata-track']),
usePlayerObjectFit: getInputValue(stateEls['object-fit'])
}
}
diff --git a/src/playlist-controller.js b/src/playlist-controller.js
index d4181509f..f2097342b 100644
--- a/src/playlist-controller.js
+++ b/src/playlist-controller.js
@@ -249,12 +249,14 @@ export class PlaylistController extends videojs.EventTarget {
this.hasPlayed_ = false;
this.syncController_ = new SyncController(options);
- this.segmentMetadataTrack_ = tech.addRemoteTextTrack({
- kind: 'metadata',
- label: 'segment-metadata'
- }, false).track;
+ if (options.useSegmentMetadataTrack !== false) {
+ this.segmentMetadataTrack_ = tech.addRemoteTextTrack({
+ kind: 'metadata',
+ label: 'segment-metadata'
+ }, false).track;
- this.segmentMetadataTrack_.mode = 'hidden';
+ this.segmentMetadataTrack_.mode = 'hidden';
+ }
this.decrypter_ = new Decrypter();
this.sourceUpdater_ = new SourceUpdater(this.mediaSource);
diff --git a/src/videojs-http-streaming.js b/src/videojs-http-streaming.js
index 21dfe7aad..90916b1a3 100644
--- a/src/videojs-http-streaming.js
+++ b/src/videojs-http-streaming.js
@@ -697,6 +697,7 @@ class VhsHandler extends Component {
this.source_.useBandwidthFromLocalStorage :
this.options_.useBandwidthFromLocalStorage || false;
this.options_.useForcedSubtitles = this.options_.useForcedSubtitles || false;
+ this.options_.useSegmentMetadataTrack = this.options_.useSegmentMetadataTrack !== false;
this.options_.useNetworkInformationApi = typeof this.options_.useNetworkInformationApi !== 'undefined' ?
this.options_.useNetworkInformationApi : true;
this.options_.useDtsForTimestampOffset = this.options_.useDtsForTimestampOffset || false;
@@ -753,6 +754,7 @@ class VhsHandler extends Component {
'liveRangeSafeTimeDelta',
'llhls',
'useForcedSubtitles',
+ 'useSegmentMetadataTrack',
'useNetworkInformationApi',
'useDtsForTimestampOffset',
'exactManifestTimings',
diff --git a/test/playlist-controller.test.js b/test/playlist-controller.test.js
index 77dfe527e..23f52efa3 100644
--- a/test/playlist-controller.test.js
+++ b/test/playlist-controller.test.js
@@ -7807,3 +7807,18 @@ QUnit.module('Native tracks', {
QUnit.test('Native text track mode is hidden', function(assert) {
assert.strictEqual(this.pc.segmentMetadataTrack_.mode, 'hidden', 'track mode is hidden');
});
+
+QUnit.test('does not create a native segment metadata track when disabled', function(assert) {
+ this.player.src({
+ src: 'manifest/main.m3u8',
+ type: 'application/vnd.apple.mpegurl',
+ useSegmentMetadataTrack: false
+ });
+ this.clock.tick(1);
+
+ const controller = this.player.tech_.vhs.playlistController_;
+ const tracks = this.player.tech_.el().querySelectorAll('track[label="segment-metadata"]');
+
+ assert.strictEqual(controller.segmentMetadataTrack_, undefined, 'controller has no segment metadata track');
+ assert.strictEqual(tracks.length, 0, 'no native segment metadata track element is attached');
+});
diff --git a/test/segment-loader.test.js b/test/segment-loader.test.js
index 24fd672c7..a3fd1009f 100644
--- a/test/segment-loader.test.js
+++ b/test/segment-loader.test.js
@@ -2561,6 +2561,30 @@ QUnit.module('SegmentLoader', function(hooks) {
});
});
+ QUnit.test('appends and removes segments without a segment metadata track', function(assert) {
+ loader.segmentMetadataTrack_ = undefined;
+
+ return this.setupMediaSource(loader.mediaSource_, loader.sourceUpdater_).then(() => {
+ loader.playlist(playlistWithDuration(50));
+ loader.load();
+ this.clock.tick(1);
+
+ const appended = new Promise((resolve, reject) => {
+ loader.one('appended', resolve);
+ loader.one('error', reject);
+ });
+
+ standardXHRResponse(this.requests.shift(), muxedSegment());
+ return appended;
+ }).then(() => {
+ assert.strictEqual(loader.mediaIndex, 0, 'first segment appended without a metadata track');
+ assert.notOk(loader.error(), 'segment processing did not fail');
+ loader.remove(0, 1);
+ this.clock.tick(1);
+ assert.strictEqual(loader.segmentMetadataTrack_, undefined, 'buffer removal does not create a metadata track');
+ });
+ });
+
QUnit.test('does not add cue for invalid segment timing info', function(assert) {
const addCueSpy = sinon.spy();
diff --git a/test/videojs-http-streaming.test.js b/test/videojs-http-streaming.test.js
index 65d860b09..2f75f5026 100644
--- a/test/videojs-http-streaming.test.js
+++ b/test/videojs-http-streaming.test.js
@@ -4688,6 +4688,62 @@ QUnit.test(
}
);
+[false, true].forEach((nativeTextTracks) => {
+ QUnit.test(`useSegmentMetadataTrack supports initialization and source options (nativeTextTracks: ${nativeTextTracks})`, function(assert) {
+ this.player.dispose();
+ this.player = createPlayer({html5: {
+ nativeTextTracks,
+ vhs: {useSegmentMetadataTrack: false}
+ }});
+
+ const setSource = (options = {}) => {
+ this.player.src(merge({
+ src: 'manifest/main.m3u8',
+ type: 'application/vnd.apple.mpegurl'
+ }, options));
+ this.clock.tick(1);
+ return this.player.tech_.vhs.playlistController_;
+ };
+ const segmentTracks = () => {
+ if (nativeTextTracks) {
+ return this.player.tech_.el().querySelectorAll('track[label="segment-metadata"]');
+ }
+
+ return Array.from(this.player.textTracks()).filter((track) => track.label === 'segment-metadata');
+ };
+
+ let controller = setSource();
+
+ assert.strictEqual(controller.segmentMetadataTrack_, undefined, 'initialization option prevents track creation');
+ assert.strictEqual(controller.mainSegmentLoader_.segmentMetadataTrack_, undefined, 'loader receives no track');
+ assert.strictEqual(segmentTracks().length, 0, 'no segment metadata track is exposed');
+
+ controller = setSource({useSegmentMetadataTrack: true});
+
+ assert.strictEqual(controller.segmentMetadataTrack_.mode, 'hidden', 'source option overrides initialization option');
+ assert.strictEqual(segmentTracks().length, 1, 'source change creates one segment metadata track');
+
+ controller = setSource({useSegmentMetadataTrack: false});
+
+ assert.strictEqual(controller.segmentMetadataTrack_, undefined, 'source option disables track again');
+ assert.strictEqual(segmentTracks().length, 0, 'previous source track is removed');
+
+ const subtitle = this.player.addRemoteTextTrack({kind: 'subtitles', label: 'English'}, false).track;
+
+ assert.strictEqual(subtitle.kind, 'subtitles', 'other text tracks remain available');
+ });
+});
+
+QUnit.test('useSegmentMetadataTrack is enabled by default', function(assert) {
+ this.player.src({src: 'manifest/main.m3u8', type: 'application/vnd.apple.mpegurl'});
+ this.clock.tick(1);
+
+ const vhs = this.player.tech_.vhs;
+
+ assert.strictEqual(vhs.options_.useSegmentMetadataTrack, true, 'default option is enabled');
+ assert.strictEqual(vhs.playlistController_.segmentMetadataTrack_.mode, 'hidden', 'default track remains hidden');
+});
+
QUnit.test('populates quality levels list when available', function(assert) {
this.player.src({
src: 'manifest/main.m3u8',