diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml
index 470a25b6e0..74a94b57a8 100644
--- a/.github/workflows/build-and-test.yaml
+++ b/.github/workflows/build-and-test.yaml
@@ -41,9 +41,11 @@ jobs:
include:
# Run Linux browsers with xvfb, so they're in a headless X session.
# Additionally, generate a code coverage report from Linux Chrome.
+ # It should be the uncompiled build, or else we won't execute any
+ # coverage instrumentation on full-stack player integration tests.
- os: ubuntu-latest
browser: Chrome
- extra_flags: "--use-xvfb --html-coverage-report"
+ extra_flags: "--use-xvfb --html-coverage-report --uncompiled"
- os: ubuntu-latest
browser: Firefox
extra_flags: "--use-xvfb"
@@ -57,7 +59,7 @@ jobs:
- os: macos-latest
browser: Safari
- os: macos-latest
- browser: Safari-14
+ browser: Safari-old
- os: windows-latest
browser: Chrome
@@ -87,21 +89,22 @@ jobs:
with:
ref: ${{ github.event.inputs.ref || github.ref }}
- # Safari 14 can be installed, but not to the root, and it can't replace
- # the standard version, at least not on GitHub's VMs. If you try to
- # install directly to the root with sudo, it will appear to succeed, but
- # will have no effect. If you try to script it explicitly with rm -rf
- # and cp, this will fail. Safari may be on a read-only filesystem.
- - name: Install Safari 14 to home directory
- if: matrix.os == 'macos-latest' && matrix.browser == 'Safari-14'
+ # Older versions of Safari can be installed, but not to the root, and it
+ # can't replace the standard version, at least not on GitHub's VMs. If
+ # you try to install directly to the root with sudo, it will appear to
+ # succeed, but will have no effect. If you try to script it explicitly
+ # with rm -rf and cp, this will fail. Safari may be on a read-only
+ # filesystem.
+ - name: Install old Safari to home directory
+ if: matrix.os == 'macos-latest' && matrix.browser == 'Safari-old'
run: |
- # Download Safari 14
- # See also https://www.macupdate.com/app/mac/15675/apple-safari/old-versions
- curl -Lv https://www.macupdate.com/action/download/62946 > Safari14.0CatalinaAuto.pkg.zip
+ # Download Safari 15
+ # This URL discovered through the seed files listed at
+ # https://github.com/zhangyoufu/swscan.apple.com/blob/master/url.txt
+ curl -Lv http://swcdn.apple.com/content/downloads/42/33/012-57329-A_41P2VU6UHN/5fw5vna27fdw4mqfak5adj3pjpxvo9hgh7/Safari15.6.1CatalinaAuto.pkg > Safari.pkg
- # Install Safari 14 to homedir specifically.
- unzip Safari14.0CatalinaAuto.pkg.zip
- installer -pkg Safari14.0CatalinaAuto.pkg -target CurrentUserHomeDirectory
+ # Install older Safari to homedir specifically.
+ installer -pkg Safari.pkg -target CurrentUserHomeDirectory
# Install a launcher that can execute a shell script to launch this
npm install karma-script-launcher --save-dev
@@ -114,7 +117,7 @@ jobs:
run: |
browser=${{ matrix.browser }}
- if [[ "$browser" == "Safari-14" ]]; then
+ if [[ "$browser" == "Safari-old" ]]; then
# Replace the browser name with a script that can launch this
# browser from the command line.
browser="$PWD/.github/workflows/safari-homedir-launcher.sh"
@@ -165,6 +168,18 @@ jobs:
# an environment variable set, the file should definitely be there.
if-no-files-found: error
+ # Upload new screenshots and diffs on failure; ignore if missing
+ - name: Upload screenshots
+ uses: actions/upload-artifact@v3
+ if: ${{ failure() }}
+ with:
+ name: screenshots-${{ matrix.browser }}
+ path: |
+ test/test/assets/screenshots/*/*.png-new
+ test/test/assets/screenshots/*/*.png-diff
+ if-no-files-found: ignore
+ retention-days: 5
+
build_in_docker:
# Don't waste time doing a full matrix of test runs when there was an
# obvious linter error.
diff --git a/.github/workflows/selenium-lab-tests.yaml b/.github/workflows/selenium-lab-tests.yaml
index db8d157f53..6852b6813f 100644
--- a/.github/workflows/selenium-lab-tests.yaml
+++ b/.github/workflows/selenium-lab-tests.yaml
@@ -180,6 +180,15 @@ jobs:
# the container.
- name: Test Player
run: |
+ # Generate a coverage report from uncompiled code on ChromeLinux.
+ # It should be the uncompiled build, or else we won't execute any
+ # coverage instrumentation on full-stack player integration tests.
+ if [[ "${{ matrix.browser }}" == "ChromeLinux" ]]; then
+ extra_flags="--html-coverage-report --uncompiled"
+ else
+ extra_flags=""
+ fi
+
python3 build/test.py \
--no-build \
--reporters spec --spec-hide-passed \
@@ -189,7 +198,7 @@ jobs:
--grid-config build/shaka-lab.yaml \
--grid-address selenium-grid.lab:4444 \
--browsers ${{ matrix.browser }} \
- --html-coverage-report
+ $extra_flags
- name: Find coverage report (ChromeLinux only)
id: coverage
diff --git a/demo/common/message_ids.js b/demo/common/message_ids.js
index b39d74e68b..6f57cc69e9 100644
--- a/demo/common/message_ids.js
+++ b/demo/common/message_ids.js
@@ -226,6 +226,7 @@ shakaDemo.MessageIds = {
MAX_HEIGHT: 'DEMO_MAX_HEIGHT',
MAX_PIXELS: 'DEMO_MAX_PIXELS',
MAX_WIDTH: 'DEMO_MAX_WIDTH',
+ MEDIA_SOURCE_SECTION_HEADER: 'DEMO_MEDIA_SOURCE_SECTION_HEADER',
MIN_BANDWIDTH: 'DEMO_MIN_BANDWIDTH',
MIN_BYTES: 'DEMO_MIN_BYTES',
MIN_FRAMERATE: 'DEMO_MIN_FRAMERATE',
@@ -244,6 +245,7 @@ shakaDemo.MessageIds = {
PREFER_NATIVE_HLS: 'DEMO_PREFER_NATIVE_HLS',
REBUFFERING_GOAL: 'DEMO_REBUFFERING_GOAL',
RESTRICT_TO_ELEMENT_SIZE: 'DEMO_RESTRICT_TO_ELEMENT_SIZE',
+ RESTRICT_TO_SCREEN_SIZE: 'DEMO_RESTRICT_TO_SCREEN_SIZE',
RESTRICTIONS_SECTION_HEADER: 'DEMO_RESTRICTIONS_SECTION_HEADER',
SAFE_SEEK_OFFSET: 'DEMO_SAFE_SEEK_OFFSET',
SAFE_SKIP_DISTANCE: 'DEMO_SAFE_SKIP_DISTANCE',
@@ -251,6 +253,7 @@ shakaDemo.MessageIds = {
SESSION_ID: 'DEMO_SESSION_ID',
SHAKA_CONTROLS: 'DEMO_SHAKA_CONTROLS',
SLOW_HALF_LIFE: 'DEMO_SLOW_HALF_LIFE',
+ SOURCE_BUFFER_EXTRA_FEATURES: 'DEMO_SOURCE_BUFFER_EXTRA_FEATURES',
STALL_DETECTOR_ENABLED: 'DEMO_STALL_DETECTOR_ENABLED',
STALL_THRESHOLD: 'DEMO_STALL_THRESHOLD',
STALL_TIMEOUT: 'DEMO_STALL_TIMEOUT',
diff --git a/demo/config.js b/demo/config.js
index a0ca8b24be..d6e424346f 100644
--- a/demo/config.js
+++ b/demo/config.js
@@ -92,6 +92,7 @@ shakaDemo.Config = class {
this.addOfflineSection_();
this.addDrmSection_();
this.addStreamingSection_();
+ this.addMediaSourceSection_();
this.addManifestSection_();
this.addRetrictionsSection_('',
shakaDemo.MessageIds.RESTRICTIONS_SECTION_HEADER);
@@ -275,6 +276,8 @@ shakaDemo.Config = class {
/* canBeDecimal= */ true)
.addBoolInput_(MessageIds.RESTRICT_TO_ELEMENT_SIZE,
'abr.restrictToElementSize')
+ .addBoolInput_(MessageIds.RESTRICT_TO_SCREEN_SIZE,
+ 'abr.restrictToScreenSize')
.addBoolInput_(MessageIds.IGNORE_DEVICE_PIXEL_RATIO,
'abr.ignoreDevicePixelRatio');
this.addRetrictionsSection_('abr',
@@ -438,6 +441,15 @@ shakaDemo.Config = class {
MessageIds.STREAMING_RETRY_SECTION_HEADER);
}
+ /** @private */
+ addMediaSourceSection_() {
+ const MessageIds = shakaDemo.MessageIds;
+ const docLink = this.resolveExternLink_('.MediaSourceConfiguration');
+ this.addSection_(MessageIds.MEDIA_SOURCE_SECTION_HEADER, docLink)
+ .addTextInput_(MessageIds.SOURCE_BUFFER_EXTRA_FEATURES,
+ 'mediaSource.sourceBufferExtraFeatures');
+ }
+
/** @private */
addLanguageSection_() {
const MessageIds = shakaDemo.MessageIds;
diff --git a/demo/locales/en.json b/demo/locales/en.json
index 63178a01d7..321434d534 100644
--- a/demo/locales/en.json
+++ b/demo/locales/en.json
@@ -148,6 +148,7 @@
"DEMO_MAX_HEIGHT": "Max Height",
"DEMO_MAX_PIXELS": "Max Pixels",
"DEMO_MAX_WIDTH": "Max Width",
+ "DEMO_MEDIA_SOURCE_SECTION_HEADER": "Media source",
"DEMO_METACDN": "MetaCDN",
"DEMO_MICROSOFT": "Microsoft",
"DEMO_MIME_TYPE": "MIME Type",
@@ -188,6 +189,7 @@
"DEMO_REBUFFERING_GOAL": "Rebuffering Goal",
"DEMO_REPORT_BUG": "REPORT BUG",
"DEMO_RESTRICT_TO_ELEMENT_SIZE": "Restrict to element size",
+ "DEMO_RESTRICT_TO_SCREEN_SIZE": "Restrict to screen size",
"DEMO_RESTRICTIONS_SECTION_HEADER": "Restrictions",
"DEMO_SAFE_SEEK_OFFSET": "Safe Seek Offset",
"DEMO_SAFE_SKIP_DISTANCE": "Safe Skip Distance",
@@ -198,6 +200,7 @@
"DEMO_SHAKA_CONTROLS": "Shaka Controls",
"DEMO_SLOW_HALF_LIFE": "Slow half life",
"DEMO_SOURCE": "Source on GitHub",
+ "DEMO_SOURCE_BUFFER_EXTRA_FEATURES": "Source buffer extra features",
"DEMO_SOURCE_SEARCH": "Source",
"DEMO_STALL_DETECTOR_ENABLED": "Stall Detector Enabled",
"DEMO_STALL_THRESHOLD": "Stall Threshold",
diff --git a/demo/locales/source.json b/demo/locales/source.json
index e66939c4f6..62219ead8e 100644
--- a/demo/locales/source.json
+++ b/demo/locales/source.json
@@ -595,6 +595,10 @@
"description": "The name of a configuration value.",
"message": "Max Width"
},
+ "DEMO_MEDIA_SOURCE_SECTION_HEADER": {
+ "description": "The header for a section of configuration values.",
+ "message": "Media source"
+ },
"DEMO_METACDN": {
"description": "Text that describes an asset that comes from the MetaCDN asset library.",
"message": "[PROPER_NAME:MetaCDN]"
@@ -755,6 +759,10 @@
"description": "The name of a configuration value.",
"message": "Restrict to element size"
},
+ "DEMO_RESTRICT_TO_SCREEN_SIZE": {
+ "description": "The name of a configuration value.",
+ "message": "Restrict to screen size"
+ },
"DEMO_RESTRICTIONS_SECTION_HEADER": {
"description": "The header for a section of configuration values.",
"message": "Restrictions"
@@ -795,6 +803,10 @@
"description": "A link in the footer, to the Shaka Player source on GitHub.",
"message": "Source on [PROPER_NAME:GitHub]"
},
+ "DEMO_SOURCE_BUFFER_EXTRA_FEATURES": {
+ "description": "The name of a configuration value.",
+ "message": "Source buffer extra features"
+ },
"DEMO_SOURCE_SEARCH": {
"description": "A header on a search field that filters by the source of the asset.",
"message": "Source"
diff --git a/externs/ima.js b/externs/ima.js
index 24511e2308..456f5e7628 100644
--- a/externs/ima.js
+++ b/externs/ima.js
@@ -170,6 +170,9 @@ google.ima.Ad = class {
/** @return {string} */
getTitle() {}
+
+ /** @return {string} */
+ getDescription() {}
};
@@ -395,6 +398,9 @@ google.ima.dai.api.Ad = class {
/** @return {string} */
getTitle() {}
+
+ /** @return {string} */
+ getDescription() {}
};
diff --git a/externs/shaka/ads.js b/externs/shaka/ads.js
index 4259a3ed42..af83b7c046 100644
--- a/externs/shaka/ads.js
+++ b/externs/shaka/ads.js
@@ -232,4 +232,9 @@ shaka.extern.IAd = class {
* @return {string}
*/
getTitle() {}
+
+ /**
+ * @return {string}
+ */
+ getDescription() {}
};
diff --git a/externs/shaka/player.js b/externs/shaka/player.js
index 66c7ba083d..a8efdad7f1 100644
--- a/externs/shaka/player.js
+++ b/externs/shaka/player.js
@@ -1017,6 +1017,23 @@ shaka.extern.ManifestConfiguration;
shaka.extern.StreamingConfiguration;
+/**
+ * @typedef {{
+ * sourceBufferExtraFeatures: string
+ * }}
+ *
+ * @description
+ * Media source configuration.
+ *
+ * @property {string} sourceBufferExtraFeatures
+ * Some platforms may need to pass features when initializing the
+ * sourceBuffer.
+ * This string is ultimately appended to MIME types in addSourceBuffer().
+ * @exportDoc
+ */
+shaka.extern.MediaSourceConfiguration;
+
+
/**
* @typedef {{
* enabled: boolean,
@@ -1028,6 +1045,7 @@ shaka.extern.StreamingConfiguration;
* bandwidthDowngradeTarget: number,
* advanced: shaka.extern.AdvancedAbrConfiguration,
* restrictToElementSize: boolean,
+ * restrictToScreenSize: boolean,
* ignoreDevicePixelRatio: boolean
* }}
*
@@ -1063,9 +1081,12 @@ shaka.extern.StreamingConfiguration;
* Note: The use of ResizeObserver is required for it to work properly. If
* true without ResizeObserver, it behaves as false.
* Defaults false.
+ * @property {boolean} restrictToScreenSize
+ * If true, restrict the quality to screen size.
+ * Defaults false.
* @property {boolean} ignoreDevicePixelRatio
* If true,device pixel ratio is ignored when restricting the quality to
- * media element size.
+ * media element size or screen size.
* Defaults false.
* @exportDoc
*/
@@ -1220,6 +1241,7 @@ shaka.extern.OfflineConfiguration;
* drm: shaka.extern.DrmConfiguration,
* manifest: shaka.extern.ManifestConfiguration,
* streaming: shaka.extern.StreamingConfiguration,
+ * mediaSource: shaka.extern.MediaSourceConfiguration,
* abrFactory: shaka.extern.AbrManager.Factory,
* abr: shaka.extern.AbrConfiguration,
* cmcd: shaka.extern.CmcdConfiguration,
@@ -1248,6 +1270,8 @@ shaka.extern.OfflineConfiguration;
* Manifest configuration and settings.
* @property {shaka.extern.StreamingConfiguration} streaming
* Streaming configuration and settings.
+ * @property {shaka.extern.MediaSourceConfiguration} mediaSource
+ * Media source configuration and settings.
* @property {shaka.extern.AbrManager.Factory} abrFactory
* A factory to construct an abr manager.
* @property {shaka.extern.AbrConfiguration} abr
diff --git a/lib/abr/simple_abr_manager.js b/lib/abr/simple_abr_manager.js
index d799dbf06b..19bec41518 100644
--- a/lib/abr/simple_abr_manager.js
+++ b/lib/abr/simple_abr_manager.js
@@ -148,6 +148,13 @@ shaka.abr.SimpleAbrManager = class {
let maxHeight = Infinity;
let maxWidth = Infinity;
+ if (this.config_.restrictToScreenSize) {
+ const devicePixelRatio =
+ this.config_.ignoreDevicePixelRatio ? 1 : window.devicePixelRatio;
+ maxHeight = window.screen.height * devicePixelRatio;
+ maxWidth = window.screen.width * devicePixelRatio;
+ }
+
if (this.resizeObserver_ && this.config_.restrictToElementSize) {
const devicePixelRatio =
this.config_.ignoreDevicePixelRatio ? 1 : window.devicePixelRatio;
diff --git a/lib/ads/client_side_ad.js b/lib/ads/client_side_ad.js
index f768648dc2..2b54da55a5 100644
--- a/lib/ads/client_side_ad.js
+++ b/lib/ads/client_side_ad.js
@@ -244,6 +244,14 @@ shaka.ads.ClientSideAd = class {
return this.ad_.getTitle();
}
+ /**
+ * @override
+ * @export
+ */
+ getDescription() {
+ return this.ad_.getDescription();
+ }
+
/**
* @override
* @export
diff --git a/lib/ads/server_side_ad.js b/lib/ads/server_side_ad.js
index a078585ad9..12aa2db89a 100644
--- a/lib/ads/server_side_ad.js
+++ b/lib/ads/server_side_ad.js
@@ -213,6 +213,14 @@ shaka.ads.ServerSideAd = class {
return this.ad_.getTitle();
}
+ /**
+ * @override
+ * @export
+ */
+ getDescription() {
+ return this.ad_.getDescription();
+ }
+
/**
* @override
* @export
diff --git a/lib/cast/cast_utils.js b/lib/cast/cast_utils.js
index 74e3eb6cee..3eb3e47fc0 100644
--- a/lib/cast/cast_utils.js
+++ b/lib/cast/cast_utils.js
@@ -374,6 +374,7 @@ shaka.cast.CastUtils.PlayerInitAfterLoadState = [
shaka.cast.CastUtils.PlayerVoidMethods = [
'addChaptersTrack',
'addTextTrackAsync',
+ 'addThumbnailsTrack',
'cancelTrickPlay',
'configure',
'getChapters',
diff --git a/lib/cea/cea608_memory.js b/lib/cea/cea608_memory.js
index a30d8e8073..751141a39f 100644
--- a/lib/cea/cea608_memory.js
+++ b/lib/cea/cea608_memory.js
@@ -184,6 +184,10 @@ shaka.cea.Cea608Memory = class {
* @param {number} count Count of rows to move.
*/
moveRows(dst, src, count) {
+ if (src < 0 || dst < 0) {
+ return;
+ }
+
if (dst >= src) {
for (let i = count-1; i >= 0; i--) {
this.rows_[dst + i] = this.rows_[src + i].map((e) => e);
diff --git a/lib/media/media_source_engine.js b/lib/media/media_source_engine.js
index 82799d9fe6..9e2373585f 100644
--- a/lib/media/media_source_engine.js
+++ b/lib/media/media_source_engine.js
@@ -57,6 +57,9 @@ shaka.media.MediaSourceEngine = class {
/** @private {HTMLMediaElement} */
this.video_ = video;
+ /** @private {?shaka.extern.MediaSourceConfiguration} */
+ this.config_ = null;
+
/** @private {shaka.extern.TextDisplayer} */
this.textDisplayer_ = textDisplayer;
@@ -298,6 +301,7 @@ shaka.media.MediaSourceEngine = class {
this.video_ = null;
}
+ this.config_ = null;
this.mediaSource_ = null;
this.textEngine_ = null;
this.textDisplayer_ = null;
@@ -367,8 +371,8 @@ shaka.media.MediaSourceEngine = class {
mimeType =
shaka.media.Transmuxer.convertTsCodecs(contentType, mimeType);
}
-
- const sourceBuffer = this.mediaSource_.addSourceBuffer(mimeType);
+ const type = mimeType + this.config_.sourceBufferExtraFeatures;
+ const sourceBuffer = this.mediaSource_.addSourceBuffer(type);
this.eventManager_.listen(
sourceBuffer, 'error',
@@ -384,6 +388,16 @@ shaka.media.MediaSourceEngine = class {
}
}
+ /**
+ * Called by the Player to provide an updated configuration any time it
+ * changes. Must be called at least once before init().
+ *
+ * @param {shaka.extern.MediaSourceConfiguration} config
+ */
+ configure(config) {
+ this.config_ = config;
+ }
+
/**
* Reinitialize the TextEngine for a new text type.
* @param {string} mimeType
diff --git a/lib/player.js b/lib/player.js
index b376897817..62c0d4725f 100644
--- a/lib/player.js
+++ b/lib/player.js
@@ -27,6 +27,7 @@ goog.require('shaka.media.QualityObserver');
goog.require('shaka.media.RegionObserver');
goog.require('shaka.media.RegionTimeline');
goog.require('shaka.media.SegmentIndex');
+goog.require('shaka.media.SegmentReference');
goog.require('shaka.media.SrcEqualsPlayhead');
goog.require('shaka.media.StreamingEngine');
goog.require('shaka.media.TimeRangesUtils');
@@ -1696,6 +1697,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
this.processTimedMetadataMediaSrc_(metadata, offset, endTime);
},
this.lcevcDil_);
+ mediaSourceEngine.configure(this.config_.mediaSource);
const {segmentRelativeVttTiming} = this.config_.manifest;
mediaSourceEngine.setSegmentRelativeVttTiming(segmentRelativeVttTiming);
@@ -3187,6 +3189,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
}
if (this.mediaSourceEngine_) {
+ this.mediaSourceEngine_.configure(this.config_.mediaSource);
const {segmentRelativeVttTiming} = this.config_.manifest;
this.mediaSourceEngine_.setSegmentRelativeVttTiming(
segmentRelativeVttTiming);
@@ -4728,6 +4731,138 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
return shaka.util.StreamUtils.textStreamToTrack(stream);
}
+ /**
+ * Adds the given thumbnails track to the loaded manifest.
+ * load()
must resolve before calling. The presentation must
+ * have a duration.
+ *
+ * This returns the created track, which can immediately be used by the
+ * application.
+ *
+ * @param {string} uri
+ * @param {string=} mimeType
+ * @return {!Promise.}
+ * @export
+ */
+ async addThumbnailsTrack(uri, mimeType) {
+ if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE &&
+ this.loadMode_ != shaka.Player.LoadMode.SRC_EQUALS) {
+ shaka.log.error(
+ 'Must call load() and wait for it to resolve before adding image ' +
+ 'tracks.');
+ throw new shaka.util.Error(
+ shaka.util.Error.Severity.RECOVERABLE,
+ shaka.util.Error.Category.PLAYER,
+ shaka.util.Error.Code.CONTENT_NOT_LOADED);
+ }
+
+ if (this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS) {
+ shaka.log.error('Cannot add this thumbnail track when loaded with src=');
+ throw new shaka.util.Error(
+ shaka.util.Error.Severity.RECOVERABLE,
+ shaka.util.Error.Category.TEXT,
+ shaka.util.Error.Code.CANNOT_ADD_EXTERNAL_THUMBNAILS_TO_SRC_EQUALS);
+ }
+
+ if (!mimeType) {
+ mimeType = await this.getTextMimetype_(uri);
+ }
+
+ if (mimeType != 'text/vtt') {
+ throw new shaka.util.Error(
+ shaka.util.Error.Severity.RECOVERABLE,
+ shaka.util.Error.Category.TEXT,
+ shaka.util.Error.Code.UNSUPPORTED_EXTERNAL_THUMBNAILS_URI,
+ uri);
+ }
+
+ const ContentType = shaka.util.ManifestParserUtils.ContentType;
+
+ const duration = this.manifest_.presentationTimeline.getDuration();
+ if (duration == Infinity) {
+ throw new shaka.util.Error(
+ shaka.util.Error.Severity.RECOVERABLE,
+ shaka.util.Error.Category.MANIFEST,
+ shaka.util.Error.Code.CANNOT_ADD_EXTERNAL_THUMBNAILS_TO_LIVE_STREAM);
+ }
+
+ goog.asserts.assert(
+ this.networkingEngine_, 'Need networking engine.');
+ const buffer = await this.getTextData_(uri,
+ this.networkingEngine_,
+ this.config_.streaming.retryParameters);
+
+ const factory = shaka.text.TextEngine.findParser(mimeType);
+ if (!factory) {
+ throw new shaka.util.Error(
+ shaka.util.Error.Severity.CRITICAL,
+ shaka.util.Error.Category.TEXT,
+ shaka.util.Error.Code.MISSING_TEXT_PLUGIN,
+ mimeType);
+ }
+ const TextParser = factory();
+ const time = {
+ periodStart: 0,
+ segmentStart: 0,
+ segmentEnd: duration,
+ vttOffset: 0,
+ };
+ const data = shaka.util.BufferUtils.toUint8(buffer);
+ const cues = TextParser.parseMedia(data, time);
+
+ const references = [];
+ for (const cue of cues) {
+ const imageUri = shaka.util.ManifestParserUtils.resolveUris(
+ [uri], [cue.payload])[0];
+ if (imageUri.includes('#xywh')) {
+ shaka.log.alwaysWarn('Unsupported image uri', imageUri);
+ continue;
+ }
+ references.push(new shaka.media.SegmentReference(
+ cue.startTime,
+ cue.endTime,
+ () => [imageUri],
+ /* startByte= */ 0,
+ /* endByte= */ null,
+ /* initSegmentReference= */ null,
+ /* timestampOffset= */ 0,
+ /* appendWindowStart= */ 0,
+ /* appendWindowEnd= */ Infinity,
+ ));
+ }
+
+ /** @type {shaka.extern.Stream} */
+ const stream = {
+ id: this.nextExternalStreamId_++,
+ originalId: null,
+ createSegmentIndex: () => Promise.resolve(),
+ segmentIndex: new shaka.media.SegmentIndex(references),
+ mimeType: mimeType || '',
+ codecs: '',
+ kind: '',
+ encrypted: false,
+ drmInfos: [],
+ keyIds: new Set(),
+ language: 'und',
+ label: null,
+ type: ContentType.IMAGE,
+ primary: false,
+ trickModeVideo: null,
+ emsgSchemeIdUris: null,
+ roles: [],
+ forced: false,
+ channelsCount: null,
+ audioSamplingRate: null,
+ spatialAudio: false,
+ closedCaptions: null,
+ tilesLayout: '1x1',
+ };
+
+ this.manifest_.imageStreams.push(stream);
+ this.onTracksChanged_();
+ return shaka.util.StreamUtils.imageStreamToTrack(stream);
+ }
+
/**
* Adds the given chapters track to the loaded manifest. load()
* must resolve before calling. The presentation must have a duration.
diff --git a/lib/text/ttml_text_parser.js b/lib/text/ttml_text_parser.js
index de34a05241..bf26ec4509 100644
--- a/lib/text/ttml_text_parser.js
+++ b/lib/text/ttml_text_parser.js
@@ -164,6 +164,12 @@ shaka.text.TtmlTextParser = class {
cellResolutionInfo, /* parentCueElement= */ null,
/* isContent= */ false);
if (cue) {
+ // According to the TTML spec, backgrounds default to transparent.
+ // So default the background of the top-level element to transparent.
+ // Nested elements may override that background color already.
+ if (!cue.backgroundColor) {
+ cue.backgroundColor = 'transparent';
+ }
cues.push(cue);
}
}
diff --git a/lib/util/error.js b/lib/util/error.js
index 45f7f4fd18..9cf5305cc7 100644
--- a/lib/util/error.js
+++ b/lib/util/error.js
@@ -358,6 +358,18 @@ shaka.util.Error.Code = {
*/
'CHAPTERS_TRACK_FAILED': 2015,
+ /**
+ * External thumbnails tracks cannot be added in src= because native platform
+ * doesn't support it.
+ */
+ 'CANNOT_ADD_EXTERNAL_THUMBNAILS_TO_SRC_EQUALS': 2016,
+
+ /**
+ * Only external urls of WebVTT type are supported.
+ *
error.data[0] is the uri.
+ */
+ 'UNSUPPORTED_EXTERNAL_THUMBNAILS_URI': 2017,
+
/**
* Some component tried to read past the end of a buffer. The segment index,
* init segment, or PSSH may be malformed.
@@ -707,6 +719,11 @@ shaka.util.Error.Code = {
*/
'HLS_AES_128_INVALID_KEY_LENGTH': 4044,
+ /**
+ * External thumbnails tracks cannot be added to live streams.
+ */
+ 'CANNOT_ADD_EXTERNAL_THUMBNAILS_TO_LIVE_STREAM': 4045,
+
// RETIRED: 'INCONSISTENT_BUFFER_STATE': 5000,
// RETIRED: 'INVALID_SEGMENT_INDEX': 5001,
diff --git a/lib/util/player_configuration.js b/lib/util/player_configuration.js
index e016445909..e6246ff1c2 100644
--- a/lib/util/player_configuration.js
+++ b/lib/util/player_configuration.js
@@ -247,6 +247,7 @@ shaka.util.PlayerConfiguration = class {
slowHalfLife: 5,
},
restrictToElementSize: false,
+ restrictToScreenSize: false,
ignoreDevicePixelRatio: false,
};
@@ -263,6 +264,10 @@ shaka.util.PlayerConfiguration = class {
drawLogo: false,
};
+ const mediaSource = {
+ sourceBufferExtraFeatures: '',
+ };
+
const AutoShowText = shaka.config.AutoShowText;
/** @type {shaka.extern.PlayerConfiguration} */
@@ -270,6 +275,7 @@ shaka.util.PlayerConfiguration = class {
drm: drm,
manifest: manifest,
streaming: streaming,
+ mediaSource: mediaSource,
offline: offline,
abrFactory: () => new shaka.abr.SimpleAbrManager(),
abr: abr,
diff --git a/test/cast/cast_utils_unit.js b/test/cast/cast_utils_unit.js
index 93e08c7654..fa78068622 100644
--- a/test/cast/cast_utils_unit.js
+++ b/test/cast/cast_utils_unit.js
@@ -207,6 +207,9 @@ describe('CastUtils', () => {
video,
new shaka.test.FakeClosedCaptionParser(),
new shaka.test.FakeTextDisplayer());
+ const config =
+ shaka.util.PlayerConfiguration.createDefault().mediaSource;
+ mediaSourceEngine.configure(config);
const ContentType = shaka.util.ManifestParserUtils.ContentType;
const initObject = new Map();
diff --git a/test/cea/cea608_memory_unit.js b/test/cea/cea608_memory_unit.js
index 4105b0bd41..b7858c414c 100644
--- a/test/cea/cea608_memory_unit.js
+++ b/test/cea/cea608_memory_unit.js
@@ -295,5 +295,93 @@ describe('Cea608Memory', () => {
const caption = memory.forceEmit(startTime, endTime);
expect(caption).toEqual(expectedCaption);
});
+
+ it('does not move rows if source row index is negative', () => {
+ const startTime = 1;
+ const endTime = 2;
+ const text = 'test';
+
+ // Add the text to the buffer, each character on separate rows.
+ // At this point, the memory looks like:
+ // [1]: t
+ // [2]: e
+ // [3]: s
+ // [4]: t
+ for (const c of text) {
+ memory.addChar(CharSet.BASIC_NORTH_AMERICAN,
+ c.charCodeAt(0));
+ memory.setRow(memory.getRow() + 1); // increment row
+ }
+
+ const srcRowIdx = -1;
+ const dstRowIdx = 2;
+ const rowsToMove = 3;
+ memory.moveRows(dstRowIdx, srcRowIdx, rowsToMove);
+
+ // Expected text is 't\ne\ns\nt'
+ const topLevelCue = new shaka.text.Cue(startTime, endTime, '');
+ topLevelCue.nestedCues = [
+ CeaUtils.createDefaultCue(startTime, endTime, 't'),
+ CeaUtils.createLineBreakCue(startTime, endTime),
+ CeaUtils.createDefaultCue(startTime, endTime, 'e'),
+ CeaUtils.createLineBreakCue(startTime, endTime),
+ CeaUtils.createDefaultCue(startTime, endTime, 's'),
+ CeaUtils.createLineBreakCue(startTime, endTime),
+ CeaUtils.createDefaultCue(startTime, endTime, 't'),
+ ];
+
+ const expectedCaption = {
+ stream,
+ cue: topLevelCue,
+ };
+
+ // Force out the new memory.
+ const caption = memory.forceEmit(startTime, endTime);
+ expect(caption).toEqual(expectedCaption);
+ });
+
+ it('does not move rows if destination row index is negative', () => {
+ const startTime = 1;
+ const endTime = 2;
+ const text = 'test';
+
+ // Add the text to the buffer, each character on separate rows.
+ // At this point, the memory looks like:
+ // [1]: t
+ // [2]: e
+ // [3]: s
+ // [4]: t
+ for (const c of text) {
+ memory.addChar(CharSet.BASIC_NORTH_AMERICAN,
+ c.charCodeAt(0));
+ memory.setRow(memory.getRow() + 1); // increment row
+ }
+
+ const srcRowIdx = 1;
+ const dstRowIdx = -2;
+ const rowsToMove = 3;
+ memory.moveRows(dstRowIdx, srcRowIdx, rowsToMove);
+
+ // Expected text is 't\ne\ns\nt'
+ const topLevelCue = new shaka.text.Cue(startTime, endTime, '');
+ topLevelCue.nestedCues = [
+ CeaUtils.createDefaultCue(startTime, endTime, 't'),
+ CeaUtils.createLineBreakCue(startTime, endTime),
+ CeaUtils.createDefaultCue(startTime, endTime, 'e'),
+ CeaUtils.createLineBreakCue(startTime, endTime),
+ CeaUtils.createDefaultCue(startTime, endTime, 's'),
+ CeaUtils.createLineBreakCue(startTime, endTime),
+ CeaUtils.createDefaultCue(startTime, endTime, 't'),
+ ];
+
+ const expectedCaption = {
+ stream,
+ cue: topLevelCue,
+ };
+
+ // Force out the new memory.
+ const caption = memory.forceEmit(startTime, endTime);
+ expect(caption).toEqual(expectedCaption);
+ });
});
});
diff --git a/test/hls/hls_parser_unit.js b/test/hls/hls_parser_unit.js
index a0c6612d68..2d6ccca6d4 100644
--- a/test/hls/hls_parser_unit.js
+++ b/test/hls/hls_parser_unit.js
@@ -118,6 +118,8 @@ describe('HlsParser', () => {
.setResponseText('test:/audio2', media)
.setResponseText('test:/video', media)
.setResponseText('test:/video2', media)
+ .setResponseText('test:/text', media)
+ .setResponseText('test:/text2', media)
.setResponseText('test:/main.vtt', vttText)
.setResponseValue('test:/init.mp4', initSegmentData)
.setResponseValue('test:/init2.mp4', initSegmentData)
@@ -935,11 +937,14 @@ describe('HlsParser', () => {
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,mp4a",',
'RESOLUTION=960x540,FRAME-RATE=60,AUDIO="aud1"\n',
'video\n',
+
'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="en",',
- 'URI="audio"\n',
+ 'NAME="English",URI="audio"\n',
+
'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="en",',
'CHARACTERISTICS="public.accessibility.describes-video,',
- 'public.accessibility.describes-music-and-sound",URI="audio2"\n',
+ 'public.accessibility.describes-music-and-sound",',
+ 'NAME="English (describes-video)",URI="audio2"\n',
].join('');
const media = [
@@ -977,6 +982,58 @@ describe('HlsParser', () => {
await testHlsParser(master, media, manifest);
});
+ it('parses characteristics from text tags', async () => {
+ const master = [
+ '#EXTM3U\n',
+ '#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",',
+ 'RESOLUTION=960x540,FRAME-RATE=60,SUBTITLES="sub1"\n',
+ 'video\n',
+
+ '#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub1",LANGUAGE="en",',
+ 'NAME="English (caption)",DEFAULT=YES,AUTOSELECT=YES,',
+ 'URI="text"\n',
+
+ '#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub1",LANGUAGE="en",',
+ 'NAME="English (caption)",DEFAULT=YES,AUTOSELECT=YES,',
+ 'CHARACTERISTICS="public.accessibility.describes-spoken-dialog,',
+ 'public.accessibility.describes-music-and-sound",',
+ 'URI="text2"\n',
+ ].join('');
+
+ const media = [
+ '#EXTM3U\n',
+ '#EXT-X-PLAYLIST-TYPE:VOD\n',
+ '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
+ '#EXTINF:5,\n',
+ '#EXT-X-BYTERANGE:121090@616\n',
+ 'main.mp4',
+ ].join('');
+
+ const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
+ manifest.anyTimeline();
+ manifest.addPartialVariant((variant) => {
+ variant.addPartialStream(ContentType.VIDEO);
+ });
+ manifest.addPartialTextStream((stream) => {
+ stream.language = 'en';
+ stream.kind = TextStreamKind.SUBTITLE;
+ stream.mime('application/mp4', '');
+ });
+ manifest.addPartialTextStream((stream) => {
+ stream.language = 'en';
+ stream.kind = TextStreamKind.SUBTITLE;
+ stream.mime('application/mp4', '');
+ stream.roles = [
+ 'public.accessibility.describes-spoken-dialog',
+ 'public.accessibility.describes-music-and-sound',
+ ];
+ });
+ manifest.sequenceMode = true;
+ });
+
+ await testHlsParser(master, media, manifest);
+ });
+
it('gets mime type from header request', async () => {
const master = [
'#EXTM3U\n',
diff --git a/test/media/drm_engine_integration.js b/test/media/drm_engine_integration.js
index 8b3f587af9..25ea1524ae 100644
--- a/test/media/drm_engine_integration.js
+++ b/test/media/drm_engine_integration.js
@@ -128,6 +128,9 @@ describe('DrmEngine', () => {
video,
new shaka.test.FakeClosedCaptionParser(),
new shaka.test.FakeTextDisplayer());
+ const mediaSourceConfig =
+ shaka.util.PlayerConfiguration.createDefault().mediaSource;
+ mediaSourceEngine.configure(mediaSourceConfig);
const expectedObject = new Map();
expectedObject.set(ContentType.AUDIO, audioStream);
diff --git a/test/media/media_source_engine_integration.js b/test/media/media_source_engine_integration.js
index ebbd49a370..b950129967 100644
--- a/test/media/media_source_engine_integration.js
+++ b/test/media/media_source_engine_integration.js
@@ -41,6 +41,8 @@ describe('MediaSourceEngine', () => {
video,
new shaka.media.ClosedCaptionParser(),
textDisplayer);
+ const config = shaka.util.PlayerConfiguration.createDefault().mediaSource;
+ mediaSourceEngine.configure(config);
mediaSource = /** @type {?} */(mediaSourceEngine)['mediaSource_'];
expect(video.src).toBeTruthy();
diff --git a/test/media/media_source_engine_unit.js b/test/media/media_source_engine_unit.js
index 2ef9e190db..8a805d5e7e 100644
--- a/test/media/media_source_engine_unit.js
+++ b/test/media/media_source_engine_unit.js
@@ -150,6 +150,8 @@ describe('MediaSourceEngine', () => {
video,
mockClosedCaptionParser,
mockTextDisplayer);
+ const config = shaka.util.PlayerConfiguration.createDefault().mediaSource;
+ mediaSourceEngine.configure(config);
});
afterEach(() => {
diff --git a/test/media/streaming_engine_integration.js b/test/media/streaming_engine_integration.js
index 36af6b43d6..517b68ecba 100644
--- a/test/media/streaming_engine_integration.js
+++ b/test/media/streaming_engine_integration.js
@@ -68,6 +68,9 @@ describe('StreamingEngine', () => {
video,
new shaka.test.FakeClosedCaptionParser(),
new shaka.test.FakeTextDisplayer());
+ const mediaSourceConfig =
+ shaka.util.PlayerConfiguration.createDefault().mediaSource;
+ mediaSourceEngine.configure(mediaSourceConfig);
waiter.setMediaSourceEngine(mediaSourceEngine);
});
@@ -90,17 +93,17 @@ describe('StreamingEngine', () => {
segmentAvailability = {
start: 0,
- end: 60,
+ end: 40,
};
timeline = shaka.test.StreamingEngineUtil.createFakePresentationTimeline(
segmentAvailability,
- /* presentationDuration= */ 60,
+ /* presentationDuration= */ 40,
/* maxSegmentDuration= */ metadata.video.segmentDuration,
/* isLive= */ false);
setupNetworkingEngine(
- /* presentationDuration= */ 60,
+ /* presentationDuration= */ 40,
{
audio: metadata.audio.segmentDuration,
video: metadata.video.segmentDuration,
@@ -108,8 +111,8 @@ describe('StreamingEngine', () => {
setupManifest(
/* firstPeriodStartTime= */ 0,
- /* secondPeriodStartTime= */ 30,
- /* presentationDuration= */ 60);
+ /* secondPeriodStartTime= */ 20,
+ /* presentationDuration= */ 40);
setupPlayhead();
@@ -263,10 +266,11 @@ describe('StreamingEngine', () => {
streamingEngine.switchVariant(variant);
await streamingEngine.start();
video.play();
- // The overall test timeout is 120 seconds, and the content is 60
+ // The overall test timeout is 120 seconds, and the content is 40
// seconds. It should be possible to complete this test in 100 seconds,
// and if not, we want the error thrown to be within the overall test's
- // timeout window.
+ // timeout window. Note that we have seen some devices fail to play at
+ // full speed for reasons beyond our control, so we plan for >= 0.5x.
await waiter.timeoutAfter(100).waitForEnd(video);
});
diff --git a/test/offline/offline_integration.js b/test/offline/offline_integration.js
index a4771c93a2..db935b0ffa 100644
--- a/test/offline/offline_integration.js
+++ b/test/offline/offline_integration.js
@@ -68,7 +68,7 @@ filterDescribe('Offline', supportsStorage, () => {
await player.load(contentUri);
video.play();
- await playTo(/* end= */ 3, /* timeout= */ 10);
+ await playTo(/* end= */ 3, /* timeout= */ 20);
await player.unload();
await storage.remove(contentUri);
});
@@ -104,7 +104,7 @@ filterDescribe('Offline', supportsStorage, () => {
await player.load(contentUri);
video.play();
- await playTo(/* end= */ 3, /* timeout= */ 10);
+ await playTo(/* end= */ 3, /* timeout= */ 20);
await player.unload();
await storage.remove(contentUri);
});
@@ -147,7 +147,7 @@ filterDescribe('Offline', supportsStorage, () => {
await player.load(contentUri);
video.play();
- await playTo(/* end= */ 3, /* timeout= */ 10);
+ await playTo(/* end= */ 3, /* timeout= */ 20);
await player.unload();
await storage.remove(contentUri);
});
diff --git a/test/player_integration.js b/test/player_integration.js
index 4093b2f906..7109fff5db 100644
--- a/test/player_integration.js
+++ b/test/player_integration.js
@@ -262,6 +262,49 @@ describe('Player', () => {
expect(cues.length).toBeGreaterThan(0);
});
+ it('skip appended thumbnails for external thumbnails with sprites',
+ async () => {
+ await player.load('test:sintel_no_text_compiled');
+ const locationUri = new goog.Uri(location.href);
+ const partialUri =
+ new goog.Uri('/base/test/test/assets/thumbnails-sprites.vtt');
+ const absoluteUri = locationUri.resolve(partialUri);
+ const newTrack =
+ await player.addThumbnailsTrack(absoluteUri.toString());
+
+ expect(player.getImageTracks()).toEqual([newTrack]);
+
+ const thumbnail1 = await player.getThumbnails(newTrack.id, 0);
+ expect(thumbnail1).toBe(null);
+ const thumbnail2 = await player.getThumbnails(newTrack.id, 10);
+ expect(thumbnail2).toBe(null);
+ const thumbnail3 = await player.getThumbnails(newTrack.id, 40);
+ expect(thumbnail3).toBe(null);
+ });
+
+ it('appends thumbnails for external thumbnails without sprites',
+ async () => {
+ await player.load('test:sintel_no_text_compiled');
+ const locationUri = new goog.Uri(location.href);
+ const partialUri =
+ new goog.Uri('/base/test/test/assets/thumbnails.vtt');
+ const absoluteUri = locationUri.resolve(partialUri);
+ const newTrack =
+ await player.addThumbnailsTrack(absoluteUri.toString());
+
+ expect(player.getImageTracks()).toEqual([newTrack]);
+
+ const thumbnail1 = await player.getThumbnails(newTrack.id, 0);
+ expect(thumbnail1.startTime).toBe(0);
+ expect(thumbnail1.duration).toBe(5);
+ const thumbnail2 = await player.getThumbnails(newTrack.id, 10);
+ expect(thumbnail2.startTime).toBe(5);
+ expect(thumbnail2.duration).toBe(25);
+ const thumbnail3 = await player.getThumbnails(newTrack.id, 40);
+ expect(thumbnail3.startTime).toBe(30);
+ expect(thumbnail3.duration).toBe(30);
+ });
+
// https://github.com/shaka-project/shaka-player/issues/2553
it('does not change the selected track', async () => {
player.configure('streaming.alwaysStreamText', false);
@@ -506,7 +549,7 @@ describe('Player', () => {
// Seek the video, and see if it can continue playing from that point.
video.currentTime = 20;
// Expect that we can then reach the end of the video.
- await waiter.timeoutAfter(20).waitForEnd(video);
+ await waiter.timeoutAfter(40).waitForEnd(video);
});
// Regression test for #2326.
@@ -857,7 +900,7 @@ describe('Player', () => {
/** @type {shaka.test.Waiter} */
const waiter = new shaka.test.Waiter(eventManager)
.setPlayer(player)
- .timeoutAfter(20)
+ .timeoutAfter(40)
.failOnTimeout(true);
await waiter.waitForEnd(video);
diff --git a/test/player_unit.js b/test/player_unit.js
index 91380327ec..ba48ff5f5a 100644
--- a/test/player_unit.js
+++ b/test/player_unit.js
@@ -123,6 +123,7 @@ describe('Player', () => {
streamingEngine = new shaka.test.FakeStreamingEngine();
mediaSourceEngine = {
init: jasmine.createSpy('init').and.returnValue(Promise.resolve()),
+ configure: jasmine.createSpy('configure'),
open: jasmine.createSpy('open').and.returnValue(Promise.resolve()),
destroy:
jasmine.createSpy('destroy').and.returnValue(Promise.resolve()),
diff --git a/test/test/assets/screenshots/safari-Mac/native-end-time-edge-case.png b/test/test/assets/screenshots/safari-Mac/native-end-time-edge-case.png
index 966ddb9255..173924f94f 100644
Binary files a/test/test/assets/screenshots/safari-Mac/native-end-time-edge-case.png and b/test/test/assets/screenshots/safari-Mac/native-end-time-edge-case.png differ
diff --git a/test/test/assets/thumbnails-sprites.vtt b/test/test/assets/thumbnails-sprites.vtt
new file mode 100644
index 0000000000..42d2cf0fb5
--- /dev/null
+++ b/test/test/assets/thumbnails-sprites.vtt
@@ -0,0 +1,10 @@
+WEBVTT
+
+00:00.000 --> 00:05.000
+image1.jpg#xywh=0,0,160,90
+
+00:05.000 --> 00:30.000
+image2.jpg#xywh=0,0,160,90
+
+00:30.000 --> 01:00.000
+image3.jpg#xywh=0,0,160,90
\ No newline at end of file
diff --git a/test/test/assets/thumbnails.vtt b/test/test/assets/thumbnails.vtt
new file mode 100644
index 0000000000..b55896bf7c
--- /dev/null
+++ b/test/test/assets/thumbnails.vtt
@@ -0,0 +1,10 @@
+WEBVTT
+
+00:00.000 --> 00:05.000
+image1.jpg
+
+00:05.000 --> 00:30.000
+image2.jpg
+
+00:30.000 --> 01:00.000
+image3.jpg
\ No newline at end of file
diff --git a/test/test/util/fake_ad.js b/test/test/util/fake_ad.js
index ef67c45925..dbd9fb774d 100644
--- a/test/test/util/fake_ad.js
+++ b/test/test/util/fake_ad.js
@@ -37,6 +37,9 @@ shaka.test.FakeAd = class {
/** @private {string} */
this.title_ = 'Test Title';
+
+ /** @private {string} */
+ this.description_ = 'Test Description';
}
/**
@@ -201,6 +204,14 @@ shaka.test.FakeAd = class {
return this.title_;
}
+ /**
+ * @override
+ * @export
+ */
+ getDescription() {
+ return this.description_;
+ }
+
/**
* @override
* @export
diff --git a/test/test/util/fake_media_source_engine.js b/test/test/util/fake_media_source_engine.js
index 516f42d820..40224f0943 100644
--- a/test/test/util/fake_media_source_engine.js
+++ b/test/test/util/fake_media_source_engine.js
@@ -54,6 +54,9 @@ shaka.test.FakeMediaSourceEngine = class {
/** @type {!jasmine.Spy} */
this.open = jasmine.createSpy('open').and.returnValue(Promise.resolve());
+ /** @type {!jasmine.Spy} */
+ this.configure = jasmine.createSpy('configure').and.stub();
+
/** @type {!jasmine.Spy} */
this.reinitText = jasmine.createSpy('reinitText').and.stub();
diff --git a/test/test/util/ttml_utils.js b/test/test/util/ttml_utils.js
index bb7ed09482..a8417bade4 100644
--- a/test/test/util/ttml_utils.js
+++ b/test/test/util/ttml_utils.js
@@ -51,8 +51,6 @@ shaka.test.TtmlUtils = class {
region,
nestedCues: jasmine.any(Object),
payload: '',
- startTime: 0,
- endTime: Infinity,
isContainer: true,
});
Object.assign(containerCue, properties);
diff --git a/test/text/ttml_text_parser_unit.js b/test/text/ttml_text_parser_unit.js
index 32f4e3353a..a0b09bb0fb 100644
--- a/test/text/ttml_text_parser_unit.js
+++ b/test/text/ttml_text_parser_unit.js
@@ -1535,6 +1535,52 @@ describe('TtmlTextParser', () => {
{startTime: 1, endTime: 2});
});
+ // Regression test for #4468
+ it('defaults the body background to transparent', () => {
+ verifyHelper(
+ // One cue, don't care about the details
+ [{}],
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '
' +
+ 'Test' +
+ '
' +
+ '
',
+ {periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0},
+ // The body must have these properties:
+ {backgroundColor: 'transparent'},
+ // The div must have these properties:
+ {}); // don't care
+ });
+
+ // Regression test for #4468
+ it('allows the body background color to be set', () => {
+ verifyHelper(
+ // One cue, don't care about the details
+ [{}],
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '
' +
+ 'Test' +
+ '
' +
+ '
',
+ {periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0},
+ // The body must have these properties:
+ {backgroundColor: 'black'},
+ // The div must have these properties:
+ {}); // don't care
+ });
+
it('parses wrapping option', () => {
verifyHelper(
[
diff --git a/test/ui/text_displayer_layout_unit.js b/test/ui/text_displayer_layout_unit.js
index 61f2f63586..2b2118520d 100644
--- a/test/ui/text_displayer_layout_unit.js
+++ b/test/ui/text_displayer_layout_unit.js
@@ -262,11 +262,19 @@ filterDescribe('TextDisplayer layout', supportsScreenshots, () => {
// Regression test for #3151
// Only one cue should be displayed. Note, however, that we don't control
- // this in a browser's native display. As of Feb 2021, only Firefox does
- // the right thing in native text display. Chrome, Edge, and Safari all
+ // this in a browser's native display. As of Sep 2022, both Firefox and
+ // Safari>=16 do the right thing in native text display. Chrome and Edge
// show both cues in this edge case. When we control the display of text
// through the UI & DOM, we can always get the timing right.
it('cues ending exactly now', async () => {
+ // Due to a Safari implementation bug, the browser only does the correct
+ // thing for this test case on Safari 16+. Skip the test on earlier
+ // versions.
+ const safariVersion = shaka.util.Platform.safariVersion();
+ if (prefix == 'native' && safariVersion && safariVersion < 16) {
+ pending('Native implementation known to be broken on Safari < 16');
+ }
+
// At time exactly 1, this cue should _not_ be displayed any more.
textDisplayer.append([
new shaka.text.Cue(0, 1, 'This cue is over and gone.'),