diff --git a/docs/supported-features.md b/docs/supported-features.md
index 887c333f5..cb96207a5 100644
--- a/docs/supported-features.md
+++ b/docs/supported-features.md
@@ -64,6 +64,7 @@ not meant serve as an exhaustive list.
* In-manifest [WebVTT] subtitles are automatically translated into standard HTML5 subtitle
tracks
* [AES-128] segment encryption
+* DASH In-manifest EventStream and Event tags are automatically translated into HTML5 metadata cues
## Notable Missing Features
diff --git a/src/dash-playlist-loader.js b/src/dash-playlist-loader.js
index d5f761c16..bb6523cc7 100644
--- a/src/dash-playlist-loader.js
+++ b/src/dash-playlist-loader.js
@@ -312,6 +312,7 @@ export default class DashPlaylistLoader extends EventTarget {
this.vhs_ = vhs;
this.withCredentials = withCredentials;
+ this.addMetadataToTextTrack = options.addMetadataToTextTrack;
if (!srcUrlOrPlaylist) {
throw new Error('A non-empty playlist URL or object is required');
@@ -773,6 +774,8 @@ export default class DashPlaylistLoader extends EventTarget {
this.updateMinimumUpdatePeriodTimeout_();
}
+ this.addEventStreamToMetadataTrack_(newMain);
+
return Boolean(newMain);
}
@@ -901,4 +904,24 @@ export default class DashPlaylistLoader extends EventTarget {
this.trigger('loadedplaylist');
}
+
+ /**
+ * Takes eventstream data from a parsed DASH manifest and adds it to the metadata text track.
+ *
+ * @param {manifest} newMain the newly parsed manifest
+ */
+ addEventStreamToMetadataTrack_(newMain) {
+ // Only add new event stream metadata if we have a new manifest.
+ if (newMain && this.mainPlaylistLoader_.main.eventStream) {
+ // convert EventStream to ID3-like data.
+ const metadataArray = this.mainPlaylistLoader_.main.eventStream.map((eventStreamNode) => {
+ return {
+ cueTime: eventStreamNode.start,
+ frames: [{ data: eventStreamNode.messageData }]
+ };
+ });
+
+ this.addMetadataToTextTrack('EventStream', metadataArray, this.mainPlaylistLoader_.main.duration);
+ }
+ }
}
diff --git a/src/playlist-controller.js b/src/playlist-controller.js
index 79f0da771..eea65ea0b 100644
--- a/src/playlist-controller.js
+++ b/src/playlist-controller.js
@@ -26,6 +26,7 @@ import { codecsForPlaylist, unwrapCodecList, codecCount } from './util/codecs.js
import { createMediaTypes, setupMediaGroups } from './media-groups';
import logger from './util/logger';
import {merge, createTimeRanges} from './util/vjs-compat';
+import { addMetadata, createMetadataTrackIfNotExists } from './util/text-tracks';
const ABORT_EARLY_EXCLUSION_SECONDS = 60 * 2;
@@ -254,7 +255,8 @@ export class PlaylistController extends videojs.EventTarget {
cacheEncryptionKeys,
sourceUpdater: this.sourceUpdater_,
timelineChangeController: this.timelineChangeController_,
- exactManifestTimings: options.exactManifestTimings
+ exactManifestTimings: options.exactManifestTimings,
+ addMetadataToTextTrack: this.addMetadataToTextTrack.bind(this)
};
// The source type check not only determines whether a special DASH playlist loader
@@ -262,7 +264,7 @@ export class PlaylistController extends videojs.EventTarget {
// manifest object (instead of a URL). In the case of vhs-json, the default
// PlaylistLoader should be used.
this.mainPlaylistLoader_ = this.sourceType_ === 'dash' ?
- new DashPlaylistLoader(src, this.vhs_, this.requestOptions_) :
+ new DashPlaylistLoader(src, this.vhs_, merge(this.requestOptions_, { addMetadataToTextTrack: this.addMetadataToTextTrack.bind(this) })) :
new PlaylistLoader(src, this.vhs_, this.requestOptions_);
this.setupMainPlaylistLoaderListeners_();
@@ -2009,4 +2011,19 @@ export class PlaylistController extends videojs.EventTarget {
return Config.BUFFER_HIGH_WATER_LINE;
}
+ addMetadataToTextTrack(dispatchType, metadataArray, videoDuration) {
+ const timestampOffset = this.sourceUpdater_.videoTimestampOffset() === null ?
+ this.sourceUpdater_.audioTimestampOffset() : this.sourceUpdater_.videoTimestampOffset();
+
+ // There's potentially an issue where we could double add metadata if there's a muxed
+ // audio/video source with a metadata track, and an alt audio with a metadata track.
+ // However, this probably won't happen, and if it does it can be handled then.
+ createMetadataTrackIfNotExists(this.inbandTextTracks_, dispatchType, this.tech_);
+ addMetadata({
+ inbandTextTracks: this.inbandTextTracks_,
+ metadataArray,
+ timestampOffset,
+ videoDuration
+ });
+ }
}
diff --git a/src/segment-loader.js b/src/segment-loader.js
index 5428a8347..8015eac76 100644
--- a/src/segment-loader.js
+++ b/src/segment-loader.js
@@ -14,8 +14,6 @@ import logger from './util/logger';
import { concatSegments } from './util/segment';
import {
createCaptionsTrackIfNotExists,
- createMetadataTrackIfNotExists,
- addMetadata,
addCaptionData,
removeCuesFromTrack
} from './util/text-tracks';
@@ -563,6 +561,7 @@ export default class SegmentLoader extends videojs.EventTarget {
this.useDtsForTimestampOffset_ = settings.useDtsForTimestampOffset;
this.captionServices_ = settings.captionServices;
this.exactManifestTimings = settings.exactManifestTimings;
+ this.addMetadataToTextTrack = settings.addMetadataToTextTrack;
// private instance variables
this.checkBufferTimeout_ = null;
@@ -1872,21 +1871,7 @@ export default class SegmentLoader extends videojs.EventTarget {
this.metadataQueue_.id3.push(this.handleId3_.bind(this, simpleSegment, id3Frames, dispatchType));
return;
}
-
- const timestampOffset = this.sourceUpdater_.videoTimestampOffset() === null ?
- this.sourceUpdater_.audioTimestampOffset() :
- this.sourceUpdater_.videoTimestampOffset();
-
- // There's potentially an issue where we could double add metadata if there's a muxed
- // audio/video source with a metadata track, and an alt audio with a metadata track.
- // However, this probably won't happen, and if it does it can be handled then.
- createMetadataTrackIfNotExists(this.inbandTextTracks_, dispatchType, this.vhs_.tech_);
- addMetadata({
- inbandTextTracks: this.inbandTextTracks_,
- metadataArray: id3Frames,
- timestampOffset,
- videoDuration: this.duration_()
- });
+ this.addMetadataToTextTrack(dispatchType, id3Frames, this.duration_());
}
processMetadataQueue_() {
diff --git a/test/loader-common.js b/test/loader-common.js
index 0016b114d..c59540d9e 100644
--- a/test/loader-common.js
+++ b/test/loader-common.js
@@ -52,7 +52,9 @@ export const LoaderCommonHooks = {
playbackRate: () => this.playbackRate,
currentTime: () => this.currentTime,
textTracks: () => {},
- addRemoteTextTrack: () => {},
+ addRemoteTextTrack: (track) => {
+ return track;
+ },
trigger: () => {}
}
};
@@ -60,10 +62,16 @@ export const LoaderCommonHooks = {
this.goalBufferLength =
PlaylistController.prototype.goalBufferLength.bind(this);
this.mediaSource = new window.MediaSource();
- this.sourceUpdater = new SourceUpdater(this.mediaSource);
+ this.sourceUpdater_ = new SourceUpdater(this.mediaSource);
+ this.inbandTextTracks_ = {
+ metadataTrack_: {
+ addCue: () => {}
+ }
+ };
this.syncController = new SyncController();
this.decrypter = new Decrypter();
this.timelineChangeController = new TimelineChangeController();
+ this.addMetadataToTextTrack = PlaylistController.prototype.addMetadataToTextTrack.bind(this);
this.video = document.createElement('video');
@@ -80,7 +88,7 @@ export const LoaderCommonHooks = {
this.env.restore();
this.decrypter.terminate();
- this.sourceUpdater.dispose();
+ this.sourceUpdater_.dispose();
this.timelineChangeController.dispose();
}
};
@@ -105,10 +113,11 @@ export const LoaderCommonSettings = function(settings) {
duration: () => this.mediaSource.duration,
goalBufferLength: () => this.goalBufferLength(),
mediaSource: this.mediaSource,
- sourceUpdater: this.sourceUpdater,
+ sourceUpdater: this.sourceUpdater_,
syncController: this.syncController,
decrypter: this.decrypter,
- timelineChangeController: this.timelineChangeController
+ timelineChangeController: this.timelineChangeController,
+ addMetadataToTextTrack: this.addMetadataToTextTrack
}, settings);
};
diff --git a/test/manifests/eventStream.mpd b/test/manifests/eventStream.mpd
new file mode 100644
index 000000000..974068d19
--- /dev/null
+++ b/test/manifests/eventStream.mpd
@@ -0,0 +1,39 @@
+
+
+ https://547f72e6652371c3.mediapackage.us-east-1.amazonaws.com/out/v1/ef88d1a64fd543f4a4be9a56c913d868/
+ https://dai.google.com/linear/dash/pa/event/PSzZMzAkSXCmlJOWDmRj8Q/stream/7e6b742f-0013-412c-b8a9-fd527adafc89:ATL/manifest.mpd
+
+ https://dai.google.com/linear/pods/v1/p/PSzZMzAkSXCmlJOWDmRj8Q/7e6b742f-0013-412c-b8a9-fd527adafc89:ATL/1065804/0/0/
+
+
+ foo
+ bar
+ foo_bar
+ bar_foo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/manifests/eventStreamMessageData.mpd b/test/manifests/eventStreamMessageData.mpd
new file mode 100644
index 000000000..43eee0101
--- /dev/null
+++ b/test/manifests/eventStreamMessageData.mpd
@@ -0,0 +1,49 @@
+
+
+ https://547f72e6652371c3.mediapackage.us-east-1.amazonaws.com/out/v1/ef88d1a64fd543f4a4be9a56c913d868/
+ https://dai.google.com/linear/dash/pa/event/PSzZMzAkSXCmlJOWDmRj8Q/stream/7e6b742f-0013-412c-b8a9-fd527adafc89:ATL/manifest.mpd
+
+ https://dai.google.com/linear/pods/v1/p/PSzZMzAkSXCmlJOWDmRj8Q/7e6b742f-0013-412c-b8a9-fd527adafc89:ATL/1065804/0/0/
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/playlist-controller.test.js b/test/playlist-controller.test.js
index b70a0828d..0241a555c 100644
--- a/test/playlist-controller.test.js
+++ b/test/playlist-controller.test.js
@@ -340,6 +340,123 @@ QUnit.test('passes options to PlaylistLoader', function(assert) {
controller.dispose();
});
+QUnit.test('addMetadataToTextTrack adds expected metadata to the metadataTrack', function(assert) {
+ const options = {
+ src: 'test.mpd',
+ tech: this.player.tech_,
+ sourceType: 'dash'
+ };
+
+ // Test messageData property manifest
+ let expectedCueValues = [
+ {
+ startTime: 63857834.256000005,
+ data: 'google_7617584398642699833'
+ },
+ {
+ startTime: 63857835.056,
+ data: 'google_gkmxVFMIdHz413g3pIgZtITUSFFQYDnQ421MGEkVnTA'
+ },
+ {
+ startTime: 63857836.056,
+ data: 'google_Yl7LFi1Fh-TD39nqQzIiGLDD1lx7tYRjjmYND7tEEjM'
+ },
+ {
+ startTime: 63857836.650000006,
+ data: 'google_5437877779805246002'
+ },
+ {
+ startTime: 63857837.056,
+ data: 'google_8X2eBAFbC2cUJmNNHkrcDKqSJQncj2nrVoB2eIu6lrc'
+ },
+ {
+ startTime: 63857838.056,
+ data: 'google_Qyxg2ZhKfBUls-J7oj0Re0_-gCQFviaaEMMDvIOTEWE'
+ },
+ {
+ startTime: 63857838.894,
+ data: 'google_7174574530630198647'
+ },
+ {
+ startTime: 63857839.056,
+ data: 'google_EFt2jovkcT9PqjuLLC5kH7gIIjWvc0iIhROFED6kqsg'
+ },
+ {
+ startTime: 63857840.056,
+ data: 'google_eUHx4vMmAikHojJZLOTR2XZdg1A9b9A8TY7F2CVC3cA'
+ },
+ {
+ startTime: 63857841.056,
+ data: 'google_gkmxVFMIdHz413g3pIgZtITUSFFQYDnQ421MGEkVnTA'
+ },
+ {
+ startTime: 63857841.638000004,
+ data: 'google_1443613685977331553'
+ },
+ {
+ startTime: 63857842.056,
+ data: 'google_Yl7LFi1Fh-TD39nqQzIiGLDD1lx7tYRjjmYND7tEEjM'
+ },
+ {
+ startTime: 63857843.056,
+ data: 'google_8X2eBAFbC2cUJmNNHkrcDKqSJQncj2nrVoB2eIu6lrc'
+ },
+ {
+ startTime: 63857843.13200001,
+ data: 'google_5822903356700578162'
+ }
+ ];
+
+ let controller = new PlaylistController(options);
+
+ controller.mainPlaylistLoader_.mainXml_ = manifests.eventStreamMessageData;
+ controller.mainPlaylistLoader_.handleMain_();
+ // Gather actual cues.
+ let actualCueValues = controller.inbandTextTracks_.metadataTrack_.cues_.map((cue) => {
+ return {
+ startTime: cue.startTime,
+ data: cue.value.data
+ };
+ });
+
+ assert.ok(controller.mainPlaylistLoader_.addMetadataToTextTrack, 'addMetadataToTextTrack is passed to the DASH mainPlaylistLoader');
+ assert.deepEqual(actualCueValues, expectedCueValues, 'expected cue values are added to the metadataTrack');
+ controller.dispose();
+
+ // Test content manifest
+ expectedCueValues = [
+ {
+ startTime: 63857834.256000005,
+ data: 'foo'
+ },
+ {
+ startTime: 63857835.056,
+ data: 'bar'
+ },
+ {
+ startTime: 63857836.056,
+ data: 'foo_bar'
+ },
+ {
+ startTime: 63857836.650000006,
+ data: 'bar_foo'
+ }
+ ];
+
+ controller = new PlaylistController(options);
+ controller.mainPlaylistLoader_.mainXml_ = manifests.eventStream;
+ controller.mainPlaylistLoader_.handleMain_();
+ actualCueValues = controller.inbandTextTracks_.metadataTrack_.cues_.map((cue) => {
+ return {
+ startTime: cue.startTime,
+ data: cue.value.data
+ };
+ });
+
+ assert.deepEqual(actualCueValues, expectedCueValues, 'expected cue values are added to the metadataTrack');
+ controller.dispose();
+});
+
QUnit.test('obeys metadata preload option', function(assert) {
this.player.preload('metadata');
// main
diff --git a/test/segment-loader.test.js b/test/segment-loader.test.js
index a3b6a4d39..816b4d06d 100644
--- a/test/segment-loader.test.js
+++ b/test/segment-loader.test.js
@@ -2421,6 +2421,9 @@ QUnit.module('SegmentLoader', function(hooks) {
}
});
+ this.sourceUpdater_ = loader.sourceUpdater_;
+ this.inbandTextTracks_ = loader.inbandTextTracks_;
+ this.tech_ = loader.vhs_.tech_;
standardXHRResponse(this.requests.shift(), muxedSegment());
});
@@ -2528,6 +2531,9 @@ QUnit.module('SegmentLoader', function(hooks) {
}
});
+ this.sourceUpdater_ = loader.sourceUpdater_;
+ this.inbandTextTracks_ = loader.inbandTextTracks_;
+ this.tech_ = loader.vhs_.tech_;
standardXHRResponse(this.requests.shift(), audioSegment());
});
});