diff --git a/externs/shaka/ads.js b/externs/shaka/ads.js
index 67e8a6dd91..40de84311e 100644
--- a/externs/shaka/ads.js
+++ b/externs/shaka/ads.js
@@ -33,6 +33,24 @@
shaka.extern.AdsStats;
+/**
+ * @typedef {{
+ * start: number,
+ * end: ?number
+ * }}
+ *
+ * @description
+ * Contains the times of a range of an Ad.
+ *
+ * @property {number} start
+ * The start time of the range, in milliseconds.
+ * @property {number} end
+ * The end time of the range, in milliseconds.
+ * @exportDoc
+ */
+shaka.extern.AdCuePoint;
+
+
/**
* An object that's responsible for all the ad-related logic
* in the player.
@@ -77,6 +95,11 @@ shaka.extern.IAdManager = class extends EventTarget {
*/
replaceServerSideAdTagParameters(adTagParameters) {}
+ /**
+ * @return {!Array.}
+ */
+ getServerSideCuePoints() {}
+
/**
* Get statistics for the current playback session. If the player is not
* playing content, this will return an empty stats object.
diff --git a/lib/ads/ad_manager.js b/lib/ads/ad_manager.js
index 65ca694ac3..5fba7813b3 100644
--- a/lib/ads/ad_manager.js
+++ b/lib/ads/ad_manager.js
@@ -6,7 +6,6 @@
goog.provide('shaka.ads.AdManager');
-goog.provide('shaka.ads.CuePoint');
goog.require('shaka.Player');
goog.require('shaka.ads.AdsStats');
@@ -568,6 +567,22 @@ shaka.ads.AdManager = class extends shaka.util.FakeEventTarget {
}
+ /**
+ * @return {!Array.}
+ * @override
+ * @export
+ */
+ getServerSideCuePoints() {
+ if (!this.ssAdManager_) {
+ throw new shaka.util.Error(
+ shaka.util.Error.Severity.RECOVERABLE,
+ shaka.util.Error.Category.ADS,
+ shaka.util.Error.Code.SS_AD_MANAGER_NOT_INITIALIZED);
+ }
+ return this.ssAdManager_.getCuePoints();
+ }
+
+
/**
* @return {shaka.extern.AdsStats}
* @override
@@ -620,20 +635,6 @@ shaka.ads.AdManager = class extends shaka.util.FakeEventTarget {
}
};
-
-shaka.ads.CuePoint = class {
- /**
- * @param {number} start
- * @param {?number=} end
- */
- constructor(start, end = null) {
- /** @public {number} */
- this.start = start;
- /** @public {?number} */
- this.end = end;
- }
-};
-
/**
* The event name for when a sequence of ads has been loaded.
*
diff --git a/lib/ads/client_side_ad_manager.js b/lib/ads/client_side_ad_manager.js
index c9e28b864a..41421f330e 100644
--- a/lib/ads/client_side_ad_manager.js
+++ b/lib/ads/client_side_ad_manager.js
@@ -145,10 +145,14 @@ shaka.ads.ClientSideAdManager = class {
const cuePointStarts = this.imaAdsManager_.getCuePoints();
if (cuePointStarts.length) {
- /** @type {!Array.} */
+ /** @type {!Array.} */
const cuePoints = [];
for (const start of cuePointStarts) {
- const shakaCuePoint = new shaka.ads.CuePoint(start);
+ /** @type {shaka.extern.AdCuePoint} */
+ const shakaCuePoint = {
+ start: start,
+ end: null,
+ };
cuePoints.push(shakaCuePoint);
}
diff --git a/lib/ads/server_side_ad_manager.js b/lib/ads/server_side_ad_manager.js
index 191a0e5ea8..c1a6d33abe 100644
--- a/lib/ads/server_side_ad_manager.js
+++ b/lib/ads/server_side_ad_manager.js
@@ -62,6 +62,9 @@ shaka.ads.ServerSideAdManager = class {
/** @private {string} */
this.backupUrl_ = '';
+ /** @private {!Array.} */
+ this.currentCuePoints_ = [];
+
/** @private {shaka.util.EventManager} */
this.eventManager_ = new shaka.util.EventManager();
@@ -212,6 +215,7 @@ shaka.ads.ServerSideAdManager = class {
// this.streamManager_.reset();
this.backupUrl_ = '';
this.snapForwardTime_ = null;
+ this.currentCuePoints_ = [];
}
/**
@@ -241,6 +245,13 @@ shaka.ads.ServerSideAdManager = class {
}
}
+ /**
+ * @return {!Array.}
+ */
+ getCuePoints() {
+ return this.currentCuePoints_;
+ }
+
/**
* If a seek jumped over the ad break, return to the start of the
* ad break, then complete the seek after the ad played through.
@@ -369,13 +380,19 @@ shaka.ads.ServerSideAdManager = class {
onCuePointsChanged_(e) {
const streamData = e.getStreamData();
- /** @type {!Array.} */
+ /** @type {!Array.} */
const cuePoints = [];
for (const point of streamData.cuepoints) {
- const shakaCuePoint = new shaka.ads.CuePoint(point.start, point.end);
+ /** @type {shaka.extern.AdCuePoint} */
+ const shakaCuePoint = {
+ start: point.start,
+ end: point.end,
+ };
cuePoints.push(shakaCuePoint);
}
+ this.currentCuePoints_ = cuePoints;
+
this.onEvent_(
new shaka.util.FakeEvent(shaka.ads.AdManager.CUEPOINTS_CHANGED,
{'cuepoints': cuePoints}));
diff --git a/lib/player.js b/lib/player.js
index bf307ad1f9..21dc227813 100644
--- a/lib/player.js
+++ b/lib/player.js
@@ -4452,12 +4452,20 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
mimeType = await this.getTextMimetype_(uri);
}
+ let adCuePoints = [];
+ if (this.adManager_) {
+ try {
+ adCuePoints = this.adManager_.getServerSideCuePoints();
+ } catch (error) {}
+ }
+
if (this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS) {
if (forced) {
// See: https://github.com/whatwg/html/issues/4472
kind = 'forced';
}
- await this.addSrcTrackElement_(uri, language, kind, mimeType, label);
+ await this.addSrcTrackElement_(uri, language, kind, mimeType, label || '',
+ adCuePoints);
const textTracks = this.getTextTracks();
const srcTrack = textTracks.find((t) => {
return t.language == language &&
@@ -4487,6 +4495,18 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
shaka.util.Error.Code.CANNOT_ADD_EXTERNAL_TEXT_TO_LIVE_STREAM);
}
+ if (adCuePoints.length) {
+ goog.asserts.assert(
+ this.networkingEngine_, 'Need networking engine.');
+ const data = await this.getTextData_(uri,
+ this.networkingEngine_,
+ this.config_.streaming.retryParameters);
+ const vvtText = this.convertToWebVTT_(data, mimeType, adCuePoints);
+ const blob = new Blob([vvtText], {type: 'text/vtt'});
+ uri = shaka.media.MediaSourceEngine.createObjectURL(blob);
+ mimeType = 'text/vtt';
+ }
+
/** @type {shaka.extern.Stream} */
const stream = {
id: this.nextExternalStreamId_++,
@@ -4559,8 +4579,14 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
if (!mimeType) {
mimeType = await this.getTextMimetype_(uri);
}
+ let adCuePoints = [];
+ if (this.adManager_) {
+ try {
+ adCuePoints = this.adManager_.getServerSideCuePoints();
+ } catch (error) {}
+ }
await this.addSrcTrackElement_(uri, language, /* kind= */ 'chapters',
- mimeType);
+ mimeType, /* label= */ '', adCuePoints);
const chaptersTracks = this.getChaptersTracks();
const chaptersTrack = chaptersTracks.find((t) => {
return t.language == language;
@@ -4623,17 +4649,19 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
* @param {string} language
* @param {string} kind
* @param {string} mimeType
- * @param {string=} label
+ * @param {string} label
+ * @param {!Array.} adCuePoints
* @private
*/
- async addSrcTrackElement_(uri, language, kind, mimeType, label) {
- if (mimeType != 'text/vtt') {
+ async addSrcTrackElement_(uri, language, kind, mimeType, label,
+ adCuePoints) {
+ if (mimeType != 'text/vtt' || adCuePoints.length) {
goog.asserts.assert(
this.networkingEngine_, 'Need networking engine.');
const data = await this.getTextData_(uri,
this.networkingEngine_,
this.config_.streaming.retryParameters);
- const vvtText = this.convertToWebVTT_(data, mimeType);
+ const vvtText = this.convertToWebVTT_(data, mimeType, adCuePoints);
const blob = new Blob([vvtText], {type: 'text/vtt'});
uri = shaka.media.MediaSourceEngine.createObjectURL(blob);
mimeType = 'text/vtt';
@@ -4641,7 +4669,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
const trackElement =
/** @type {!HTMLTrackElement} */(document.createElement('track'));
trackElement.src = uri;
- trackElement.label = label || '';
+ trackElement.label = label;
trackElement.kind = kind;
trackElement.srclang = language;
// Because we're pulling in the text track file via Javascript, the
@@ -4680,10 +4708,11 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
*
* @param {BufferSource} buffer
* @param {string} mimeType
+ * @param {!Array.} adCuePoints
* @return {string}
* @private
*/
- convertToWebVTT_(buffer, mimeType) {
+ convertToWebVTT_(buffer, mimeType, adCuePoints) {
const factory = shaka.text.TextEngine.findParser(mimeType);
if (factory) {
const obj = factory();
@@ -4694,7 +4723,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
};
const data = shaka.util.BufferUtils.toUint8(buffer);
const cues = obj.parseMedia(data, time);
- return shaka.text.WebVttGenerator.convert(cues);
+ return shaka.text.WebVttGenerator.convert(cues, adCuePoints);
}
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
diff --git a/lib/text/web_vtt_generator.js b/lib/text/web_vtt_generator.js
index 800dae44b5..634541349d 100644
--- a/lib/text/web_vtt_generator.js
+++ b/lib/text/web_vtt_generator.js
@@ -17,9 +17,10 @@ goog.require('shaka.text.Cue');
shaka.text.WebVttGenerator = class {
/**
* @param {!Array.} cues
+ * @param {!Array.} adCuePoints
* @return {string}
*/
- static convert(cues) {
+ static convert(cues, adCuePoints) {
// Flatten nested cue payloads recursively. If a cue has nested cues,
// their contents should be combined and replace the payload of the parent.
const flattenPayload = (cue) => {
@@ -64,6 +65,25 @@ shaka.text.WebVttGenerator = class {
}
};
+ const webvttTimeString = (time) => {
+ let newTime = time;
+ for (const adCuePoint of adCuePoints) {
+ if (adCuePoint.end && adCuePoint.start < time) {
+ const offset = adCuePoint.end - adCuePoint.start;
+ newTime += offset;
+ }
+ }
+ const hours = Math.floor(newTime / 3600);
+ const minutes = Math.floor(newTime / 60 % 60);
+ const seconds = Math.floor(newTime % 60);
+ const milliseconds = Math.floor(newTime * 1000 % 1000);
+ return (hours < 10 ? '0' : '') + hours + ':' +
+ (minutes < 10 ? '0' : '') + minutes + ':' +
+ (seconds < 10 ? '0' : '') + seconds + '.' +
+ (milliseconds < 100 ? (milliseconds < 10 ? '00' : '0') : '') +
+ milliseconds;
+ };
+
// We don't want to modify the array or objects passed in, since we don't
// technically own them. So we build a new array and replace certain items
// in it if they need to be flattened.
@@ -80,17 +100,6 @@ shaka.text.WebVttGenerator = class {
let webvttString = 'WEBVTT\n\n';
for (const cue of flattenedCues) {
- const webvttTimeString = (time) => {
- const hours = Math.floor(time / 3600);
- const minutes = Math.floor(time / 60 % 60);
- const seconds = Math.floor(time % 60);
- const milliseconds = Math.floor(time * 1000 % 1000);
- return (hours < 10 ? '0' : '') + hours + ':' +
- (minutes < 10 ? '0' : '') + minutes + ':' +
- (seconds < 10 ? '0' : '') + seconds + '.' +
- (milliseconds < 100 ? (milliseconds < 10 ? '00' : '0') : '') +
- milliseconds;
- };
const webvttSettings = (cue) => {
const settings = [];
const Cue = shaka.text.Cue;
diff --git a/test/test/util/fake_ad_manager.js b/test/test/util/fake_ad_manager.js
index 7cf9f0aa97..5cf5367810 100644
--- a/test/test/util/fake_ad_manager.js
+++ b/test/test/util/fake_ad_manager.js
@@ -50,6 +50,13 @@ shaka.test.FakeAdManager = class extends shaka.util.FakeEventTarget {
/** @override */
replaceServerSideAdTagParameters(adTagParameters) {}
+ /**
+ * @override
+ */
+ getServerSideCuePoints() {
+ return [];
+ }
+
/** @override */
getStats() {
return this.stats_;
diff --git a/test/text/web_vtt_generator_unit.js b/test/text/web_vtt_generator_unit.js
index 184b72455d..1e2a7f7f29 100644
--- a/test/text/web_vtt_generator_unit.js
+++ b/test/text/web_vtt_generator_unit.js
@@ -9,7 +9,7 @@ goog.require('shaka.text.WebVttGenerator');
describe('WebVttGenerator', () => {
it('supports no cues', () => {
- verifyHelper([], 'WEBVTT\n\n');
+ verifyHelper([], [], 'WEBVTT\n\n');
});
it('convert cues to WebVTT', () => {
@@ -26,6 +26,8 @@ describe('WebVttGenerator', () => {
const shakaCue5 = new shaka.text.Cue(53, 54, 'Test5');
shakaCue5.textAlign = shaka.text.Cue.textAlign.END;
+ const adCuePoints = [];
+
verifyHelper(
[
shakaCue1,
@@ -34,6 +36,7 @@ describe('WebVttGenerator', () => {
shakaCue4,
shakaCue5,
],
+ adCuePoints,
'WEBVTT\n\n' +
'00:00:20.000 --> 00:00:40.000 align:left vertical:lr\n' +
'Test\n\n' +
@@ -68,19 +71,71 @@ describe('WebVttGenerator', () => {
nestedCue4.textDecoration.push(shaka.text.Cue.textDecoration.UNDERLINE);
shakaCue.nestedCues = [nestedCue1, nestedCue2, nestedCue3, nestedCue4];
+
+ const adCuePoints = [];
+
verifyHelper(
[shakaCue],
+ adCuePoints,
'WEBVTT\n\n' +
'00:00:10.000 --> 00:00:20.000 align:middle\n' +
'Test1Test2Test3Test4\n\n');
});
+ it('computes the time with ad cue points', () => {
+ const shakaCue1 = new shaka.text.Cue(20, 30, 'Test');
+ shakaCue1.textAlign = shaka.text.Cue.textAlign.LEFT;
+ shakaCue1.writingMode = shaka.text.Cue.writingMode.VERTICAL_LEFT_TO_RIGHT;
+ const shakaCue2 = new shaka.text.Cue(40, 50, 'Test2');
+ shakaCue2.textAlign = shaka.text.Cue.textAlign.RIGHT;
+ shakaCue2.writingMode = shaka.text.Cue.writingMode.VERTICAL_RIGHT_TO_LEFT;
+ const shakaCue3 = new shaka.text.Cue(50, 51, 'Test3');
+ shakaCue3.textAlign = shaka.text.Cue.textAlign.CENTER;
+ const shakaCue4 = new shaka.text.Cue(52, 53, 'Test4');
+ shakaCue4.textAlign = shaka.text.Cue.textAlign.START;
+ const shakaCue5 = new shaka.text.Cue(53, 54, 'Test5');
+ shakaCue5.textAlign = shaka.text.Cue.textAlign.END;
+
+ const adCuePoints = [
+ {
+ start: 0,
+ end: 10,
+ },
+ {
+ start: 35,
+ end: 45,
+ },
+ ];
+
+ verifyHelper(
+ [
+ shakaCue1,
+ shakaCue2,
+ shakaCue3,
+ shakaCue4,
+ shakaCue5,
+ ],
+ adCuePoints,
+ 'WEBVTT\n\n' +
+ '00:00:30.000 --> 00:00:40.000 align:left vertical:lr\n' +
+ 'Test\n\n' +
+ '00:01:00.000 --> 00:01:10.000 align:right vertical:rl\n' +
+ 'Test2\n\n' +
+ '00:01:10.000 --> 00:01:11.000 align:middle\n' +
+ 'Test3\n\n' +
+ '00:01:12.000 --> 00:01:13.000 align:start\n' +
+ 'Test4\n\n' +
+ '00:01:13.000 --> 00:01:14.000 align:end\n' +
+ 'Test5\n\n');
+ });
+
/**
* @param {!Array} cues
+ * @param {!Array} adCuePoints
* @param {string} text
*/
- function verifyHelper(cues, text) {
- const result = shaka.text.WebVttGenerator.convert(cues);
+ function verifyHelper(cues, adCuePoints, text) {
+ const result = shaka.text.WebVttGenerator.convert(cues, adCuePoints);
expect(text).toBe(result);
}
});
diff --git a/ui/seek_bar.js b/ui/seek_bar.js
index b30662b729..c59a8e5499 100644
--- a/ui/seek_bar.js
+++ b/ui/seek_bar.js
@@ -15,7 +15,6 @@ goog.require('shaka.ui.RangeElement');
goog.require('shaka.ui.Utils');
goog.require('shaka.util.Dom');
goog.require('shaka.util.Timer');
-goog.requireType('shaka.ads.CuePoint');
goog.requireType('shaka.ui.Controls');
@@ -83,7 +82,7 @@ shaka.ui.SeekBar = class extends shaka.ui.RangeElement {
*/
this.wasPlaying_ = false;
- /** @private {!Array.} */
+ /** @private {!Array.} */
this.adCuePoints_ = [];
this.eventManager.listen(this.localization,