diff --git a/build/types/complete b/build/types/complete index cdd7be2f6..5ae17fd65 100644 --- a/build/types/complete +++ b/build/types/complete @@ -12,4 +12,5 @@ +@text +@optionalText +@transmuxer ++@devices +@ui diff --git a/build/types/core b/build/types/core index 339e5e6e6..2bb089d78 100644 --- a/build/types/core +++ b/build/types/core @@ -20,6 +20,12 @@ +../../lib/deprecate/enforcer.js +../../lib/deprecate/version.js ++../../lib/device/abstract_device.js ++../../lib/device/apple_browser.js ++../../lib/device/default_browser.js ++../../lib/device/device_factory.js ++../../lib/device/i_device.js + +../../lib/drm/drm_engine.js +../../lib/drm/drm_utils.js +../../lib/drm/fairplay.js @@ -113,7 +119,6 @@ +../../lib/util/object_utils.js +../../lib/util/operation_manager.js +../../lib/util/periods.js -+../../lib/util/platform.js +../../lib/util/player_configuration.js +../../lib/util/pssh.js +../../lib/util/public_promise.js diff --git a/build/types/devices b/build/types/devices new file mode 100644 index 000000000..d102f3967 --- /dev/null +++ b/build/types/devices @@ -0,0 +1,10 @@ +# Device + ++../../lib/device/chromecast.js ++../../lib/device/hisense.js ++../../lib/device/playstation.js ++../../lib/device/tizen.js ++../../lib/device/vizio.js ++../../lib/device/webkit_stb.js ++../../lib/device/webos.js ++../../lib/device/xbox.js diff --git a/externs/xbox.js b/externs/xbox.js index af243b96d..00e524984 100644 --- a/externs/xbox.js +++ b/externs/xbox.js @@ -61,3 +61,10 @@ chrome.webview.hostObjects.sync = {}; /** @const */ chrome.webview.hostObjects.sync.Windows = Windows; + +/** + * Typedef for the module interface. + * + * @typedef {typeof Windows} + */ +var WinRT; diff --git a/lib/ads/interstitial_ad_manager.js b/lib/ads/interstitial_ad_manager.js index 142145f83..c86983be4 100644 --- a/lib/ads/interstitial_ad_manager.js +++ b/lib/ads/interstitial_ad_manager.js @@ -12,6 +12,8 @@ goog.require('shaka.Player'); goog.require('shaka.ads.InterstitialAd'); goog.require('shaka.ads.InterstitialStaticAd'); goog.require('shaka.ads.Utils'); +goog.require('shaka.device.DeviceFactory'); +goog.require('shaka.device.IDevice'); goog.require('shaka.log'); goog.require('shaka.media.PreloadManager'); goog.require('shaka.net.NetworkingEngine'); @@ -21,7 +23,6 @@ goog.require('shaka.util.Error'); goog.require('shaka.util.EventManager'); goog.require('shaka.util.FakeEvent'); goog.require('shaka.util.IReleasable'); -goog.require('shaka.util.Platform'); goog.require('shaka.util.PublicPromise'); goog.require('shaka.util.StringUtils'); goog.require('shaka.util.Timer'); @@ -251,8 +252,7 @@ shaka.ads.InterstitialAdManager = class { this.baseVideo_, 'seeked', this.onSeeked_); this.eventManager_.listen( this.baseVideo_, 'ended', this.checkForInterstitials_); - if ('requestVideoFrameCallback' in this.baseVideo_ && - !shaka.util.Platform.isSmartTV()) { + if ('requestVideoFrameCallback' in this.baseVideo_ && !this.isSmartTV_()) { const baseVideo = /** @type {!HTMLVideoElement} */ (this.baseVideo_); const videoFrameCallback = (now, metadata) => { if (this.videoCallbackId_ == -1) { @@ -1080,7 +1080,7 @@ shaka.ads.InterstitialAdManager = class { const detachBasePlayerPromise = new shaka.util.PublicPromise(); const checkState = async (e) => { if (e['state'] == 'detach') { - if (shaka.util.Platform.isSmartTV()) { + if (this.isSmartTV_()) { await new Promise( (resolve) => new shaka.util.Timer(resolve).tickAfter(0.1)); } @@ -1747,6 +1747,20 @@ shaka.ads.InterstitialAdManager = class { return Array.from(this.interstitials_); } + /** + * @return {boolean} + * @private + */ + isSmartTV_() { + const device = shaka.device.DeviceFactory.getDevice(); + const deviceType = device.getDeviceType(); + if (deviceType == shaka.device.IDevice.DeviceType.TV || + deviceType == shaka.device.IDevice.DeviceType.CONSOLE || + deviceType == shaka.device.IDevice.DeviceType.CAST) { + return true; + } + return false; + } /** * @param {!shaka.extern.AdInterstitial} interstitial diff --git a/lib/cast/cast_receiver.js b/lib/cast/cast_receiver.js index 98e4bd4c9..1c8bf8e6d 100644 --- a/lib/cast/cast_receiver.js +++ b/lib/cast/cast_receiver.js @@ -9,13 +9,14 @@ goog.provide('shaka.cast.CastReceiver'); goog.require('goog.asserts'); goog.require('shaka.Player'); goog.require('shaka.cast.CastUtils'); +goog.require('shaka.device.DeviceFactory'); +goog.require('shaka.device.IDevice'); goog.require('shaka.log'); goog.require('shaka.util.Error'); goog.require('shaka.util.EventManager'); goog.require('shaka.util.FakeEvent'); goog.require('shaka.util.FakeEventTarget'); goog.require('shaka.util.IDestroyable'); -goog.require('shaka.util.Platform'); goog.require('shaka.util.Timer'); @@ -271,7 +272,9 @@ shaka.cast.CastReceiver = class extends shaka.util.FakeEventTarget { // browser is a Chromecast before starting the receiver manager. We // wouldn't do browser detection except for debugging, so only do this in // uncompiled mode. - if (shaka.util.Platform.isChromecast()) { + const device = shaka.device.DeviceFactory.getDevice(); + const deviceType = device.getDeviceType(); + if (deviceType === shaka.device.IDevice.DeviceType.CAST) { manager.start(); } } else { diff --git a/lib/device/abstract_device.js b/lib/device/abstract_device.js new file mode 100644 index 000000000..9162057f5 --- /dev/null +++ b/lib/device/abstract_device.js @@ -0,0 +1,305 @@ +/*! @license + * Shaka Player + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +goog.provide('shaka.device.AbstractDevice'); + +goog.require('shaka.device.IDevice'); +goog.require('shaka.util.Dom'); +goog.require('shaka.util.Lazy'); + + +/** + * @abstract + * @implements {shaka.device.IDevice} + */ +shaka.device.AbstractDevice = class { + constructor() { + /** @private {!shaka.util.Lazy} */ + this.abstractDeviceType_ = new shaka.util.Lazy(() => { + if (navigator.userAgent.match(/Smart( ?|_)TV/i) || + navigator.userAgent.match(/Android ?TV/i)) { + return shaka.device.IDevice.DeviceType.TV; + } + if (navigator.userAgentData) { + if (navigator.userAgentData.mobile) { + return shaka.device.IDevice.DeviceType.MOBILE; + } else { + return shaka.device.IDevice.DeviceType.DESKTOP; + } + } + if (/(?:iPhone|iPad|iPod)/.test(navigator.userAgent)) { + return shaka.device.IDevice.DeviceType.MOBILE; + } + if (navigator.userAgentData && navigator.userAgentData.platform) { + if (navigator.userAgentData.platform.toLowerCase() == 'android') { + return shaka.device.IDevice.DeviceType.MOBILE; + } else { + return shaka.device.IDevice.DeviceType.DESKTOP; + } + } + if (navigator.userAgent.includes('Android')) { + return shaka.device.IDevice.DeviceType.MOBILE; + } + return shaka.device.IDevice.DeviceType.DESKTOP; + }); + + /** @private {!shaka.util.Lazy} */ + this.browserEngine_ = new shaka.util.Lazy(() => { + if (navigator.vendor.includes('Apple') && + (navigator.userAgent.includes('Version/') || + navigator.userAgent.includes('OS/'))) { + return shaka.device.IDevice.BrowserEngine.WEBKIT; + } + if (navigator.userAgent.includes('Edge/')) { + return shaka.device.IDevice.BrowserEngine.EDGE; + } + if (navigator.userAgent.includes('Chrome/')) { + return shaka.device.IDevice.BrowserEngine.CHROMIUM; + } + if (navigator.userAgent.includes('Firefox/')) { + return shaka.device.IDevice.BrowserEngine.GECKO; + } + return shaka.device.IDevice.BrowserEngine.UNKNOWN; + }); + } + + /** + * @override + */ + supportsMediaSource() { + const mediaSource = window.ManagedMediaSource || window.MediaSource; + // Browsers that lack a media source implementation will have no reference + // to |window.MediaSource|. Platforms that we see having problematic media + // source implementations will have this reference removed via a polyfill. + if (!mediaSource) { + return false; + } + + // Some very old MediaSource implementations didn't have isTypeSupported. + if (!mediaSource.isTypeSupported) { + return false; + } + + return true; + } + + /** + * @override + */ + supportsMediaType(mimeType) { + const video = shaka.util.Dom.anyMediaElement(); + return video.canPlayType(mimeType) != ''; + } + + /** + * @override + */ + supportsMediaCapabilities() { + return !!navigator.mediaCapabilities; + } + + /** + * @override + */ + getDeviceType() { + return this.abstractDeviceType_.value(); + } + + /** + * @override + */ + getBrowserEngine() { + return this.browserEngine_.value(); + } + + /** + * @override + */ + requiresEncryptionInfoInAllInitSegments(keySystem, contentType) { + return false; + } + + /** + * @override + */ + requiresClearAndEncryptedInitSegments() { + return false; + } + + /** + * @override + */ + insertEncryptionDataBeforeClear() { + return false; + } + + /** + * @override + */ + requiresTfhdFix(contentType) { + return false; + } + + /** + * @override + */ + requiresEC3InitSegments() { + return false; + } + + /** + * @override + */ + supportsSequenceMode() { + return true; + } + + /** + * @override + */ + supportsSmoothCodecSwitching() { + return true; + } + + /** + * @override + */ + supportsServerCertificate() { + return true; + } + + /** + * @override + */ + seekDelay() { + return 0; + } + + /** + * @override + */ + detectMaxHardwareResolution() { + return Promise.resolve({width: Infinity, height: Infinity}); + } + + /** + * @override + */ + shouldOverrideDolbyVisionCodecs() { + return false; + } + + /** + * @override + */ + shouldAvoidUseTextDecoderEncoder() { + return false; + } + + /** + * @override + */ + adjustConfig(config) { + const deviceType = this.getDeviceType(); + if (deviceType === shaka.device.IDevice.DeviceType.TV || + deviceType === shaka.device.IDevice.DeviceType.CONSOLE || + deviceType === shaka.device.IDevice.DeviceType.CAST) { + config.ads.customPlayheadTracker = true; + config.ads.skipPlayDetection = true; + config.ads.supportsMultipleMediaElements = false; + } + return config; + } + + /** + * @override + */ + supportsOfflineStorage() { + return !!window.indexedDB; + } + + /** + * @override + */ + rejectCodecs() { + return []; + } + + /** + * @override + */ + getHdrLevel(preferHLG) { + if (window.matchMedia !== undefined && + window.matchMedia('(color-gamut: p3)').matches) { + return preferHLG ? 'HLG' : 'PQ'; + } + return 'SDR'; + } + + /** + * @override + */ + supportsAirPlay() { + return false; + } + + /** + * @override + */ + misreportAC3UsingDrm() { + return false; + } + + /** + * @override + */ + returnLittleEndianUsingPlayReady() { + return false; + } + + /** + * @override + */ + supportsEncryptionSchemePolyfill() { + return true; + } + + /** + * @override + */ + misreportsSupportForPersistentLicenses() { + return false; + } + + /** + * @override + */ + supportStandardVP9Checking() { + return true; + } + + /** + * @override + */ + createMediaKeysWhenCheckingSupport() { + return true; + } + + /** + * @override + */ + disableHEVCSupport() { + return false; + } + + /** + * @override + */ + toString() { + return `Device: ${this.getDeviceName()} v${this.getVersion()}; ` + + `Type: ${this.getDeviceType()}`; + } +}; diff --git a/lib/device/apple_browser.js b/lib/device/apple_browser.js new file mode 100644 index 000000000..6a142195e --- /dev/null +++ b/lib/device/apple_browser.js @@ -0,0 +1,152 @@ +/*! @license + * Shaka Player + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +goog.provide('shaka.device.AppleBrowser'); + +goog.require('shaka.device.AbstractDevice'); +goog.require('shaka.device.DeviceFactory'); +goog.require('shaka.device.IDevice'); +goog.require('shaka.util.Lazy'); + + +/** + * @final + */ +shaka.device.AppleBrowser = class extends shaka.device.AbstractDevice { + constructor() { + super(); + + /** @private {!shaka.util.Lazy} */ + this.version_ = new shaka.util.Lazy(() => { + // This works for iOS Safari and desktop Safari, which contain something + // like "Version/13.0" indicating the major Safari or iOS version. + let match = navigator.userAgent.match(/Version\/(\d+)/); + if (match) { + return parseInt(match[1], /* base= */ 10); + } + + // This works for all other browsers on iOS, which contain something like + // "OS 13_3" indicating the major & minor iOS version. + match = navigator.userAgent.match(/OS (\d+)(?:_\d+)?/); + if (match) { + return parseInt(match[1], /* base= */ 10); + } + + return null; + }); + + /** @private {!shaka.util.Lazy} */ + this.deviceType_ = new shaka.util.Lazy(() => { + if (/(?:iPhone|iPad|iPod)/.test(navigator.userAgent) || + navigator.maxTouchPoints > 1) { + return shaka.device.IDevice.DeviceType.MOBILE; + } + if ('xr' in navigator) { + return shaka.device.IDevice.DeviceType.VR; + } + return shaka.device.IDevice.DeviceType.DESKTOP; + }); + } + + /** + * @override + */ + getVersion() { + return this.version_.value(); + } + + /** + * @override + */ + getDeviceName() { + return 'Apple Browser'; + } + + /** + * @override + */ + getDeviceType() { + return this.deviceType_.value(); + } + + /** + * @override + */ + getBrowserEngine() { + return shaka.device.IDevice.BrowserEngine.WEBKIT; + } + + /** + * @override + */ + supportsMediaCapabilities() { + return false; + } + + /** + * @override + */ + requiresEncryptionInfoInAllInitSegments(keySystem, contentType) { + return contentType === 'audio'; + } + + /** + * @override + */ + insertEncryptionDataBeforeClear() { + return true; + } + + /** + * @override + */ + requiresTfhdFix(contentType) { + return contentType === 'audio'; + } + + /** + * @override + */ + adjustConfig(config) { + super.adjustConfig(config); + config.abr.minTimeToSwitch = 0.5; + return config; + } + + /** + * @override + */ + supportsAirPlay() { + return true; + } + + /** + * @return {boolean} + * @private + */ + static isAppleBrowser_() { + if (!(navigator.vendor || '').includes('Apple')) { + return false; + } + if (/(?:iPhone|iPad|iPod)/.test(navigator.userAgent) || + navigator.maxTouchPoints > 1) { + return true; + } + if (navigator.userAgentData && navigator.userAgentData.platform && + navigator.userAgentData.platform.toLowerCase() == 'macos') { + return true; + } else if (navigator.platform && + navigator.platform.toLowerCase().includes('mac')) { + return true; + } + return false; + } +}; + +if (shaka.device.AppleBrowser.isAppleBrowser_()) { + shaka.device.DeviceFactory.registerDeviceFactory( + () => new shaka.device.AppleBrowser()); +} diff --git a/lib/device/chromecast.js b/lib/device/chromecast.js new file mode 100644 index 000000000..764a92c54 --- /dev/null +++ b/lib/device/chromecast.js @@ -0,0 +1,188 @@ +/*! @license + * Shaka Player + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +goog.provide('shaka.device.Chromecast'); + +goog.require('shaka.device.AbstractDevice'); +goog.require('shaka.device.DeviceFactory'); +goog.require('shaka.device.IDevice'); +goog.require('shaka.log'); +goog.require('shaka.util.Lazy'); + + +/** + * @final + */ +shaka.device.Chromecast = class extends shaka.device.AbstractDevice { + constructor() { + if (!shaka.device.Chromecast.isChromecast_()) { + throw new Error('Not a Chromecast device!'); + } + super(); + + /** @private {!shaka.util.Lazy} */ + this.version_ = new shaka.util.Lazy(() => { + // Looking for something like "Chrome/106.0.0.0" + const match = navigator.userAgent.match(/Chrome\/(\d+)/); + if (match) { + return parseInt(match[1], /* base= */ 10); + } + + return null; + }); + + /** @private {!shaka.util.Lazy} */ + this.osType_ = new shaka.util.Lazy(() => { + let fieldToCheck = (navigator.userAgentData && + navigator.userAgentData.platform) || navigator.userAgent; + fieldToCheck = fieldToCheck.toLowerCase(); + if (fieldToCheck.includes('android')) { + return shaka.device.Chromecast.OsType_.ANDROID; + } else if (fieldToCheck.includes('fuchsia')) { + return shaka.device.Chromecast.OsType_.FUCHSIA; + } else { + return shaka.device.Chromecast.OsType_.LINUX; + } + }); + } + + /** + * @override + */ + getVersion() { + return this.version_.value(); + } + + /** + * @override + */ + getDeviceName() { + return 'Chromecast with ' + this.osType_.value(); + } + + /** + * @override + */ + getDeviceType() { + return shaka.device.IDevice.DeviceType.CAST; + } + + /** + * @override + */ + getBrowserEngine() { + return shaka.device.IDevice.BrowserEngine.CHROMIUM; + } + + /** + * @override + */ + supportsMediaCapabilities() { + return super.supportsMediaCapabilities() && + this.osType_.value() !== shaka.device.Chromecast.OsType_.LINUX; + } + + /** + * @override + */ + supportsSmoothCodecSwitching() { + return this.osType_.value() !== shaka.device.Chromecast.OsType_.LINUX; + } + + /** + * @override + */ + seekDelay() { + switch (this.osType_.value()) { + case shaka.device.Chromecast.OsType_.ANDROID: + return 0; + case shaka.device.Chromecast.OsType_.FUCHSIA: + return 3; + default: + return 1; + } + } + + /** + * @override + */ + async detectMaxHardwareResolution() { + // In our tests, the original Chromecast seems to have trouble decoding + // above 1080p. It would be a waste to select a higher res anyway, given + // that the device only outputs 1080p to begin with. + // Chromecast has an extension to query the device/display's resolution. + const hasCanDisplayType = window.cast && cast.__platform__ && + cast.__platform__.canDisplayType; + + // Some hub devices can only do 720p. Default to that. + const maxResolution = {width: 1280, height: 720}; + + try { + if (hasCanDisplayType && await cast.__platform__.canDisplayType( + 'video/mp4; codecs="avc1.640028"; width=3840; height=2160')) { + // The device and display can both do 4k. Assume a 4k limit. + maxResolution.width = 3840; + maxResolution.height = 2160; + } else if (hasCanDisplayType && await cast.__platform__.canDisplayType( + 'video/mp4; codecs="avc1.640028"; width=1920; height=1080')) { + // Most Chromecasts can do 1080p. + maxResolution.width = 1920; + maxResolution.height = 1080; + } + } catch (error) { + // This shouldn't generally happen. Log the error. + shaka.log.alwaysError('Failed to check canDisplayType:', error); + // Now ignore the error and let the 720p default stand. + } + + return maxResolution; + } + + /** + * @override + */ + adjustConfig(config) { + super.adjustConfig(config); + // Chromecast has long hardware pipeline that respond slowly to seeking. + // Therefore we should not seek when we detect a stall on this platform. + // Instead, default stallSkip to 0 to force the stall detector to pause + // and play instead. + config.streaming.stallSkip = 0; + return config; + } + + /** + * @override + */ + supportsOfflineStorage() { + return false; + } + + /** + * Check if the current platform is Vizio TV. + * @return {boolean} + * @private + */ + static isChromecast_() { + return navigator.userAgent.includes('CrKey') && + !navigator.userAgent.includes('VIZIO SmartCast'); + } +}; + +/** + * @private + * @enum {string} + */ +shaka.device.Chromecast.OsType_ = { + ANDROID: 'Android', + FUCHSIA: 'Fuchsia', + LINUX: 'Linux', +}; + +if (shaka.device.Chromecast.isChromecast_()) { + shaka.device.DeviceFactory.registerDeviceFactory( + () => new shaka.device.Chromecast()); +} diff --git a/lib/device/default_browser.js b/lib/device/default_browser.js new file mode 100644 index 000000000..9c6418296 --- /dev/null +++ b/lib/device/default_browser.js @@ -0,0 +1,167 @@ +/*! @license + * Shaka Player + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +goog.provide('shaka.device.DefaultBrowser'); + +goog.require('shaka.debug.RunningInLab'); +goog.require('shaka.device.AbstractDevice'); +goog.require('shaka.device.DeviceFactory'); +goog.require('shaka.device.IDevice'); +goog.require('shaka.drm.DrmUtils'); +goog.require('shaka.util.Lazy'); + + +/** + * @final + */ +shaka.device.DefaultBrowser = class extends shaka.device.AbstractDevice { + constructor() { + super(); + + /** @private {!shaka.util.Lazy} */ + this.version_ = new shaka.util.Lazy(() => { + // Looking for something like "Chrome/106.0.0.0" or Firefox/135.0 + const match = navigator.userAgent.match(/(Chrome|Firefox)\/(\d+)/); + if (match) { + return parseInt(match[2], /* base= */ 10); + } + + return null; + }); + + /** @private {!shaka.util.Lazy} */ + this.deviceName_ = new shaka.util.Lazy(() => { + // Legacy Edge contains "Edge/version". + // Chromium-based Edge contains "Edg/version" (no "e"). + if (navigator.userAgent.match(/Edge?\//)) { + return 'Edge'; + } else if (navigator.userAgent.includes('Chrome')) { + return 'Chrome'; + } else if (navigator.userAgent.includes('Firefox')) { + return 'Firefox'; + } + return 'Unknown'; + }); + + /** @private {!shaka.util.Lazy} */ + this.isWindows_ = new shaka.util.Lazy(() => { + // Try the newer standard first. + if (navigator.userAgentData && navigator.userAgentData.platform) { + return navigator.userAgentData.platform.toLowerCase() == 'windows'; + } + // Fall back to the old API, with less strict matching. + if (!navigator.platform) { + return false; + } + return navigator.platform.toLowerCase().includes('win32'); + }); + + /** @private {!shaka.util.Lazy} */ + this.supportsSmoothCodecSwitching_ = new shaka.util.Lazy(() => { + if (!navigator.userAgent.match(/Edge?\//)) { + return true; + } + return !this.isWindows_.value(); + }); + + /** @private {!shaka.util.Lazy} */ + this.isSonyTV_ = new shaka.util.Lazy(() => { + return navigator.userAgent.includes('sony.hbbtv.tv.G5'); + }); + } + + /** + * @override + */ + getVersion() { + return this.version_.value(); + } + + /** + * @override + */ + getDeviceName() { + return this.deviceName_.value(); + } + + /** + * @override + */ + requiresEncryptionInfoInAllInitSegments(keySystem) { + if (shaka.drm.DrmUtils.isPlayReadyKeySystem(keySystem)) { + return this.deviceName_.value() === 'Edge' && this.isWindows_.value(); + } + return false; + } + + /** + * @override + */ + requiresClearAndEncryptedInitSegments() { + return this.deviceName_.value() === 'Edge' && this.isWindows_.value(); + } + + /** + * @override + */ + insertEncryptionDataBeforeClear() { + return this.deviceName_.value() === 'Edge' && this.isWindows_.value(); ; + } + + /** + * @override + */ + supportsSmoothCodecSwitching() { + return this.supportsSmoothCodecSwitching_.value(); + } + + /** + * @override + */ + adjustConfig(config) { + super.adjustConfig(config); + + // Other browsers different than Edge only supports HW PlayReady with the + // recommendation keysystem on Windows, so we do a direct mapping here. + if (this.isWindows_.value() && this.deviceName_.value() !== 'Edge') { + config.drm.keySystemsMapping = { + 'com.microsoft.playready': + 'com.microsoft.playready.recommendation', + }; + } + return config; + } + + /** + * @override + */ + returnLittleEndianUsingPlayReady() { + return this.deviceName_.value() === 'Edge' || this.isSonyTV_.value(); + } + + /** + * @override + */ + createMediaKeysWhenCheckingSupport() { + if (goog.DEBUG && shaka.debug.RunningInLab && this.isWindows_.value() && + this.getBrowserEngine() === shaka.device.IDevice.BrowserEngine.GECKO) { + return false; + } + return true; + } + + /** + * @override + */ + disableHEVCSupport() { + // It seems that HEVC on Firefox Windows is incomplete. + return this.isWindows_.value() && + this.getBrowserEngine() === shaka.device.IDevice.BrowserEngine.GECKO; + } +}; + +shaka.device.DeviceFactory.registerDefaultDeviceFactory( + () => new shaka.device.DefaultBrowser()); diff --git a/lib/device/device_factory.js b/lib/device/device_factory.js new file mode 100644 index 000000000..7f56e5a9b --- /dev/null +++ b/lib/device/device_factory.js @@ -0,0 +1,64 @@ +/*! @license + * Shaka Player + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +goog.provide('shaka.device.DeviceFactory'); + +goog.require('goog.asserts'); +goog.require('shaka.log'); +goog.require('shaka.util.Lazy'); +goog.requireType('shaka.device.IDevice'); + + +shaka.device.DeviceFactory = class { + /** + * @param {?function(): shaka.device.IDevice} deviceFactory + */ + static registerDeviceFactory(deviceFactory) { + goog.asserts.assert(!shaka.device.DeviceFactory.factory_, + 'Device Factory should NOT be defined'); + shaka.device.DeviceFactory.factory_ = deviceFactory; + } + + /** + * @param {?function(): shaka.device.IDevice} deviceFactory + */ + static registerDefaultDeviceFactory(deviceFactory) { + goog.asserts.assert(!shaka.device.DeviceFactory.factory_, + 'Default device Factory should NOT be defined'); + shaka.device.DeviceFactory.defaultFactory_ = deviceFactory; + } + + /** + * @return {shaka.device.IDevice} + */ + static getDevice() { + goog.asserts.assert(shaka.device.DeviceFactory.factory_ || + shaka.device.DeviceFactory.defaultFactory_, + 'Device Factory should be defined'); + return shaka.device.DeviceFactory.device_.value(); + } +}; + +/** @private {?function(): shaka.device.IDevice} */ +shaka.device.DeviceFactory.factory_ = null; + +/** @private {?function(): shaka.device.IDevice} */ +shaka.device.DeviceFactory.defaultFactory_ = null; + +/** @private {!shaka.util.Lazy} */ +shaka.device.DeviceFactory.device_ = new shaka.util.Lazy(() => { + let device = undefined; + if (shaka.device.DeviceFactory.factory_) { + device = shaka.device.DeviceFactory.factory_(); + } + if (!device && shaka.device.DeviceFactory.defaultFactory_) { + device = shaka.device.DeviceFactory.defaultFactory_(); + } + if (device) { + shaka.log.info(device.toString()); + } + return device; +}); diff --git a/lib/device/hisense.js b/lib/device/hisense.js new file mode 100644 index 000000000..80a081422 --- /dev/null +++ b/lib/device/hisense.js @@ -0,0 +1,100 @@ +/*! @license + * Shaka Player + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +goog.provide('shaka.device.Hisense'); + +goog.require('shaka.device.AbstractDevice'); +goog.require('shaka.device.DeviceFactory'); +goog.require('shaka.device.IDevice'); +goog.require('shaka.log'); + + +/** + * @final + */ +shaka.device.Hisense = class extends shaka.device.AbstractDevice { + /** + * @override + */ + getVersion() { + return null; + } + + /** + * @override + */ + getDeviceName() { + return 'Hisense'; + } + + /** + * @override + */ + getDeviceType() { + return shaka.device.IDevice.DeviceType.TV; + } + + /** + * @override + */ + supportsMediaCapabilities() { + return false; + } + + /** + * @override + */ + detectMaxHardwareResolution() { + const maxResolution = {width: 1920, height: 1080}; + let supports4k = null; + if (window.Hisense_Get4KSupportState) { + try { + // eslint-disable-next-line new-cap + supports4k = window.Hisense_Get4KSupportState(); + } catch (e) { + shaka.log.debug('Hisense: Failed to get 4K support state', e); + } + } + if (supports4k == null) { + // If API is not there or not working for whatever reason, fallback to + // user agent check, as it contains UHD or FHD info. + supports4k = navigator.userAgent.includes('UHD'); + } + if (supports4k) { + maxResolution.width = 3840; + maxResolution.height = 2160; + } + + return Promise.resolve(maxResolution); + } + + /** + * @override + */ + adjustConfig(config) { + super.adjustConfig(config); + // Hisense has long hardware pipeline that respond slowly to seeking. + // Therefore we should not seek when we detect a stall on this platform. + // Instead, default stallSkip to 0 to force the stall detector to pause + // and play instead. + config.streaming.stallSkip = 0; + return config; + } + + /** + * @return {boolean} + * @private + */ + static isHisense_() { + return navigator.userAgent.includes('Hisense') || + navigator.userAgent.includes('VIDAA'); + } +}; + +if (shaka.device.Hisense.isHisense_()) { + shaka.device.DeviceFactory.registerDeviceFactory( + () => new shaka.device.Hisense()); +} diff --git a/lib/device/i_device.js b/lib/device/i_device.js new file mode 100644 index 000000000..169f59e50 --- /dev/null +++ b/lib/device/i_device.js @@ -0,0 +1,250 @@ +/*! @license + * Shaka Player + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +goog.provide('shaka.device.IDevice'); + + +/** + * @interface + */ +shaka.device.IDevice = class { + /** + * Check if the current platform supports media source. We assume that if + * the current platform supports media source, then we can use media source + * as per its design. + * + * @return {boolean} + */ + supportsMediaSource() {} + + /** + * Returns true if the media type is supported natively by the platform. + * + * @param {string} mimeType + * @return {boolean} + */ + supportsMediaType(mimeType) {} + + /** + * @return {boolean} + */ + supportsMediaCapabilities() {} + + /** + * Version of device or null if unknown. + * @return {?number} + */ + getVersion() {} + + /** + * Friendly device name. + * @return {string} + */ + getDeviceName() {} + + /** + * @return {!shaka.device.IDevice.DeviceType} + */ + getDeviceType() {} + + /** + * @return {!shaka.device.IDevice.BrowserEngine} + */ + getBrowserEngine() {} + + /** + * Returns true if the platform requires encryption information in all init + * segments. For such platforms, MediaSourceEngine will attempt to work + * around a lack of such info by inserting fake encryption information into + * initialization segments. + * + * @param {?string} keySystem + * @param {?string} contentType + * @return {boolean} + * @see https://github.com/shaka-project/shaka-player/issues/2759 + */ + requiresEncryptionInfoInAllInitSegments(keySystem, contentType) {} + + /** + * Returns true if the platform requires both clear & encryption information + * in clear init segments. For such platforms, MediaSourceEngine will attempt + * to work around a lack of such info by inserting fake information into + * initialization segments. It is called only when + * requiresEncryptionInfoInAllInitSegments() is also true + * and works as the extension of it. + * + * @return {boolean} + * @see https://github.com/shaka-project/shaka-player/pull/6719 + */ + requiresClearAndEncryptedInitSegments() {} + + /** + * Indicates should the encryption data be inserted before or after + * the clear data in the init segment. + * @return {boolean} + */ + insertEncryptionDataBeforeClear() {} + + /** + * @param {string} contentType + * @return {boolean} + */ + requiresTfhdFix(contentType) {} + + /** + * Returns true if the platform requires AC-3 signalling in init + * segments to be replaced with EC-3 signalling. + * For such platforms, MediaSourceEngine will attempt to work + * around it by inserting fake EC-3 signalling into + * initialization segments. + * + * @return {boolean} + */ + requiresEC3InitSegments() {} + + /** + * Returns true if the platform supports SourceBuffer "sequence mode". + * + * @return {boolean} + */ + supportsSequenceMode() {} + + /** + * Returns if codec switching SMOOTH is known reliable device support. + * + * Some devices are known not to support SourceBuffer.changeType + * well. These devices should use the reload strategy. If a device + * reports that it supports but supports it unreliably + * it should be disallowed in this method. + * + * @return {boolean} + */ + supportsSmoothCodecSwitching() {} + + /** + * On some platforms, the act of seeking can take a significant amount + * of time, so we need to delay a seek. + * @return {number} + */ + seekDelay() {} + + /** + * Detect the maximum resolution that the platform's hardware can handle. + * + * @return {!Promise} + */ + detectMaxHardwareResolution() {} + + /** + * @return {boolean} + */ + supportsServerCertificate() {} + + /** + * Adjusts player configuration with device specific tweaks. Changes are done + * in-place and the same object is returned. + * @param {shaka.extern.PlayerConfiguration} config + * @return {shaka.extern.PlayerConfiguration} + */ + adjustConfig(config) {} + + /** + * Checks should Dolby Vision codecs be overridden to their H.264 and H.265 + * equivalents. + * @return {boolean} + */ + shouldOverrideDolbyVisionCodecs() {} + + /** + * Indicates whether or not to use window.TextDecoder and window.TextEncoder + * even if they are available + * @return {boolean} + */ + shouldAvoidUseTextDecoderEncoder() {} + + /** + * Checks does the platform supports offline storage by IDB. + * @return {boolean} + */ + supportsOfflineStorage() {} + + /** + * Lists all codecs that should be rejected by MediaSource. + * @return {!Array} + */ + rejectCodecs() {} + + /** + * Check the current HDR level supported by the screen. + * + * @param {boolean} preferHLG + * @return {string} + */ + getHdrLevel(preferHLG) {} + + /** + * @return {boolean} + */ + supportsAirPlay() {} + + /** + * @return {boolean} + */ + misreportAC3UsingDrm() {} + + /** + * @return {boolean} + */ + returnLittleEndianUsingPlayReady() {} + + /** + * @return {boolean} + */ + supportsEncryptionSchemePolyfill() {} + + /** + * @return {boolean} + */ + misreportsSupportForPersistentLicenses() {} + + /** + * @return {boolean} + */ + supportStandardVP9Checking() {} + + /** + * @return {boolean} + */ + createMediaKeysWhenCheckingSupport() {} + + /** + * @return {boolean} + */ + disableHEVCSupport() {} +}; + +/** + * @enum {string} + */ +shaka.device.IDevice.DeviceType = { + 'DESKTOP': 'DESKTOP', + 'MOBILE': 'MOBILE', + 'TV': 'TV', + 'VR': 'VR', + 'CONSOLE': 'CONSOLE', + 'CAST': 'CAST', +}; + +/** + * @enum {string} + */ +shaka.device.IDevice.BrowserEngine = { + 'CHROMIUM': 'CHROMIUM', + 'EDGE': 'EDGE', + 'GECKO': 'GECKO', + 'WEBKIT': 'WEBKIT', + 'UNKNOWN': 'UNKNOWN', +}; diff --git a/lib/device/playstation.js b/lib/device/playstation.js new file mode 100644 index 000000000..ed8df78f5 --- /dev/null +++ b/lib/device/playstation.js @@ -0,0 +1,155 @@ +/*! @license + * Shaka Player + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +goog.provide('shaka.device.PlayStation'); + +goog.require('shaka.device.AbstractDevice'); +goog.require('shaka.device.DeviceFactory'); +goog.require('shaka.device.IDevice'); +goog.require('shaka.log'); +goog.require('shaka.util.Lazy'); + + +/** + * @final + */ +shaka.device.PlayStation = class extends shaka.device.AbstractDevice { + constructor() { + super(); + + /** @private {!shaka.util.Lazy} */ + this.version_ = new shaka.util.Lazy(() => { + const match = navigator.userAgent.match(/PlayStation (\d+)/); + if (match) { + return parseInt(match[1], 10); + } + return null; + }); + } + + /** + * @override + */ + getDeviceName() { + return 'PlayStation'; + } + + /** + * @override + */ + getDeviceType() { + return shaka.device.IDevice.DeviceType.CONSOLE; + } + + /** + * @override + */ + getBrowserEngine() { + return shaka.device.IDevice.BrowserEngine.WEBKIT; + } + + /** + * @override + */ + getVersion() { + return this.version_.value(); + } + + /** + * @override + */ + supportsMediaCapabilities() { + return false; + } + + /** + * @override + */ + supportsSequenceMode() { + return false; + } + + /** + * @override + */ + supportsSmoothCodecSwitching() { + return false; + } + + /** + * @override + */ + shouldAvoidUseTextDecoderEncoder() { + return this.getVersion() === 4; + } + + /** + * @override + */ + async detectMaxHardwareResolution() { + const maxResolution = {width: 1920, height: 1080}; + let supports4K = false; + try { + const result = await window.msdk.device.getDisplayInfo(); + supports4K = result.resolution === '4K'; + } catch (e) { + try { + const result = await window.msdk.device.getDisplayInfoImmediate(); + supports4K = result.resolution === '4K'; + } catch (e) { + shaka.log.alwaysWarn( + 'PlayStation: Failed to get the display info:', e); + } + } + if (supports4K) { + maxResolution.width = 3840; + maxResolution.height = 2160; + } + + return maxResolution; + } + + /** + * @override + */ + adjustConfig(config) { + super.adjustConfig(config); + // The PS4 only supports the Playready DRM, so it should + // prefer that key system by default to improve startup performance. + if (this.getVersion() === 4) { + config.drm.preferredKeySystems.push('com.microsoft.playready'); + } + config.streaming.clearDecodingCache = true; + return config; + } + + /** + * @override + */ + returnLittleEndianUsingPlayReady() { + return this.getVersion() === 4; + } + + /** + * @override + */ + supportsEncryptionSchemePolyfill() { + return this.getVersion() !== 4; + } + + /** + * @return {boolean} + * @private + */ + static isPlayStation_() { + return navigator.userAgent.includes('PlayStation'); + } +}; + +if (shaka.device.PlayStation.isPlayStation_()) { + shaka.device.DeviceFactory.registerDeviceFactory( + () => new shaka.device.PlayStation()); +} diff --git a/lib/device/tizen.js b/lib/device/tizen.js new file mode 100644 index 000000000..223de7b5d --- /dev/null +++ b/lib/device/tizen.js @@ -0,0 +1,202 @@ +/*! @license + * Shaka Player + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +goog.provide('shaka.device.Tizen'); + +goog.require('shaka.config.CrossBoundaryStrategy'); +goog.require('shaka.device.AbstractDevice'); +goog.require('shaka.device.DeviceFactory'); +goog.require('shaka.device.IDevice'); +goog.require('shaka.log'); + + +/** + * @final + */ +shaka.device.Tizen = class extends shaka.device.AbstractDevice { + constructor() { + super(); + + const match = navigator.userAgent.match(/Tizen (\d+).(\d+)/); + + /** @private {?number} */ + this.osMajorVersion_ = match ? parseInt(match[1], 10) : null; + + /** @private {?number} */ + this.osMinorVersion_ = match ? parseInt(match[2], 10) : null; + } + + /** + * @override + */ + getVersion() { + return this.osMajorVersion_; + } + + /** + * @override + */ + getDeviceName() { + return 'Tizen'; + } + + /** + * @override + */ + getBrowserEngine() { + return shaka.device.IDevice.BrowserEngine.CHROMIUM; + } + + /** + * @override + */ + getDeviceType() { + return shaka.device.IDevice.DeviceType.TV; + } + + /** + * @override + */ + requiresEncryptionInfoInAllInitSegments(keySystem) { + return true; + } + + /** + * @override + */ + requiresEC3InitSegments() { + return this.getVersion() === 3; + } + + /** + * @override + */ + supportsMediaCapabilities() { + return false; + } + + /** + * @override + */ + supportsSequenceMode() { + const version = this.getVersion(); + return version !== null ? version >= 4 : super.supportsSequenceMode(); + } + + /** + * @override + */ + supportsSmoothCodecSwitching() { + return false; + } + + /** + * @override + */ + supportsServerCertificate() { + // Tizen 5.0 and earlier do not support server certificates. + if (!this.osMajorVersion_ || !this.osMinorVersion_) { + return super.supportsServerCertificate(); + } + if (this.osMajorVersion_ === 5) { + return this.osMinorVersion_ >= 5; + } + return this.osMajorVersion_ > 5; + } + + /** + * @override + */ + detectMaxHardwareResolution() { + const maxResolution = {width: 1920, height: 1080}; + try { + if (webapis.systeminfo && webapis.systeminfo.getMaxVideoResolution) { + const maxVideoResolution = + webapis.systeminfo.getMaxVideoResolution(); + maxResolution.width = maxVideoResolution.width; + maxResolution.height = maxVideoResolution.height; + } else { + if (webapis.productinfo.is8KPanelSupported && + webapis.productinfo.is8KPanelSupported()) { + maxResolution.width = 7680; + maxResolution.height = 4320; + } else if (webapis.productinfo.isUdPanelSupported && + webapis.productinfo.isUdPanelSupported()) { + maxResolution.width = 3840; + maxResolution.height = 2160; + } + } + } catch (e) { + shaka.log.alwaysWarn('Tizen: Error detecting screen size, default ' + + 'screen size 1920x1080.'); + } + + return Promise.resolve(maxResolution); + } + + /** + * @override + */ + adjustConfig(config) { + super.adjustConfig(config); + + config.drm.ignoreDuplicateInitData = this.getVersion() !== 2; + + if (this.getVersion() === 3) { + config.streaming.crossBoundaryStrategy = + shaka.config.CrossBoundaryStrategy.RESET; + } + config.streaming.shouldFixTimestampOffset = true; + // Tizen has long hardware pipeline that respond slowly to seeking. + // Therefore we should not seek when we detect a stall on this platform. + // Instead, default stallSkip to 0 to force the stall detector to pause + // and play instead. + config.streaming.stallSkip = 0; + config.streaming.gapPadding = 2; + return config; + } + + /** + * @override + */ + rejectCodecs() { + // Tizen's implementation of MSE does not work well with opus. To prevent + // the player from trying to play opus on Tizen, we will override media + // source to always reject opus content. + const codecs = []; + if (this.osMajorVersion_ !== null && this.osMajorVersion_ < 5) { + codecs.push('opus'); + } + return codecs; + } + + /** + * @override + */ + misreportAC3UsingDrm() { + return true; + } + + /** + * @override + */ + misreportsSupportForPersistentLicenses() { + return this.getVersion() === 3; + } + + /** + * @return {boolean} + * @private + */ + static isTizen_() { + return navigator.userAgent.includes('Tizen'); + } +}; + +if (shaka.device.Tizen.isTizen_()) { + shaka.device.DeviceFactory.registerDeviceFactory( + () => new shaka.device.Tizen()); +} diff --git a/lib/device/vizio.js b/lib/device/vizio.js new file mode 100644 index 000000000..1c8f10749 --- /dev/null +++ b/lib/device/vizio.js @@ -0,0 +1,59 @@ +/*! @license + * Shaka Player + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +goog.provide('shaka.device.Vizio'); + +goog.require('shaka.device.AbstractDevice'); +goog.require('shaka.device.DeviceFactory'); +goog.require('shaka.device.IDevice'); + + +/** + * @final + */ +shaka.device.Vizio = class extends shaka.device.AbstractDevice { + /** + * @override + */ + getVersion() { + return null; + } + + /** + * @override + */ + getDeviceName() { + return 'Vizio'; + } + + /** + * @override + */ + getDeviceType() { + return shaka.device.IDevice.DeviceType.TV; + } + + /** + * @override + */ + supportsMediaCapabilities() { + return false; + } + + /** + * Check if the current platform is Vizio TV. + * @return {boolean} + * @private + */ + static isVizio_() { + return navigator.userAgent.includes('VIZIO SmartCast'); + } +}; + +if (shaka.device.Vizio.isVizio_()) { + shaka.device.DeviceFactory.registerDeviceFactory( + () => new shaka.device.Vizio()); +} diff --git a/lib/device/webkit_stb.js b/lib/device/webkit_stb.js new file mode 100644 index 000000000..1e145582f --- /dev/null +++ b/lib/device/webkit_stb.js @@ -0,0 +1,146 @@ +/*! @license + * Shaka Player + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +goog.provide('shaka.device.WebKitSTB'); + +goog.require('shaka.device.AbstractDevice'); +goog.require('shaka.device.DeviceFactory'); +goog.require('shaka.device.IDevice'); +goog.require('shaka.util.Lazy'); + + +/** + * @final + */ +shaka.device.WebKitSTB = class extends shaka.device.AbstractDevice { + constructor() { + super(); + + /** + * SkyQ STB + * + * @private {!shaka.util.Lazy} + */ + this.isSkyQ_ = new shaka.util.Lazy(() => { + return navigator.userAgent.includes('DT_STB_BCM'); + }); + + /** @private {!shaka.util.Lazy} */ + this.version_ = new shaka.util.Lazy(() => { + if (navigator.userAgent.includes('DT_STB_BCM')) { + return 11; + } + // This works for iOS Safari and desktop Safari, which contain something + // like "Version/13.0" indicating the major Safari or iOS version. + let match = navigator.userAgent.match(/Version\/(\d+)/); + if (match) { + return parseInt(match[1], /* base= */ 10); + } + + // This works for all other browsers on iOS, which contain something like + // "OS 13_3" indicating the major & minor iOS version. + match = navigator.userAgent.match(/OS (\d+)(?:_\d+)?/); + if (match) { + return parseInt(match[1], /* base= */ 10); + } + + return null; + }); + } + + /** + * @override + */ + getVersion() { + return this.version_.value(); + } + + /** + * @override + */ + getDeviceName() { + return 'WebKit STB'; + } + + /** + * @override + */ + getDeviceType() { + return shaka.device.IDevice.DeviceType.TV; + } + + /** + * @override + */ + getBrowserEngine() { + return shaka.device.IDevice.BrowserEngine.WEBKIT; + } + + /** + * @override + */ + supportsMediaCapabilities() { + return false; + } + + /** + * @override + */ + supportsSequenceMode() { + // See: https://bugs.webkit.org/show_bug.cgi?id=210341 + const version = this.version_.value(); + return version !== null ? version >= 15 : true; + } + + /** + * @override + */ + detectMaxHardwareResolution() { + const maxResolution = { + width: window.screen.width * window.devicePixelRatio, + height: window.screen.height * window.devicePixelRatio, + }; + return Promise.resolve(maxResolution); + } + + /** + * @override + */ + supportsEncryptionSchemePolyfill() { + return !this.isSkyQ_.value(); + } + + /** + * @return {boolean} + * @private + */ + static isWebkitSTB_() { + if (navigator.userAgent.includes('DT_STB_BCM') || + navigator.userAgent.includes('DT_STB_BCM')) { + return true; + } + if (!(navigator.vendor || '').includes('Apple')) { + return false; + } + if (/(?:iPhone|iPad|iPod)/.test(navigator.userAgent) || + navigator.maxTouchPoints > 1) { + return false; + } + if (navigator.userAgentData && navigator.userAgentData.platform && + navigator.userAgentData.platform.toLowerCase() == 'macos') { + return false; + } else if (navigator.platform && + navigator.platform.toLowerCase().includes('mac')) { + return false; + } + return true; + } +}; + +if (shaka.device.WebKitSTB.isWebkitSTB_()) { + shaka.device.DeviceFactory.registerDeviceFactory( + () => new shaka.device.WebKitSTB()); +} diff --git a/lib/device/webos.js b/lib/device/webos.js new file mode 100644 index 000000000..8f1bbacaa --- /dev/null +++ b/lib/device/webos.js @@ -0,0 +1,178 @@ +/*! @license + * Shaka Player + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +goog.provide('shaka.device.WebOS'); + +goog.require('shaka.config.CrossBoundaryStrategy'); +goog.require('shaka.device.AbstractDevice'); +goog.require('shaka.device.DeviceFactory'); +goog.require('shaka.device.IDevice'); +goog.require('shaka.log'); + + +/** + * @final + */ +shaka.device.WebOS = class extends shaka.device.AbstractDevice { + constructor() { + super(); + + /** @private {?number} */ + this.osVersion_ = this.guessWebOSVersion_(); + } + + /** + * @override + */ + getVersion() { + return this.osVersion_; + } + + /** + * @override + */ + getDeviceName() { + return 'WebOS'; + } + + /** + * @override + */ + getDeviceType() { + return shaka.device.IDevice.DeviceType.TV; + } + + /** + * @override + */ + getBrowserEngine() { + return shaka.device.IDevice.BrowserEngine.CHROMIUM; + } + + /** + * @override + */ + supportsMediaCapabilities() { + return false; + } + + /** + * @override + */ + supportsSequenceMode() { + const version = this.getVersion(); + return version !== null ? version > 3 : super.supportsSequenceMode(); + } + + /** + * @override + */ + supportsSmoothCodecSwitching() { + const version = this.getVersion(); + return version !== null ? + version > 6 : super.supportsSmoothCodecSwitching(); + } + + /** + * @override + */ + supportsServerCertificate() { + const version = this.getVersion(); + return version !== null ? version > 3 : super.supportsServerCertificate(); + } + + /** + * @override + */ + detectMaxHardwareResolution() { + const maxResolution = {width: 1920, height: 1080}; + try { + const deviceInfo = + /** @type {{screenWidth: number, screenHeight: number}} */( + JSON.parse(window.PalmSystem.deviceInfo)); + // WebOS has always been able to do 1080p. Assume a 1080p limit. + maxResolution.width = Math.max(1920, deviceInfo['screenWidth']); + maxResolution.height = Math.max(1080, deviceInfo['screenHeight']); + } catch (e) { + shaka.log.alwaysWarn('WebOS: Error detecting screen size, default ' + + 'screen size 1920x1080.'); + } + + return Promise.resolve(maxResolution); + } + + /** + * @override + */ + adjustConfig(config) { + super.adjustConfig(config); + + if (this.getVersion() === 3) { + config.streaming.crossBoundaryStrategy = + shaka.config.CrossBoundaryStrategy.RESET; + } + config.streaming.shouldFixTimestampOffset = true; + // WebOS has long hardware pipeline that respond slowly to seeking. + // Therefore we should not seek when we detect a stall on this platform. + // Instead, default stallSkip to 0 to force the stall detector to pause + // and play instead. + config.streaming.stallSkip = 0; + return config; + } + + /** + * @return {?number} + * @private + */ + guessWebOSVersion_() { + let browserVersion = null; + const match = navigator.userAgent.match(/Chrome\/(\d+)/); + if (match) { + browserVersion = parseInt(match[1], /* base= */ 10); + } + + switch (browserVersion) { + case 38: + return 3; + case 53: + return 4; + case 68: + return 5; + case 79: + return 6; + case 87: + return 22; + case 94: + return 23; + case 108: + return 24; + case 120: + return 25; + default: + return null; + } + } + + /** + * @override + */ + supportStandardVP9Checking() { + return false; + } + + /** + * @return {boolean} + * @private + */ + static isWebOS_() { + return navigator.userAgent.includes('Web0S'); + } +}; + +if (shaka.device.WebOS.isWebOS_()) { + shaka.device.DeviceFactory.registerDeviceFactory( + () => new shaka.device.WebOS()); +} diff --git a/lib/device/xbox.js b/lib/device/xbox.js new file mode 100644 index 000000000..054a18c0c --- /dev/null +++ b/lib/device/xbox.js @@ -0,0 +1,182 @@ +/*! @license + * Shaka Player + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +goog.provide('shaka.device.Xbox'); + +goog.require('shaka.device.AbstractDevice'); +goog.require('shaka.device.DeviceFactory'); +goog.require('shaka.device.IDevice'); +goog.require('shaka.log'); + + +/** + * @final + */ +shaka.device.Xbox = class extends shaka.device.AbstractDevice { + constructor() { + super(); + + /** @private {boolean} */ + this.isLegacyEdge_ = navigator.userAgent.includes('Edge/'); + + // Looking for something like "Edg/106.0.0.0". + const match = navigator.userAgent.match(/Edge?\/(\d+)/); + + /** @private {?number} */ + this.version_ = match ? parseInt(match[1], /* base= */ 10) : null; + } + + /** + * @override + */ + getVersion() { + return this.version_; + } + + /** + * @override + */ + getDeviceName() { + return 'Xbox'; + } + + /** + * @override + */ + getDeviceType() { + return shaka.device.IDevice.DeviceType.CONSOLE; + } + + /** + * @override + */ + getBrowserEngine() { + return this.isLegacyEdge_ ? shaka.device.IDevice.BrowserEngine.EDGE : + shaka.device.IDevice.BrowserEngine.CHROMIUM; + } + + /** + * @override + */ + requiresEncryptionInfoInAllInitSegments(keySystem) { + return true; + } + + /** + * @override + */ + insertEncryptionDataBeforeClear() { + return true; + } + + /** + * @override + */ + shouldOverrideDolbyVisionCodecs() { + return this.isLegacyEdge_; + } + + /** + * @override + */ + detectMaxHardwareResolution() { + const maxResolution = {width: 1920, height: 1080}; + const winRT = shaka.device.Xbox.getWinRT_(); + + if (winRT) { + try { + const protectionCapabilities = + new winRT.Media.Protection.ProtectionCapabilities(); + const protectionResult = + winRT.Media.Protection.ProtectionCapabilityResult; + // isTypeSupported may return "maybe", which means the operation + // is not completed. This means we need to retry + // https://learn.microsoft.com/en-us/uwp/api/windows.media.protection.protectioncapabilityresult?view=winrt-22621 + let result = null; + const type = + 'video/mp4;codecs="hvc1,mp4a";features="decode-res-x=3840,' + + 'decode-res-y=2160,decode-bitrate=20000,decode-fps=30,' + + 'decode-bpc=10,display-res-x=3840,display-res-y=2160,' + + 'display-bpc=8"'; + const keySystem = 'com.microsoft.playready.recommendation'; + do { + result = protectionCapabilities.isTypeSupported(type, keySystem); + } while (result === protectionResult.maybe); + if (result === protectionResult.probably) { + maxResolution.width = 3840; + maxResolution.height = 2160; + } + } catch (e) { + shaka.log.alwaysWarn('Xbox: Error detecting screen size, default ' + + 'screen size 1920x1080.'); + } + } + + return Promise.resolve(maxResolution); + } + + /** + * @override + */ + adjustConfig(config) { + super.adjustConfig(config); + + // The Xbox One browser does not detect DRM key changes signalled by a + // change in the PSSH in media segments. We need to parse PSSH from media + // segments to detect key changes. + config.drm.parseInbandPsshEnabled = this.isLegacyEdge_; + // The Xbox only supports the Playready DRM, so it should + // prefer that key system by default to improve startup performance. + config.drm.preferredKeySystems.push('com.microsoft.playready'); + if (this.isLegacyEdge_) { + config.streaming.gapPadding = 0.01; + } + return config; + } + + /** + * @override + */ + supportsOfflineStorage() { + return false; + } + + + /** + * @return {?WinRT} + * @private + */ + static getWinRT_() { + let winRT = null; + try { + // Try to access to WinRT for WebView, if it's not defined, + // try to access to WinRT for WebView2, if it's not defined either, + // let it throw. + if (typeof Windows !== 'undefined') { + winRT = Windows; + } else { + winRT = chrome.webview.hostObjects.sync.Windows; + } + } catch (e) {} + return winRT; + } + + /** + * Check if the current platform is an Xbox One. + * + * @return {boolean} + * @private + */ + static isXbox_() { + return navigator.userAgent.includes('Xbox One') || + shaka.device.Xbox.getWinRT_() !== null; + } +}; + +if (shaka.device.Xbox.isXbox_()) { + shaka.device.DeviceFactory.registerDeviceFactory( + () => new shaka.device.Xbox()); +} diff --git a/lib/drm/drm_engine.js b/lib/drm/drm_engine.js index 885e8d4ab..25b7887ba 100644 --- a/lib/drm/drm_engine.js +++ b/lib/drm/drm_engine.js @@ -7,8 +7,9 @@ goog.provide('shaka.drm.DrmEngine'); goog.require('goog.asserts'); -goog.require('shaka.debug.RunningInLab'); goog.require('shaka.log'); +goog.require('shaka.device.DeviceFactory'); +goog.require('shaka.device.IDevice'); goog.require('shaka.drm.DrmUtils'); goog.require('shaka.net.NetworkingEngine'); goog.require('shaka.util.ArrayUtils'); @@ -23,7 +24,6 @@ goog.require('shaka.util.Iterables'); goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.util.MapUtils'); goog.require('shaka.util.ObjectUtils'); -goog.require('shaka.util.Platform'); goog.require('shaka.util.Pssh'); goog.require('shaka.util.PublicPromise'); goog.require('shaka.util.StreamUtils'); @@ -1725,12 +1725,11 @@ shaka.drm.DrmEngine = class { // NOTE that we skip this if byteLength != 16. This is used for Edge // which uses single-byte dummy key IDs. // However, unlike Edge and Chromecast, Tizen doesn't have this problem. + const device = shaka.device.DeviceFactory.getDevice(); if (shaka.drm.DrmUtils.isPlayReadyKeySystem( this.currentDrmInfo_.keySystem) && keyId.byteLength == 16 && - (shaka.util.Platform.isEdge() || - shaka.util.Platform.isPS4() || - shaka.util.Platform.isSonyTV())) { + device.returnLittleEndianUsingPlayReady()) { // Read out some fields in little-endian: const dataView = shaka.util.BufferUtils.toDataView(keyId); const part0 = dataView.getUint32(0, /* LE= */ true); @@ -1906,6 +1905,8 @@ shaka.drm.DrmEngine = class { /** @type {!Map} */ const support = new Map(); + const device = shaka.device.DeviceFactory.getDevice(); + /** * @param {string} keySystem * @param {MediaKeySystemAccess} access @@ -1917,10 +1918,7 @@ shaka.drm.DrmEngine = class { // Workaround: Our automated test lab runs Windows browsers under a // headless service. In this environment, Firefox's CDMs seem to crash // when we create the CDM here. - if (goog.DEBUG && // not a production build - shaka.util.Platform.isWindows() && // on Windows - shaka.util.Platform.isFirefox() && // with Firefox - shaka.debug.RunningInLab) { // in our headless lab + if (!device.createMediaKeysWhenCheckingSupport()) { // Reject this, since it crashes our tests. throw new Error('Suppressing Firefox Windows DRM in testing!'); } else { @@ -1944,7 +1942,7 @@ shaka.drm.DrmEngine = class { // does. It doesn't fail until you call update() with a license // response, which is way too late. // This is a work-around for #894. - if (shaka.util.Platform.isTizen3()) { + if (device.misreportsSupportForPersistentLicenses()) { persistentState = false; } @@ -2031,7 +2029,8 @@ shaka.drm.DrmEngine = class { // is not set. This is a workaround for that issue. const TIMEOUT_FOR_CHECK_ACCESS_IN_SECONDS = 5; let access; - if (shaka.util.Platform.isAndroid()) { + const device = shaka.device.DeviceFactory.getDevice(); + if (device.getDeviceType() == shaka.device.IDevice.DeviceType.MOBILE) { access = await shaka.util.Functional.promiseWithTimeout( TIMEOUT_FOR_CHECK_ACCESS_IN_SECONDS, @@ -2080,7 +2079,8 @@ shaka.drm.DrmEngine = class { // is not set. This is a workaround for that issue. const TIMEOUT_FOR_DECODING_INFO_IN_SECONDS = 5; let decodingInfo; - if (shaka.util.Platform.isAndroid()) { + const device = shaka.device.DeviceFactory.getDevice(); + if (device.getDeviceType() == shaka.device.IDevice.DeviceType.MOBILE) { decodingInfo = await shaka.util.Functional.promiseWithTimeout( TIMEOUT_FOR_DECODING_INFO_IN_SECONDS, @@ -2110,8 +2110,10 @@ shaka.drm.DrmEngine = class { // MediaKeySystemAccess for the ClearKey CDM, create and update a key // session but playback will never start // Safari bug: https://bugs.webkit.org/show_bug.cgi?id=231006 - if (shaka.drm.DrmUtils.isClearKeySystem(keySystem) && - shaka.util.Platform.isApple()) { + const device = shaka.device.DeviceFactory.getDevice(); + if (device.getBrowserEngine() === + shaka.device.IDevice.BrowserEngine.WEBKIT && + shaka.drm.DrmUtils.isClearKeySystem(keySystem)) { return false; } return true; diff --git a/lib/media/content_workarounds.js b/lib/media/content_workarounds.js index 548a0e856..fe829c7f9 100644 --- a/lib/media/content_workarounds.js +++ b/lib/media/content_workarounds.js @@ -7,13 +7,13 @@ goog.provide('shaka.media.ContentWorkarounds'); goog.require('goog.asserts'); +goog.require('shaka.device.DeviceFactory'); goog.require('shaka.log'); goog.require('shaka.util.BufferUtils'); goog.require('shaka.util.Error'); goog.require('shaka.util.Mp4BoxParsers'); goog.require('shaka.util.Mp4Generator'); goog.require('shaka.util.Mp4Parser'); -goog.require('shaka.util.Platform'); goog.require('shaka.util.Uint8ArrayUtils'); @@ -194,8 +194,8 @@ shaka.media.ContentWorkarounds = class { // patched one, otherwise video element throws following error: // CHUNK_DEMUXER_ERROR_APPEND_FAILED: Sample encryption info is not // available. - if (shaka.util.Platform.isEdge() && shaka.util.Platform.isWindows() && - !shaka.util.Platform.isXboxOne()) { + const device = shaka.device.DeviceFactory.getDevice(); + if (device.requiresClearAndEncryptedInitSegments()) { const doubleInitSegment = new Uint8Array(initSegment.byteLength + modifiedInitSegment.byteLength); doubleInitSegment.set(modifiedInitSegment); @@ -340,10 +340,10 @@ shaka.media.ContentWorkarounds = class { // For other platforms, we cut and insert at the end of the source box. It's // not clear why this is necessary on Xbox One, but it seems to be evidence // of another bug in the firmware implementation of MediaSource & EME. - const cutPoint = (shaka.util.Platform.isApple() || - shaka.util.Platform.isXboxOne() || shaka.util.Platform.isEdge()) ? - sourceBox.start : - sourceBox.start + sourceBox.size; + const device = shaka.device.DeviceFactory.getDevice(); + const cutPoint = device.insertEncryptionDataBeforeClear() ? + sourceBox.start : + sourceBox.start + sourceBox.size; // The data before the cut point will be copied to the same location as // before. The data after that will be appended after the added metadata diff --git a/lib/media/manifest_parser.js b/lib/media/manifest_parser.js index 8f75dc1b0..17bca48aa 100644 --- a/lib/media/manifest_parser.js +++ b/lib/media/manifest_parser.js @@ -7,9 +7,9 @@ goog.provide('shaka.media.ManifestParser'); goog.require('shaka.Deprecate'); +goog.require('shaka.device.DeviceFactory'); goog.require('shaka.log'); goog.require('shaka.util.Error'); -goog.require('shaka.util.Platform'); // TODO: revisit this when Closure Compiler supports partially-exported classes. @@ -67,7 +67,8 @@ shaka.media.ManifestParser = class { // Make sure all registered parsers are shown, but only for MSE-enabled // platforms where our parsers matter. - if (shaka.util.Platform.supportsMediaSource()) { + const device = shaka.device.DeviceFactory.getDevice(); + if (device.supportsMediaSource()) { for (const type of ManifestParser.parsersByMime.keys()) { support[type] = true; } @@ -88,10 +89,10 @@ shaka.media.ManifestParser = class { for (const type of testMimeTypes) { // Only query our parsers for MSE-enabled platforms. Otherwise, query a // temporary media element for native support for these types. - if (shaka.util.Platform.supportsMediaSource()) { + if (device.supportsMediaSource()) { support[type] = ManifestParser.parsersByMime.has(type); } else { - support[type] = shaka.util.Platform.supportsMediaType(type); + support[type] = device.supportsMediaType(type); } } @@ -141,7 +142,8 @@ shaka.media.ManifestParser = class { */ static isSupported(mimeType) { // Without MediaSource, our own parsers are useless. - if (!shaka.util.Platform.supportsMediaSource()) { + const device = shaka.device.DeviceFactory.getDevice(); + if (!device.supportsMediaSource()) { return false; } diff --git a/lib/media/media_source_engine.js b/lib/media/media_source_engine.js index b53f4c9f6..17561a522 100644 --- a/lib/media/media_source_engine.js +++ b/lib/media/media_source_engine.js @@ -9,6 +9,7 @@ goog.provide('shaka.media.MediaSourceEngine'); goog.require('goog.asserts'); goog.require('shaka.log'); goog.require('shaka.config.CodecSwitchingStrategy'); +goog.require('shaka.device.DeviceFactory'); goog.require('shaka.media.Capabilities'); goog.require('shaka.media.ContentWorkarounds'); goog.require('shaka.media.ClosedCaptionParser'); @@ -31,7 +32,6 @@ goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.util.MimeUtils'); goog.require('shaka.util.Mp4BoxParsers'); goog.require('shaka.util.Mp4Parser'); -goog.require('shaka.util.Platform'); goog.require('shaka.util.PublicPromise'); goog.require('shaka.util.StreamUtils'); goog.require('shaka.util.TimeUtils'); @@ -372,14 +372,15 @@ shaka.media.MediaSourceEngine = class { ]; const support = {}; + const device = shaka.device.DeviceFactory.getDevice(); for (const type of testMimeTypes) { if (shaka.text.TextEngine.isTypeSupported(type)) { support[type] = true; - } else if (shaka.util.Platform.supportsMediaSource()) { + } else if (device.supportsMediaSource()) { support[type] = shaka.media.Capabilities.isTypeSupported(type) || shaka.transmuxer.TransmuxerEngine.isSupported(type); } else { - support[type] = shaka.util.Platform.supportsMediaType(type); + support[type] = device.supportsMediaType(type); } const basicType = type.split(';')[0]; @@ -2104,7 +2105,6 @@ shaka.media.MediaSourceEngine = class { */ workAroundBrokenPlatforms_(stream, segment, reference, contentType) { const ContentType = shaka.util.ManifestParserUtils.ContentType; - const Platform = shaka.util.Platform; const isMp4 = shaka.util.MimeUtils.getContainerType( this.sourceBufferTypes_.get(contentType)) == 'mp4'; @@ -2120,6 +2120,7 @@ shaka.media.MediaSourceEngine = class { isEncrypted = reference.initSegmentReference.encrypted; } const uri = reference ? reference.getUris()[0] : null; + const device = shaka.device.DeviceFactory.getDevice(); if (this.config_.correctEc3Enca && isInitSegment && @@ -2138,20 +2139,20 @@ shaka.media.MediaSourceEngine = class { // transformation on MP4 containers. // See: https://github.com/shaka-project/shaka-player/issues/2759 if (this.config_.insertFakeEncryptionInInit && encryptionExpected && - Platform.requiresEncryptionInfoInAllInitSegments(keySystem, + device.requiresEncryptionInfoInAllInitSegments(keySystem, contentType)) { if (isInitSegment) { shaka.log.debug('Forcing fake encryption information in init segment.'); segment = shaka.media.ContentWorkarounds.fakeEncryption(stream, segment, uri); - } else if (!isEncrypted && Platform.requiresTfhdFix(contentType)) { + } else if (!isEncrypted && device.requiresTfhdFix(contentType)) { shaka.log.debug( 'Forcing fake encryption information in media segment.'); segment = shaka.media.ContentWorkarounds.fakeMediaEncryption(segment); } } - if (isInitSegment && Platform.requiresEC3InitSegments()) { + if (isInitSegment && device.requiresEC3InitSegments()) { shaka.log.debug('Forcing fake EC-3 information in init segment.'); segment = shaka.media.ContentWorkarounds.fakeEC3(segment); } diff --git a/lib/media/playhead.js b/lib/media/playhead.js index 091129c3c..967ae7bf9 100644 --- a/lib/media/playhead.js +++ b/lib/media/playhead.js @@ -9,6 +9,7 @@ goog.provide('shaka.media.Playhead'); goog.provide('shaka.media.SrcEqualsPlayhead'); goog.require('goog.asserts'); +goog.require('shaka.device.DeviceFactory'); goog.require('shaka.log'); goog.require('shaka.media.Capabilities'); goog.require('shaka.media.GapJumpingController'); @@ -17,7 +18,6 @@ goog.require('shaka.media.VideoWrapper'); goog.require('shaka.util.EventManager'); goog.require('shaka.util.IReleasable'); goog.require('shaka.util.MediaReadyState'); -goog.require('shaka.util.Platform'); goog.require('shaka.util.Timer'); goog.requireType('shaka.media.PresentationTimeline'); @@ -529,12 +529,12 @@ shaka.media.MediaSourcePlayhead = class { if (mightNeedCorrectiveSeek && Math.abs(targetTime - currentTime) > gapLimit) { let canCorrectiveSeek = false; - if (shaka.util.Platform.isSeekingSlow()) { + const seekDelay = shaka.device.DeviceFactory.getDevice().seekDelay(); + if (seekDelay) { // You can only seek like this every so often. This is to prevent an // infinite loop on systems where changing currentTime takes a // significant amount of time (e.g. Chromecast). const time = Date.now() / 1000; - const seekDelay = shaka.util.Platform.isFuchsiaCastDevice() ? 3 : 1; if (!this.lastCorrectiveSeek_ || this.lastCorrectiveSeek_ < time - seekDelay) { this.lastCorrectiveSeek_ = time; diff --git a/lib/media/preference_based_criteria.js b/lib/media/preference_based_criteria.js index a3df7937f..3f1aadf00 100644 --- a/lib/media/preference_based_criteria.js +++ b/lib/media/preference_based_criteria.js @@ -7,12 +7,12 @@ goog.provide('shaka.media.PreferenceBasedCriteria'); goog.require('shaka.config.CodecSwitchingStrategy'); +goog.require('shaka.device.DeviceFactory'); goog.require('shaka.log'); goog.require('shaka.media.AdaptationSet'); goog.require('shaka.media.AdaptationSetCriteria'); goog.require('shaka.media.Capabilities'); goog.require('shaka.util.LanguageUtils'); -goog.require('shaka.util.Platform'); /** @@ -322,7 +322,8 @@ shaka.media.PreferenceBasedCriteria = class { } return false; }); - hdrLevel = shaka.util.Platform.getHdrLevel(someHLG); + const device = shaka.device.DeviceFactory.getDevice(); + hdrLevel = device.getHdrLevel(someHLG); } return variants.filter((variant) => { if (variant.video && variant.video.hdr && variant.video.hdr != hdrLevel) { diff --git a/lib/offline/indexeddb/storage_mechanism.js b/lib/offline/indexeddb/storage_mechanism.js index fa1c50662..4904f9ee4 100644 --- a/lib/offline/indexeddb/storage_mechanism.js +++ b/lib/offline/indexeddb/storage_mechanism.js @@ -7,6 +7,7 @@ goog.provide('shaka.offline.indexeddb.StorageMechanism'); goog.require('goog.asserts'); +goog.require('shaka.device.DeviceFactory'); goog.require('shaka.log'); goog.require('shaka.offline.StorageMuxer'); goog.require('shaka.offline.indexeddb.EmeSessionStorageCell'); @@ -15,7 +16,6 @@ goog.require('shaka.offline.indexeddb.V2StorageCell'); goog.require('shaka.offline.indexeddb.V5StorageCell'); goog.require('shaka.util.Error'); goog.require('shaka.util.PublicPromise'); -goog.require('shaka.util.Platform'); goog.require('shaka.util.Timer'); @@ -403,15 +403,8 @@ shaka.offline.indexeddb.StorageMechanismOpenTimeout = 5; shaka.offline.StorageMuxer.register( 'idb', () => { - // Offline storage is not supported on the Chromecast Linux/Fuchsia or - // Xbox One platforms. - if ((shaka.util.Platform.isChromecast() && - !shaka.util.Platform.isAndroidCastDevice()) || - shaka.util.Platform.isXboxOne()) { - return null; - } - // Offline storage requires the IndexedDB API. - if (!window.indexedDB) { + const device = shaka.device.DeviceFactory.getDevice(); + if (!device.supportsOfflineStorage()) { return null; } return new shaka.offline.indexeddb.StorageMechanism(); diff --git a/lib/offline/storage.js b/lib/offline/storage.js index 495a804e0..0ca5de9f0 100644 --- a/lib/offline/storage.js +++ b/lib/offline/storage.js @@ -8,6 +8,7 @@ goog.provide('shaka.offline.Storage'); goog.require('goog.asserts'); goog.require('shaka.Player'); +goog.require('shaka.device.DeviceFactory'); goog.require('shaka.drm.DrmEngine'); goog.require('shaka.log'); goog.require('shaka.media.ManifestParser'); @@ -34,7 +35,6 @@ goog.require('shaka.util.IDestroyable'); goog.require('shaka.util.Iterables'); goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.util.MimeUtils'); -goog.require('shaka.util.Platform'); goog.require('shaka.util.PlayerConfiguration'); goog.require('shaka.util.StreamUtils'); goog.requireType('shaka.media.SegmentReference'); @@ -171,7 +171,8 @@ shaka.offline.Storage = class { // Our Storage system is useless without MediaSource. MediaSource allows us // to pull data from anywhere (including our Storage system) and feed it to // the video element. - if (!shaka.util.Platform.supportsMediaSource()) { + const device = shaka.device.DeviceFactory.getDevice(); + if (!device.supportsMediaSource()) { return false; } diff --git a/lib/player.js b/lib/player.js index 7aa55df1a..c7ab6f696 100644 --- a/lib/player.js +++ b/lib/player.js @@ -9,6 +9,8 @@ goog.provide('shaka.Player'); goog.require('goog.asserts'); goog.require('shaka.config.CrossBoundaryStrategy'); goog.require('shaka.Deprecate'); +goog.require('shaka.device.DeviceFactory'); +goog.require('shaka.device.IDevice'); goog.require('shaka.drm.DrmEngine'); goog.require('shaka.drm.DrmUtils'); goog.require('shaka.log'); @@ -62,7 +64,6 @@ goog.require('shaka.util.MimeUtils'); goog.require('shaka.util.Mutex'); goog.require('shaka.util.NumberUtils'); goog.require('shaka.util.ObjectUtils'); -goog.require('shaka.util.Platform'); goog.require('shaka.util.PlayerConfiguration'); goog.require('shaka.util.PublicPromise'); goog.require('shaka.util.Stats'); @@ -1226,18 +1227,20 @@ shaka.Player = class extends shaka.util.FakeEventTarget { } // We do not support IE - if (shaka.util.Platform.isIE()) { + const userAgent = navigator.userAgent || ''; + if (userAgent.includes('Trident/')) { return false; } // If we have MediaSource (MSE) support, we should be able to use Shaka. - if (shaka.util.Platform.supportsMediaSource()) { + const device = shaka.device.DeviceFactory.getDevice(); + if (device.supportsMediaSource()) { return true; } // If we don't have MSE, we _may_ be able to use Shaka. Look for native HLS // support, and call this platform usable if we have it. - return shaka.util.Platform.supportsMediaType('application/x-mpegurl'); + return device.supportsMediaType('application/x-mpegurl'); } /** @@ -1262,8 +1265,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget { } const manifest = shaka.media.ManifestParser.probeSupport(); const media = shaka.media.MediaSourceEngine.probeSupport(); - const hardwareResolution = - await shaka.util.Platform.detectMaxHardwareResolution(); + const device = shaka.device.DeviceFactory.getDevice(); + goog.asserts.assert(device, 'device must be non-null'); + const hardwareResolution = await device.detectMaxHardwareResolution(); /** @type {shaka.extern.SupportType} */ const ret = { @@ -1332,8 +1336,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget { } // Only initialize media source if the platform supports it. - if (initializeMediaSource && - shaka.util.Platform.supportsMediaSource() && + const device = shaka.device.DeviceFactory.getDevice(); + if (initializeMediaSource && device.supportsMediaSource() && !this.mediaSourceEngine_) { await this.initializeMediaSourceEngineInner_(); } @@ -1658,7 +1662,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget { this.mutex_.release(); } - if (initializeMediaSource && shaka.util.Platform.supportsMediaSource() && + const device = shaka.device.DeviceFactory.getDevice(); + if (initializeMediaSource && device.supportsMediaSource() && !this.mediaSourceEngine_ && this.video_) { await this.initializeMediaSourceEngineInner_(); } @@ -1979,7 +1984,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget { }, 'loadInner_'); preloadManager.stopQueuingLatePhaseQueuedOperations(); - if (this.mimeType_ && shaka.util.Platform.isApple() && + if (this.mimeType_ && + shaka.device.DeviceFactory.getDevice().supportsAirPlay() && shaka.util.MimeUtils.isHlsType(this.mimeType_)) { this.mediaSourceEngine_.addSecondarySource( this.assetUri_, this.mimeType_); @@ -2268,8 +2274,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget { if (this.maxHwRes_.width == Infinity && this.maxHwRes_.height == Infinity && !this.config_.ignoreHardwareResolution) { - const maxResolution = - await shaka.util.Platform.detectMaxHardwareResolution(); + const device = shaka.device.DeviceFactory.getDevice(); + goog.asserts.assert(device, 'device must be non-null'); + const maxResolution = await device.detectMaxHardwareResolution(); this.maxHwRes_.width = maxResolution.width; this.maxHwRes_.height = maxResolution.height; } @@ -2482,8 +2489,12 @@ shaka.Player = class extends shaka.util.FakeEventTarget { const retryParams = this.config_.manifest.retryParameters; let mimeType = await shaka.net.NetworkingUtils.getMimeType( assetUri, this.networkingEngine_, retryParams); - if (mimeType == 'application/x-mpegurl' && shaka.util.Platform.isApple()) { - mimeType = 'application/vnd.apple.mpegurl'; + if (mimeType == 'application/x-mpegurl') { + const device = shaka.device.DeviceFactory.getDevice(); + if (device.getBrowserEngine() === + shaka.device.IDevice.BrowserEngine.WEBKIT) { + mimeType = 'application/vnd.apple.mpegurl'; + } } return mimeType; } @@ -2500,19 +2511,19 @@ shaka.Player = class extends shaka.util.FakeEventTarget { * @private */ shouldUseSrcEquals_(assetUri, mimeType) { - const Platform = shaka.util.Platform; const MimeUtils = shaka.util.MimeUtils; // If we are using a platform that does not support media source, we will // fall back to src= to handle all playback. - if (!Platform.supportsMediaSource()) { + const device = shaka.device.DeviceFactory.getDevice(); + if (!device.supportsMediaSource()) { return true; } if (mimeType) { // If we have a MIME type, check if the browser can play it natively. // This will cover both single files and native HLS. - const mediaElement = this.video_ || Platform.anyMediaElement(); + const mediaElement = this.video_ || shaka.util.Dom.anyMediaElement(); const canPlayNatively = mediaElement.canPlayType(mimeType) != ''; // If we can't play natively, then src= isn't an option. @@ -2537,7 +2548,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget { // version there. if (MimeUtils.isHlsType(mimeType)) { // Native FairPlay HLS can be preferred on Apple platforms. - if (Platform.isApple() && + const device = shaka.device.DeviceFactory.getDevice(); + if (device.getBrowserEngine() === + shaka.device.IDevice.BrowserEngine.WEBKIT && (this.config_.drm.servers['com.apple.fps'] || this.config_.drm.servers['com.apple.fps.1_0'])) { return this.config_.streaming.useNativeHlsForFairPlay; @@ -2593,8 +2606,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget { * @private */ async initializeMediaSourceEngineInner_() { - goog.asserts.assert( - shaka.util.Platform.supportsMediaSource(), + const device = shaka.device.DeviceFactory.getDevice(); + goog.asserts.assert(device.supportsMediaSource(), 'We should not be initializing media source on a platform that ' + 'does not support media source.'); goog.asserts.assert( @@ -3306,11 +3319,13 @@ shaka.Player = class extends shaka.util.FakeEventTarget { mediaElement.src = playbackUri; + const device = shaka.device.DeviceFactory.getDevice(); + // Tizen 3 / WebOS won't load anything unless you call load() explicitly, // no matter the value of the preload attribute. This is harmful on some // other platforms by triggering unbounded loading of media data, but is // necessary here. - if (shaka.util.Platform.isTizen() || shaka.util.Platform.isWebOS()) { + if (device.getDeviceType() == shaka.device.IDevice.DeviceType.TV) { mediaElement.load(); } @@ -3319,7 +3334,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget { // Note: this only happens when there are not autoplay. if (mediaElement.preload != 'none' && !mediaElement.autoplay && shaka.util.MimeUtils.isHlsType(mimeType) && - shaka.util.Platform.isApple()) { + device.getBrowserEngine() === + shaka.device.IDevice.BrowserEngine.WEBKIT) { mediaElement.load(); } @@ -4247,8 +4263,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget { * @export */ configure(config, value) { - const Platform = shaka.util.Platform; - goog.asserts.assert(this.config_, 'Config must not be null!'); goog.asserts.assert(typeof(config) == 'object' || arguments.length == 2, 'String configs should have values!'); @@ -4288,8 +4302,11 @@ shaka.Player = class extends shaka.util.FakeEventTarget { 'streaming.useNativeHlsOnSafari configuration', 'Please Use streaming.useNativeHlsForFairPlay or ' + 'streaming.preferNativeHls instead.'); + const device = shaka.device.DeviceFactory.getDevice(); config['streaming']['preferNativeHls'] = - config['streaming']['useNativeHlsOnSafari'] && Platform.isApple(); + config['streaming']['useNativeHlsOnSafari'] && + device.getBrowserEngine() === + shaka.device.IDevice.BrowserEngine.WEBKIT; delete config['streaming']['useNativeHlsOnSafari']; } @@ -6924,7 +6941,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget { } if (this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS) { - if (forced && shaka.util.Platform.isApple()) { + const device = shaka.device.DeviceFactory.getDevice(); + if (forced && device.getBrowserEngine() === + shaka.device.IDevice.BrowserEngine.WEBKIT) { // See: https://github.com/whatwg/html/issues/4472 kind = 'forced'; } @@ -7588,9 +7607,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget { // On iOS where the Fullscreen API is not available we prefer // NativeTextDisplayer because it works with the Fullscreen API of the // video element itself. - const Platform = shaka.util.Platform; + const device = shaka.device.DeviceFactory.getDevice(); if (this.videoContainer_ && - (!Platform.isApple() || document.fullscreenEnabled)) { + (document.fullscreenEnabled || device.getBrowserEngine() !== + shaka.device.IDevice.BrowserEngine.WEBKIT)) { return new shaka.text.UITextDisplayer( this.video_, this.videoContainer_); } else { @@ -8819,7 +8839,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget { error.code == shaka.util.Error.Code.MEDIA_SOURCE_OPERATION_THREW || error.code == shaka.util.Error.Code.STREAMING_NOT_ALLOWED || error.code == shaka.util.Error.Code.TRANSMUXING_FAILED)) { - if (shaka.util.Platform.isApple() && + const device = shaka.device.DeviceFactory.getDevice(); + if (device.getBrowserEngine() === + shaka.device.IDevice.BrowserEngine.WEBKIT && error.code == shaka.util.Error.Code.VIDEO_ERROR) { // Wait until the MSE error occurs return; diff --git a/lib/polyfill/encryption_scheme.js b/lib/polyfill/encryption_scheme.js index f9826693a..62644d954 100644 --- a/lib/polyfill/encryption_scheme.js +++ b/lib/polyfill/encryption_scheme.js @@ -6,8 +6,8 @@ goog.provide('shaka.polyfill.EncryptionScheme'); +goog.require('shaka.device.DeviceFactory'); goog.require('shaka.polyfill'); -goog.require('shaka.util.Platform'); /** * @summary A polyfill to add support for EncryptionScheme queries in EME. @@ -28,7 +28,8 @@ shaka.polyfill.EncryptionScheme = class { // caused by unsupported encryptionScheme handling. These platforms do not // require the polyfill, and forcing encryptionScheme processing can result // in playback crashes. - if (shaka.util.Platform.isPS4() || shaka.util.Platform.isSkyQ()) { + const device = shaka.device.DeviceFactory.getDevice(); + if (!device.supportsEncryptionSchemePolyfill()) { return; } diff --git a/lib/polyfill/media_capabilities.js b/lib/polyfill/media_capabilities.js index 7b93bb868..3e17f2e07 100644 --- a/lib/polyfill/media_capabilities.js +++ b/lib/polyfill/media_capabilities.js @@ -7,11 +7,12 @@ goog.provide('shaka.polyfill.MediaCapabilities'); goog.require('shaka.log'); +goog.require('shaka.device.DeviceFactory'); +goog.require('shaka.device.IDevice'); goog.require('shaka.drm.DrmUtils'); goog.require('shaka.media.Capabilities'); goog.require('shaka.polyfill'); goog.require('shaka.util.MimeUtils'); -goog.require('shaka.util.Platform'); /** @@ -27,41 +28,7 @@ shaka.polyfill.MediaCapabilities = class { * @export */ static install() { - // We can enable MediaCapabilities in Android and Fuchsia devices, but not - // in Linux devices because the implementation is buggy. - // Since MediaCapabilities implementation is buggy in Apple browsers, we - // should always install polyfill for Apple browsers. - // See: https://github.com/shaka-project/shaka-player/issues/3530 - // TODO: re-evaluate MediaCapabilities in the future versions of Apple - // Browsers. - // Since MediaCapabilities implementation is buggy in PS5 browsers, we - // should always install polyfill for PS5 browsers. - // See: https://github.com/shaka-project/shaka-player/issues/3582 - // TODO: re-evaluate MediaCapabilities in the future versions of PS5 - // Browsers. - // Since MediaCapabilities implementation does not exist in PS4 browsers, we - // should always install polyfill. - // Since MediaCapabilities implementation is buggy in Tizen browsers, we - // should always install polyfill for Tizen browsers. - // Since MediaCapabilities implementation is buggy in WebOS browsers, we - // should always install polyfill for WebOS browsers. - // Since MediaCapabilities implementation is buggy in EOS browsers, we - // should always install polyfill for EOS browsers. - // Since MediaCapabilities implementation is buggy in Hisense browsers, we - // should always install polyfill for Hisense browsers. - let canUseNativeMCap = true; - if (shaka.util.Platform.isOlderChromecast() || - shaka.util.Platform.isApple() || - shaka.util.Platform.isPS5() || - shaka.util.Platform.isPS4() || - shaka.util.Platform.isWebOS() || - shaka.util.Platform.isTizen() || - shaka.util.Platform.isHisense() || - shaka.util.Platform.isVizio() || - shaka.util.Platform.isWebkitSTB()) { - canUseNativeMCap = false; - } - if (canUseNativeMCap && navigator.mediaCapabilities) { + if (shaka.device.DeviceFactory.getDevice().supportsMediaCapabilities()) { shaka.log.info( 'MediaCapabilities: Native mediaCapabilities support found.'); return; @@ -104,11 +71,13 @@ shaka.polyfill.MediaCapabilities = class { configuration: mediaDecodingConfig, }; + const device = shaka.device.DeviceFactory.getDevice(); + const videoConfig = mediaDecodingConfig['video']; const audioConfig = mediaDecodingConfig['audio']; if (mediaDecodingConfig.type == 'media-source') { - if (!shaka.util.Platform.supportsMediaSource()) { + if (!device.supportsMediaSource()) { return res; } @@ -131,7 +100,7 @@ shaka.polyfill.MediaCapabilities = class { } else if (mediaDecodingConfig.type == 'file') { if (videoConfig) { const contentType = videoConfig.contentType; - const isSupported = shaka.util.Platform.supportsMediaType(contentType); + const isSupported = device.supportsMediaType(contentType); if (!isSupported) { return res; } @@ -139,7 +108,7 @@ shaka.polyfill.MediaCapabilities = class { if (audioConfig) { const contentType = audioConfig.contentType; - const isSupported = shaka.util.Platform.supportsMediaType(contentType); + const isSupported = device.supportsMediaType(contentType); if (!isSupported) { return res; } @@ -179,7 +148,9 @@ shaka.polyfill.MediaCapabilities = class { // Cast platforms will additionally check canDisplayType(), which // accepts extended MIME type parameters. // See: https://github.com/shaka-project/shaka-player/issues/4726 - if (shaka.util.Platform.isChromecast()) { + const device = shaka.device.DeviceFactory.getDevice(); + const deviceType = device.getDeviceType(); + if (deviceType === shaka.device.IDevice.DeviceType.CAST) { const isSupported = await shaka.polyfill.MediaCapabilities.canCastDisplayType_( videoConfig); @@ -195,8 +166,11 @@ shaka.polyfill.MediaCapabilities = class { * @private */ static checkAudioSupport_(audioConfig) { + const device = shaka.device.DeviceFactory.getDevice(); + const deviceType = device.getDeviceType(); let extendedType = audioConfig.contentType; - if (shaka.util.Platform.isChromecast() && audioConfig.spatialRendering) { + if (deviceType === shaka.device.IDevice.DeviceType.CAST && + audioConfig.spatialRendering) { extendedType += '; spatialRendering=true'; } return shaka.media.Capabilities.isTypeSupported(extendedType); @@ -227,7 +201,7 @@ shaka.polyfill.MediaCapabilities = class { // report EC-3 support. So query EC-3 as a fallback for AC-3. // See https://github.com/shaka-project/shaka-player/issues/2989 for // details. - if (shaka.util.Platform.isTizen() && + if (shaka.device.DeviceFactory.getDevice().misreportAC3UsingDrm() && audioConfig.contentType.includes('codecs="ac-3"')) { capability.contentType = 'audio/mp4; codecs="ec-3"'; } diff --git a/lib/polyfill/mediasource.js b/lib/polyfill/mediasource.js index c52603f60..88a8062b0 100644 --- a/lib/polyfill/mediasource.js +++ b/lib/polyfill/mediasource.js @@ -6,10 +6,11 @@ goog.provide('shaka.polyfill.MediaSource'); +goog.require('shaka.device.DeviceFactory'); +goog.require('shaka.device.IDevice'); goog.require('shaka.log'); goog.require('shaka.polyfill'); goog.require('shaka.util.MimeUtils'); -goog.require('shaka.util.Platform'); /** * @summary A polyfill to patch MSE bugs. @@ -28,7 +29,10 @@ shaka.polyfill.MediaSource = class { // example, and instances are only accessible after setting up MediaSource // on a video element. Because of this, we use UA detection and other // platform detection tricks to decide which patches to install. - const safariVersion = shaka.util.Platform.safariVersion(); + const BrowserEngine = shaka.device.IDevice.BrowserEngine; + const device = shaka.device.DeviceFactory.getDevice(); + const safariVersion = device.getBrowserEngine() === BrowserEngine.WEBKIT ? + device.getVersion() : null; if (!window.MediaSource && !window.ManagedMediaSource) { shaka.log.info('No MSE implementation available.'); @@ -60,30 +64,15 @@ shaka.polyfill.MediaSource = class { // Bug filed: https://bugs.webkit.org/show_bug.cgi?id=165342 shaka.polyfill.MediaSource.stubAbort_(); } - } else if (shaka.util.Platform.isZenterio()) { - // Zenterio uses WPE based on Webkit 607.x.x which do not correctly - // implement abort() on SourceBuffer. - // Calling abort() before appending a segment causes that segment to be - // incomplete in the buffer. - // Bug filed: https://bugs.webkit.org/show_bug.cgi?id=165342 - shaka.polyfill.MediaSource.stubAbort_(); - // If you remove up to a keyframe, Webkit 607.x.x incorrectly will also - // remove that keyframe and the content up to the next. - // Offsetting the end of the removal range seems to help. - // Bug filed: https://bugs.webkit.org/show_bug.cgi?id=177884 - shaka.polyfill.MediaSource.patchRemovalRange_(); - } else if (shaka.util.Platform.isTizen2() || - shaka.util.Platform.isTizen3() || - shaka.util.Platform.isTizen4()) { - shaka.log.info('Rejecting Opus.'); - // Tizen's implementation of MSE does not work well with opus. To prevent - // the player from trying to play opus on Tizen, we will override media - // source to always reject opus content. - shaka.polyfill.MediaSource.rejectCodec_('opus'); } else { shaka.log.info('Using native MSE as-is.'); } + for (const codec of device.rejectCodecs()) { + shaka.log.info(`Rejecting ${codec}.`); + shaka.polyfill.MediaSource.rejectCodec_(codec); + } + if (window.MediaSource || window.ManagedMediaSource) { // TS content is broken on all browsers in general. // See https://github.com/shaka-project/shaka-player/issues/4955 @@ -212,7 +201,7 @@ shaka.polyfill.MediaSource = class { static patchVp09_() { const originalIsTypeSupported = MediaSource.isTypeSupported; - if (shaka.util.Platform.isWebOS()) { + if (!shaka.device.DeviceFactory.getDevice().supportStandardVP9Checking()) { // Don't do this on LG webOS as otherwise it is unable // to play vp09 at all. return; diff --git a/lib/polyfill/patchedmediakeys_cert.js b/lib/polyfill/patchedmediakeys_cert.js index b3bcd7012..6a867b6cf 100644 --- a/lib/polyfill/patchedmediakeys_cert.js +++ b/lib/polyfill/patchedmediakeys_cert.js @@ -6,9 +6,9 @@ goog.provide('shaka.polyfill.PatchedMediaKeysCert'); +goog.require('shaka.device.DeviceFactory'); goog.require('shaka.log'); goog.require('shaka.polyfill'); -goog.require('shaka.util.Platform'); /** @@ -26,9 +26,10 @@ shaka.polyfill.PatchedMediaKeysCert = class { // No MediaKeys available return; } + const device = shaka.device.DeviceFactory.getDevice(); // eslint-disable-next-line no-restricted-syntax if (MediaKeys.prototype.setServerCertificate && - !shaka.polyfill.PatchedMediaKeysCert.hasInvalidImplementation_()) { + device.supportsServerCertificate()) { // setServerCertificate is there and userAgent seems to be valid. return; } @@ -48,15 +49,6 @@ shaka.polyfill.PatchedMediaKeysCert = class { shaka.log.debug('PatchedMediaKeysCert.setServerCertificate'); return Promise.resolve(false); } - - /** - * @return {boolean} - * @private - */ - static hasInvalidImplementation_() { - return shaka.util.Platform.isTizen3() || shaka.util.Platform.isTizen4() || - shaka.util.Platform.isTizen5_0() || shaka.util.Platform.isWebOS3(); - } }; shaka.polyfill.register(shaka.polyfill.PatchedMediaKeysCert.install); diff --git a/lib/polyfill/videoplaybackquality.js b/lib/polyfill/videoplaybackquality.js index c46c7809f..b3409d2c1 100644 --- a/lib/polyfill/videoplaybackquality.js +++ b/lib/polyfill/videoplaybackquality.js @@ -7,7 +7,7 @@ goog.provide('shaka.polyfill.VideoPlaybackQuality'); goog.require('shaka.polyfill'); -goog.require('shaka.util.Platform'); +goog.require('shaka.util.Dom'); /** @@ -35,7 +35,8 @@ shaka.polyfill.VideoPlaybackQuality = class { } if ('webkitDroppedFrameCount' in proto || - shaka.util.Platform.isWebOS3()) { + typeof shaka.util.Dom.anyMediaElement().webkitDroppedFrameCount === + 'number') { proto.getVideoPlaybackQuality = shaka.polyfill.VideoPlaybackQuality.webkit_; } diff --git a/lib/text/native_text_displayer.js b/lib/text/native_text_displayer.js index 73231bd74..8d9e48438 100644 --- a/lib/text/native_text_displayer.js +++ b/lib/text/native_text_displayer.js @@ -11,11 +11,12 @@ goog.provide('shaka.text.NativeTextDisplayer'); goog.require('mozilla.LanguageMapping'); +goog.require('shaka.device.DeviceFactory'); +goog.require('shaka.device.IDevice'); goog.require('shaka.text.Utils'); goog.require('shaka.util.EventManager'); goog.require('shaka.util.FakeEvent'); goog.require('shaka.util.LanguageUtils'); -goog.require('shaka.util.Platform'); goog.require('shaka.util.Timer'); goog.requireType('shaka.Player'); @@ -87,7 +88,9 @@ shaka.text.NativeTextDisplayer = class { if (track.language in mozilla.LanguageMapping) { trackNode.srclang = track.language; } - if (shaka.util.Platform.isChrome()) { + const device = shaka.device.DeviceFactory.getDevice(); + if (device.getBrowserEngine() === + shaka.device.IDevice.BrowserEngine.CHROMIUM) { // The built-in captions menu in Chrome may refuse to list invalid // subtitles. The data URL is just to avoid this. trackNode.src = 'data:,WEBVTT'; @@ -324,7 +327,9 @@ shaka.text.NativeTextDisplayer = class { * @private */ static getTrackKind_(track) { - if (track.forced && shaka.util.Platform.isApple()) { + const device = shaka.device.DeviceFactory.getDevice(); + if (track.forced && device.getBrowserEngine() === + shaka.device.IDevice.BrowserEngine.WEBKIT) { return 'forced'; } else if ( track.kind === 'caption' || ( diff --git a/lib/transmuxer/ac3_transmuxer.js b/lib/transmuxer/ac3_transmuxer.js index 8aab54f75..bb8c93323 100644 --- a/lib/transmuxer/ac3_transmuxer.js +++ b/lib/transmuxer/ac3_transmuxer.js @@ -6,6 +6,7 @@ goog.provide('shaka.transmuxer.Ac3Transmuxer'); +goog.require('shaka.device.DeviceFactory'); goog.require('shaka.media.Capabilities'); goog.require('shaka.transmuxer.Ac3'); goog.require('shaka.transmuxer.TransmuxerEngine'); @@ -14,7 +15,6 @@ goog.require('shaka.util.Error'); goog.require('shaka.util.Id3Utils'); goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.util.Mp4Generator'); -goog.require('shaka.util.Platform'); goog.require('shaka.util.Uint8ArrayUtils'); @@ -87,7 +87,8 @@ shaka.transmuxer.Ac3Transmuxer = class { */ convertCodecs(contentType, mimeType) { if (this.isAc3Container_(mimeType)) { - if (shaka.util.Platform.requiresEC3InitSegments()) { + const device = shaka.device.DeviceFactory.getDevice(); + if (device.requiresEC3InitSegments()) { return 'audio/mp4; codecs="ec-3"'; } else { return 'audio/mp4; codecs="ac-3"'; diff --git a/lib/util/dom_utils.js b/lib/util/dom_utils.js index 561069513..f38899241 100644 --- a/lib/util/dom_utils.js +++ b/lib/util/dom_utils.js @@ -8,6 +8,7 @@ goog.provide('shaka.util.Dom'); goog.require('goog.asserts'); +goog.require('shaka.util.Timer'); // TODO: revisit this when Closure Compiler supports partially-exported classes. @@ -159,6 +160,41 @@ shaka.util.Dom = class { } } + /** + * For canPlayType queries, we just need any instance. + * + * First, use a cached element from a previous query. + * Second, search the page for one. + * Third, create a temporary one. + * + * Cached elements expire in one second so that they can be GC'd or removed. + * + * @return {!HTMLMediaElement} + */ + static anyMediaElement() { + if (shaka.util.Dom.cachedMediaElement_) { + return shaka.util.Dom.cachedMediaElement_; + } + + if (!shaka.util.Dom.cacheExpirationTimer_) { + shaka.util.Dom.cacheExpirationTimer_ = new shaka.util.Timer(() => { + shaka.util.Dom.cachedMediaElement_ = null; + }); + } + + shaka.util.Dom.cachedMediaElement_ = /** @type {HTMLMediaElement} */( + document.getElementsByTagName('video')[0] || + document.getElementsByTagName('audio')[0]); + + if (!shaka.util.Dom.cachedMediaElement_) { + shaka.util.Dom.cachedMediaElement_ = /** @type {!HTMLMediaElement} */( + document.createElement('video')); + } + + shaka.util.Dom.cacheExpirationTimer_.tickAfter(/* seconds= */ 1); + return shaka.util.Dom.cachedMediaElement_; + } + /** * Load a new font on the page. If the font was already loaded, it does * nothing. @@ -196,3 +232,9 @@ shaka.util.Dom = class { } }; +/** @private {shaka.util.Timer} */ +shaka.util.Dom.cacheExpirationTimer_ = null; + +/** @private {HTMLMediaElement} */ +shaka.util.Dom.cachedMediaElement_ = null; + diff --git a/lib/util/mp4_generator.js b/lib/util/mp4_generator.js index 8869d9825..d07ea0ad5 100644 --- a/lib/util/mp4_generator.js +++ b/lib/util/mp4_generator.js @@ -7,8 +7,8 @@ goog.provide('shaka.util.Mp4Generator'); goog.require('goog.asserts'); +goog.require('shaka.device.DeviceFactory'); goog.require('shaka.util.ManifestParserUtils'); -goog.require('shaka.util.Platform'); goog.require('shaka.util.Uint8ArrayUtils'); @@ -303,7 +303,8 @@ shaka.util.Mp4Generator = class { if (streamInfo.codecs.includes('mp3')) { audioCodec = 'mp3'; } else if (streamInfo.codecs.includes('ac-3')) { - if (shaka.util.Platform.requiresEC3InitSegments()) { + const device = shaka.device.DeviceFactory.getDevice(); + if (device.requiresEC3InitSegments()) { audioCodec = 'ec-3'; } else { audioCodec = 'ac-3'; diff --git a/lib/util/platform.js b/lib/util/platform.js deleted file mode 100644 index e49ce54c9..000000000 --- a/lib/util/platform.js +++ /dev/null @@ -1,932 +0,0 @@ -/*! @license - * Shaka Player - * Copyright 2016 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -goog.provide('shaka.util.Platform'); - -goog.require('shaka.drm.DrmUtils'); -goog.require('shaka.log'); -goog.require('shaka.util.Timer'); - - -/** - * A wrapper for platform-specific functions. - * - * @final - */ -shaka.util.Platform = class { - /** - * Check if the current platform supports media source. We assume that if - * the current platform supports media source, then we can use media source - * as per its design. - * - * @return {boolean} - */ - static supportsMediaSource() { - const mediaSource = window.ManagedMediaSource || window.MediaSource; - // Browsers that lack a media source implementation will have no reference - // to |window.MediaSource|. Platforms that we see having problematic media - // source implementations will have this reference removed via a polyfill. - if (!mediaSource) { - return false; - } - - // Some very old MediaSource implementations didn't have isTypeSupported. - if (!mediaSource.isTypeSupported) { - return false; - } - - return true; - } - - /** - * Returns true if the media type is supported natively by the platform. - * - * @param {string} mimeType - * @return {boolean} - */ - static supportsMediaType(mimeType) { - const video = shaka.util.Platform.anyMediaElement(); - return video.canPlayType(mimeType) != ''; - } - - /** - * Check if the current platform is MS Edge. - * - * @return {boolean} - */ - static isEdge() { - // Legacy Edge contains "Edge/version". - // Chromium-based Edge contains "Edg/version" (no "e"). - if (navigator.userAgent.match(/Edge?\//)) { - return true; - } - - return false; - } - - /** - * Check if the current platform is Legacy Edge. - * - * @return {boolean} - */ - static isLegacyEdge() { - // Legacy Edge contains "Edge/version". - // Chromium-based Edge contains "Edg/version" (no "e"). - if (navigator.userAgent.match(/Edge\//)) { - return true; - } - - return false; - } - - /** - * Check if the current platform is MS IE. - * - * @return {boolean} - */ - static isIE() { - return shaka.util.Platform.userAgentContains_('Trident/'); - } - - /** - * Check if the current platform is an Xbox One. - * - * @return {boolean} - */ - static isXboxOne() { - return shaka.util.Platform.userAgentContains_('Xbox One'); - } - - /** - * Check if the current platform is a Tizen TV. - * - * @return {boolean} - */ - static isTizen() { - return shaka.util.Platform.userAgentContains_('Tizen'); - } - - /** - * Check if the current platform is a Tizen 6 TV. - * - * @return {boolean} - */ - static isTizen6() { - return shaka.util.Platform.userAgentContains_('Tizen 6'); - } - - /** - * Check if the current platform is a Tizen 5.0 TV. - * - * @return {boolean} - */ - static isTizen5_0() { - return shaka.util.Platform.userAgentContains_('Tizen 5.0'); - } - - /** - * Check if the current platform is a Tizen 5 TV. - * - * @return {boolean} - */ - static isTizen5() { - return shaka.util.Platform.userAgentContains_('Tizen 5'); - } - - /** - * Check if the current platform is a Tizen 4 TV. - * - * @return {boolean} - */ - static isTizen4() { - return shaka.util.Platform.userAgentContains_('Tizen 4'); - } - - /** - * Check if the current platform is a Tizen 3 TV. - * - * @return {boolean} - */ - static isTizen3() { - return shaka.util.Platform.userAgentContains_('Tizen 3'); - } - - /** - * Check if the current platform is a Tizen 2 TV. - * - * @return {boolean} - */ - static isTizen2() { - return shaka.util.Platform.userAgentContains_('Tizen 2'); - } - - /** - * Check if the current platform is a WebOS. - * - * @return {boolean} - */ - static isWebOS() { - return shaka.util.Platform.userAgentContains_('Web0S'); - } - - /** - * Check if the current platform is a WebOS 3. - * - * @return {boolean} - */ - static isWebOS3() { - // See: https://webostv.developer.lge.com/develop/specifications/web-api-and-web-engine#useragent-string - return shaka.util.Platform.isWebOS() && - shaka.util.Platform.chromeVersion() === 38; - } - - /** - * Check if the current platform is a WebOS 4. - * - * @return {boolean} - */ - static isWebOS4() { - // See: https://webostv.developer.lge.com/develop/specifications/web-api-and-web-engine#useragent-string - return shaka.util.Platform.isWebOS() && - shaka.util.Platform.chromeVersion() === 53; - } - - /** - * Check if the current platform is a WebOS 5. - * - * @return {boolean} - */ - static isWebOS5() { - // See: https://webostv.developer.lge.com/develop/specifications/web-api-and-web-engine#useragent-string - return shaka.util.Platform.isWebOS() && - shaka.util.Platform.chromeVersion() === 68; - } - - /** - * Check if the current platform is a WebOS 6. - * - * @return {boolean} - */ - static isWebOS6() { - // See: https://webostv.developer.lge.com/develop/specifications/web-api-and-web-engine#useragent-string - return shaka.util.Platform.isWebOS() && - shaka.util.Platform.chromeVersion() === 79; - } - - /** - * Check if the current platform is a Google Chromecast. - * - * @return {boolean} - */ - static isChromecast() { - const Platform = shaka.util.Platform; - return Platform.userAgentContains_('CrKey') && !Platform.isVizio(); - } - - /** - * Check if the current platform is a Google Chromecast without - * Android or Fuchsia. - * - * @return {boolean} - */ - static isOlderChromecast() { - const Platform = shaka.util.Platform; - return Platform.isChromecast() && - !Platform.isAndroid() && !Platform.isFuchsia(); - } - - /** - * Check if the current platform is a Google Chromecast with Android - * (i.e. Chromecast with GoogleTV). - * - * @return {boolean} - */ - static isAndroidCastDevice() { - const Platform = shaka.util.Platform; - return Platform.isChromecast() && Platform.isAndroid(); - } - - /** - * Check if the current platform is a Google Chromecast with Fuchsia - * (i.e. Google Nest Hub). - * - * @return {boolean} - */ - static isFuchsiaCastDevice() { - const Platform = shaka.util.Platform; - return Platform.isChromecast() && Platform.isFuchsia(); - } - - /** - * Returns a major version number for Chrome, or Chromium-based browsers. - * - * For example: - * - Chrome 106.0.5249.61 returns 106. - * - Edge 106.0.1370.34 returns 106 (since this is based on Chromium). - * - Safari returns null (since this is independent of Chromium). - * - * @return {?number} A major version number or null if not Chromium-based. - */ - static chromeVersion() { - if (!shaka.util.Platform.isChrome()) { - return null; - } - - // Looking for something like "Chrome/106.0.0.0". - const match = navigator.userAgent.match(/Chrome\/(\d+)/); - if (match) { - return parseInt(match[1], /* base= */ 10); - } - - return null; - } - - /** - * Check if the current platform is Google Chrome. - * - * @return {boolean} - */ - static isChrome() { - // The Edge Legacy user agent will also contain the "Chrome" keyword, so we - // need to make sure this is not Edge Legacy. - return shaka.util.Platform.userAgentContains_('Chrome') && - !shaka.util.Platform.isLegacyEdge(); - } - - /** - * Check if the current platform is Firefox. - * - * @return {boolean} - */ - static isFirefox() { - return shaka.util.Platform.userAgentContains_('Firefox'); - } - - /** - * Check if the current platform is from Apple. - * - * Returns true on all iOS browsers and on desktop Safari. - * - * Returns false for non-Safari browsers on macOS, which are independent of - * Apple. - * - * @return {boolean} - */ - static isApple() { - return shaka.util.Platform.isAppleVendor_() && - (shaka.util.Platform.isMac() || shaka.util.Platform.isIOS()); - } - - /** - * Check if the current platform is Playstation 5. - * - * Returns true on Playstation 5 browsers. - * - * Returns false for Playstation 5 browsers - * - * @return {boolean} - */ - static isPS5() { - return shaka.util.Platform.userAgentContains_('PlayStation 5'); - } - - /** - * Check if the current platform is Playstation 4. - * @return {boolean} - */ - static isPS4() { - return shaka.util.Platform.userAgentContains_('PlayStation 4'); - } - - /** - * Check if the current platform is Sony TV. - * @return {boolean} - */ - static isSonyTV() { - return shaka.util.Platform.userAgentContains_('sony.hbbtv.tv.G5'); - } - - /** - * Check if the current platform is Hisense. - * @return {boolean} - */ - static isHisense() { - return shaka.util.Platform.userAgentContains_('Hisense') || - shaka.util.Platform.userAgentContains_('VIDAA'); - } - - /** - * Check if the current platform is Vizio TV. - * @return {boolean} - */ - static isVizio() { - return shaka.util.Platform.userAgentContains_('VIZIO SmartCast'); - } - - /** - * Check if the current platform is Orange. - * @return {boolean} - */ - static isOrange() { - return shaka.util.Platform.userAgentContains_('SOPOpenBrowser'); - } - - /** - * Check if the current platform is SkyQ STB. - * @return {boolean} - */ - static isSkyQ() { - return shaka.util.Platform.userAgentContains_('Sky_STB'); - } - - /** - * Check if the current platform is Deutsche Telecom Zenterio STB. - * @return {boolean} - */ - static isZenterio() { - return shaka.util.Platform.userAgentContains_('DT_STB_BCM'); - } - - /** - * Returns a major version number for Safari, or Webkit-based STBs, - * or Safari-based iOS browsers. - * - * For example: - * - Safari 13.0.4 on macOS returns 13. - * - Safari on iOS 13.3.1 returns 13. - * - Chrome on iOS 13.3.1 returns 13 (since this is based on Safari/WebKit). - * - Chrome on macOS returns null (since this is independent of Apple). - * - * Returns null on Firefox on iOS, where this version information is not - * available. - * - * @return {?number} A major version number or null if not iOS. - */ - static safariVersion() { - // All iOS browsers and desktop Safari will return true for isApple(). - if (!shaka.util.Platform.isApple() && !shaka.util.Platform.isWebkitSTB()) { - return null; - } - - // This works for iOS Safari and desktop Safari, which contain something - // like "Version/13.0" indicating the major Safari or iOS version. - let match = navigator.userAgent.match(/Version\/(\d+)/); - if (match) { - return parseInt(match[1], /* base= */ 10); - } - - // This works for all other browsers on iOS, which contain something like - // "OS 13_3" indicating the major & minor iOS version. - match = navigator.userAgent.match(/OS (\d+)(?:_\d+)?/); - if (match) { - return parseInt(match[1], /* base= */ 10); - } - - return null; - } - - /** - * Guesses if the platform is an Apple mobile one (iOS, iPad, iPod). - * @return {boolean} - */ - static isIOS() { - if (/(?:iPhone|iPad|iPod)/.test(navigator.userAgent)) { - // This is Android, iOS, or iPad < 13. - return true; - } - - // Starting with iOS 13 on iPad, the user agent string no longer has the - // word "iPad" in it. It looks very similar to desktop Safari. This seems - // to be intentional on Apple's part. - // See: https://forums.developer.apple.com/thread/119186 - // - // So if it's an Apple device with multi-touch support, assume it's a mobile - // device. If some future iOS version starts masking their user agent on - // both iPhone & iPad, this clause should still work. If a future - // multi-touch desktop Mac is released, this will need some adjustment. - // - // As of January 2020, this is mainly used to adjust the default UI config - // for mobile devices, so it's low risk if something changes to break this - // detection. - return shaka.util.Platform.isAppleVendor_() && navigator.maxTouchPoints > 1; - } - - /** - * Guesses if the platform is a mobile one. - * @return {boolean} - */ - static isMobile() { - if (navigator.userAgentData) { - return navigator.userAgentData.mobile; - } - return shaka.util.Platform.isIOS() || shaka.util.Platform.isAndroid(); - } - - /** - * Return true if the platform is a Mac, regardless of the browser. - * - * @return {boolean} - */ - static isMac() { - // Try the newer standard first. - if (navigator.userAgentData && navigator.userAgentData.platform) { - return navigator.userAgentData.platform.toLowerCase() == 'macos'; - } - // Fall back to the old API, with less strict matching. - if (!navigator.platform) { - return false; - } - return navigator.platform.toLowerCase().includes('mac'); - } - - /** - * Return true if the platform is a VisionOS. - * - * @return {boolean} - */ - static isVisionOS() { - if (!shaka.util.Platform.isMac()) { - return false; - } - if (!('xr' in navigator)) { - return false; - } - return true; - } - - /** - * Checks is non-Apple STB based on Webkit. - * @return {boolean} - */ - static isWebkitSTB() { - return shaka.util.Platform.isAppleVendor_() && - !shaka.util.Platform.isApple(); - } - - /** - * Return true if the platform is a Windows, regardless of the browser. - * - * @return {boolean} - */ - static isWindows() { - // Try the newer standard first. - if (navigator.userAgentData && navigator.userAgentData.platform) { - return navigator.userAgentData.platform.toLowerCase() == 'windows'; - } - // Fall back to the old API, with less strict matching. - if (!navigator.platform) { - return false; - } - return navigator.platform.toLowerCase().includes('win32'); - } - - /** - * Return true if the platform is a Android, regardless of the browser. - * - * @return {boolean} - */ - static isAndroid() { - if (navigator.userAgentData && navigator.userAgentData.platform) { - return navigator.userAgentData.platform.toLowerCase() == 'android'; - } - return shaka.util.Platform.userAgentContains_('Android'); - } - - /** - * Return true if the platform is a Fuchsia, regardless of the browser. - * - * @return {boolean} - */ - static isFuchsia() { - if (navigator.userAgentData && navigator.userAgentData.platform) { - return navigator.userAgentData.platform.toLowerCase() == 'fuchsia'; - } - return shaka.util.Platform.userAgentContains_('Fuchsia'); - } - - /** - * Return true if the platform is controlled by a remote control. - * - * @return {boolean} - */ - static isSmartTV() { - const Platform = shaka.util.Platform; - if (Platform.isTizen() || Platform.isWebOS() || - Platform.isXboxOne() || Platform.isPS4() || - Platform.isPS5() || Platform.isChromecast() || - Platform.isHisense() || Platform.isVizio() || - Platform.isWebkitSTB()) { - return true; - } - return false; - } - - /** - * Check if the user agent contains a key. This is the best way we know of - * right now to detect platforms. If there is a better way, please send a - * PR. - * - * @param {string} key - * @return {boolean} - * @private - */ - static userAgentContains_(key) { - const userAgent = navigator.userAgent || ''; - return userAgent.includes(key); - } - - /** - * @return {boolean} - * @private - */ - static isAppleVendor_() { - return (navigator.vendor || '').includes('Apple'); - } - - /** - * For canPlayType queries, we just need any instance. - * - * First, use a cached element from a previous query. - * Second, search the page for one. - * Third, create a temporary one. - * - * Cached elements expire in one second so that they can be GC'd or removed. - * - * @return {!HTMLMediaElement} - */ - static anyMediaElement() { - const Platform = shaka.util.Platform; - if (Platform.cachedMediaElement_) { - return Platform.cachedMediaElement_; - } - - if (!Platform.cacheExpirationTimer_) { - Platform.cacheExpirationTimer_ = new shaka.util.Timer(() => { - Platform.cachedMediaElement_ = null; - }); - } - - Platform.cachedMediaElement_ = /** @type {HTMLMediaElement} */( - document.getElementsByTagName('video')[0] || - document.getElementsByTagName('audio')[0]); - - if (!Platform.cachedMediaElement_) { - Platform.cachedMediaElement_ = /** @type {!HTMLMediaElement} */( - document.createElement('video')); - } - - Platform.cacheExpirationTimer_.tickAfter(/* seconds= */ 1); - return Platform.cachedMediaElement_; - } - - /** - * Returns true if the platform requires encryption information in all init - * segments. For such platforms, MediaSourceEngine will attempt to work - * around a lack of such info by inserting fake encryption information into - * initialization segments. - * - * @param {?string} keySystem - * @param {string} contentType - * @return {boolean} - * @see https://github.com/shaka-project/shaka-player/issues/2759 - */ - static requiresEncryptionInfoInAllInitSegments(keySystem, contentType) { - const Platform = shaka.util.Platform; - const isPlayReady = shaka.drm.DrmUtils.isPlayReadyKeySystem(keySystem); - return (Platform.isApple() && contentType === 'audio') || - Platform.isTizen() || Platform.isXboxOne() || Platform.isOrange() || - (Platform.isEdge() && Platform.isWindows() && isPlayReady); - } - - /** - * @param {string} contentType - * @return {boolean} - */ - static requiresTfhdFix(contentType) { - return shaka.util.Platform.isApple() && contentType === 'audio'; - } - - /** - * Returns true if the platform requires AC-3 signalling in init - * segments to be replaced with EC-3 signalling. - * For such platforms, MediaSourceEngine will attempt to work - * around it by inserting fake EC-3 signalling into - * initialization segments. - * - * @return {boolean} - */ - static requiresEC3InitSegments() { - return shaka.util.Platform.isTizen3(); - } - - /** - * Returns true if the platform supports SourceBuffer "sequence mode". - * - * @return {boolean} - */ - static supportsSequenceMode() { - const Platform = shaka.util.Platform; - if (Platform.isTizen3() || Platform.isTizen2() || - Platform.isWebOS3() || Platform.isPS4() || Platform.isPS5()) { - return false; - } - // See: https://bugs.webkit.org/show_bug.cgi?id=210341 - const safariVersion = Platform.safariVersion(); - if (Platform.isWebkitSTB() && safariVersion != null && safariVersion < 15) { - return false; - } - return true; - } - - /** - * Returns if codec switching SMOOTH is known reliable device support. - * - * Some devices are known not to support SourceBuffer.changeType - * well. These devices should use the reload strategy. If a device - * reports that it supports but supports it unreliably - * it should be disallowed in this method. - * - * @return {boolean} - */ - static supportsSmoothCodecSwitching() { - const Platform = shaka.util.Platform; - // All Tizen versions (up to Tizen 8) do not support SMOOTH so far. - // webOS seems to support SMOOTH from webOS 22. - if (Platform.isTizen() || Platform.isPS4() || Platform.isPS5() || - Platform.isWebOS6()) { - return false; - } - // Older chromecasts without GoogleTV seem to not support SMOOTH properly. - if (Platform.isOlderChromecast()) { - return false; - } - // See: https://chromium-review.googlesource.com/c/chromium/src/+/4577759 - if (Platform.isWindows() && Platform.isEdge()) { - return false; - } - return true; - } - - /** - * On some platforms, such as v1 Chromecasts, the act of seeking can take a - * significant amount of time. - * - * @return {boolean} - */ - static isSeekingSlow() { - const Platform = shaka.util.Platform; - if (Platform.isChromecast()) { - if (Platform.isAndroidCastDevice()) { - // Android-based Chromecasts are new enough to not be a problem. - return false; - } else { - return true; - } - } - return false; - } - - - /** - * Detect the maximum resolution that the platform's hardware can handle. - * - * @return {!Promise} - */ - static async detectMaxHardwareResolution() { - const Platform = shaka.util.Platform; - - /** @type {shaka.extern.Resolution} */ - const maxResolution = { - width: Infinity, - height: Infinity, - }; - - if (Platform.isChromecast()) { - // In our tests, the original Chromecast seems to have trouble decoding - // above 1080p. It would be a waste to select a higher res anyway, given - // that the device only outputs 1080p to begin with. - // Chromecast has an extension to query the device/display's resolution. - const hasCanDisplayType = window.cast && cast.__platform__ && - cast.__platform__.canDisplayType; - - // Some hub devices can only do 720p. Default to that. - maxResolution.width = 1280; - maxResolution.height = 720; - - try { - if (hasCanDisplayType && await cast.__platform__.canDisplayType( - 'video/mp4; codecs="avc1.640028"; width=3840; height=2160')) { - // The device and display can both do 4k. Assume a 4k limit. - maxResolution.width = 3840; - maxResolution.height = 2160; - } else if (hasCanDisplayType && await cast.__platform__.canDisplayType( - 'video/mp4; codecs="avc1.640028"; width=1920; height=1080')) { - // Most Chromecasts can do 1080p. - maxResolution.width = 1920; - maxResolution.height = 1080; - } - } catch (error) { - // This shouldn't generally happen. Log the error. - shaka.log.alwaysError('Failed to check canDisplayType:', error); - // Now ignore the error and let the 720p default stand. - } - } else if (Platform.isTizen()) { - const devicePixelRatio = window.devicePixelRatio; - maxResolution.width = window.screen.width * devicePixelRatio > 1920 ? - 3840 : 1920; - maxResolution.height = window.screen.height * devicePixelRatio > 1080 ? - 2160 : 1080; - try { - if (webapis.systeminfo && webapis.systeminfo.getMaxVideoResolution) { - const maxVideoResolution = - webapis.systeminfo.getMaxVideoResolution(); - maxResolution.width = maxVideoResolution.width; - maxResolution.height = maxVideoResolution.height; - } else { - if (webapis.productinfo.is8KPanelSupported && - webapis.productinfo.is8KPanelSupported()) { - maxResolution.width = 7680; - maxResolution.height = 4320; - } else if (webapis.productinfo.isUdPanelSupported && - webapis.productinfo.isUdPanelSupported()) { - maxResolution.width = 3840; - maxResolution.height = 2160; - } - } - } catch (e) { - shaka.log.alwaysWarn('Tizen: Error detecting screen size, default ' + - 'screen size 1920x1080.'); - } - } else if (Platform.isWebOS()) { - try { - const deviceInfo = - /** @type {{screenWidth: number, screenHeight: number}} */( - JSON.parse(window.PalmSystem.deviceInfo)); - // WebOS has always been able to do 1080p. Assume a 1080p limit. - maxResolution.width = Math.max(1920, deviceInfo['screenWidth']); - maxResolution.height = Math.max(1080, deviceInfo['screenHeight']); - } catch (e) { - shaka.log.alwaysWarn('WebOS: Error detecting screen size, default ' + - 'screen size 1920x1080.'); - maxResolution.width = 1920; - maxResolution.height = 1080; - } - } else if (Platform.isHisense()) { - let supports4k = null; - if (window.Hisense_Get4KSupportState) { - try { - // eslint-disable-next-line new-cap - supports4k = window.Hisense_Get4KSupportState(); - } catch (e) { - shaka.log.debug('Hisense: Failed to get 4K support state', e); - } - } - if (supports4k == null) { - // If API is not there or not working for whatever reason, fallback to - // user agent check, as it contains UHD or FHD info. - supports4k = Platform.userAgentContains_('UHD'); - } - if (supports4k) { - maxResolution.width = 3840; - maxResolution.height = 2160; - } else { - maxResolution.width = 1920; - maxResolution.height = 1080; - } - } else if (Platform.isPS4() || Platform.isPS5()) { - let supports4K = false; - try { - const result = await window.msdk.device.getDisplayInfo(); - supports4K = result.resolution === '4K'; - } catch (e) { - try { - const result = await window.msdk.device.getDisplayInfoImmediate(); - supports4K = result.resolution === '4K'; - } catch (e) { - shaka.log.alwaysWarn( - 'PlayStation: Failed to get the display info:', e); - } - } - if (supports4K) { - maxResolution.width = 3840; - maxResolution.height = 2160; - } else { - maxResolution.width = 1920; - maxResolution.height = 1080; - } - } else { - // For Xbox and UWP apps. - let winRT = undefined; - try { - // Try to access to WinRT for WebView, if it's not defined, - // try to access to WinRT for WebView2, if it's not defined either, - // let it throw. - if (typeof Windows !== 'undefined') { - winRT = Windows; - } else { - winRT = chrome.webview.hostObjects.sync.Windows; - } - } catch (e) {} - if (winRT) { - maxResolution.width = 1920; - maxResolution.height = 1080; - try { - const protectionCapabilities = - new winRT.Media.Protection.ProtectionCapabilities(); - const protectionResult = - winRT.Media.Protection.ProtectionCapabilityResult; - // isTypeSupported may return "maybe", which means the operation - // is not completed. This means we need to retry - // https://learn.microsoft.com/en-us/uwp/api/windows.media.protection.protectioncapabilityresult?view=winrt-22621 - let result = null; - const type = - 'video/mp4;codecs="hvc1,mp4a";features="decode-res-x=3840,' + - 'decode-res-y=2160,decode-bitrate=20000,decode-fps=30,' + - 'decode-bpc=10,display-res-x=3840,display-res-y=2160,' + - 'display-bpc=8"'; - const keySystem = 'com.microsoft.playready.recommendation'; - do { - result = protectionCapabilities.isTypeSupported(type, keySystem); - } while (result === protectionResult.maybe); - if (result === protectionResult.probably) { - maxResolution.width = 3840; - maxResolution.height = 2160; - } - } catch (e) { - shaka.log.alwaysWarn('Xbox: Error detecting screen size, default ' + - 'screen size 1920x1080.'); - } - } else if (Platform.isXboxOne()) { - maxResolution.width = 1920; - maxResolution.height = 1080; - shaka.log.alwaysWarn('Xbox: Error detecting screen size, default ' + - 'screen size 1920x1080.'); - } - } - return maxResolution; - } - - - /** - * Check the current HDR level supported by the screen. - * - * @param {boolean} preferHLG - * @return {string} - */ - static getHdrLevel(preferHLG) { - if (window.matchMedia !== undefined && - window.matchMedia('(color-gamut: p3)').matches) { - return preferHLG ? 'HLG' : 'PQ'; - } - return 'SDR'; - } -}; - -/** @private {shaka.util.Timer} */ -shaka.util.Platform.cacheExpirationTimer_ = null; - -/** @private {HTMLMediaElement} */ -shaka.util.Platform.cachedMediaElement_ = null; diff --git a/lib/util/player_configuration.js b/lib/util/player_configuration.js index 8000993c0..577381509 100644 --- a/lib/util/player_configuration.js +++ b/lib/util/player_configuration.js @@ -12,6 +12,7 @@ goog.require('shaka.config.AutoShowText'); goog.require('shaka.config.CodecSwitchingStrategy'); goog.require('shaka.config.CrossBoundaryStrategy'); goog.require('shaka.config.RepeatMode'); +goog.require('shaka.device.DeviceFactory'); goog.require('shaka.drm.DrmUtils'); goog.require('shaka.drm.FairPlay'); goog.require('shaka.log'); @@ -21,7 +22,6 @@ goog.require('shaka.net.NetworkingEngine'); goog.require('shaka.util.ConfigUtils'); goog.require('shaka.util.LanguageUtils'); goog.require('shaka.util.ManifestParserUtils'); -goog.require('shaka.util.Platform'); /** @@ -41,6 +41,8 @@ shaka.util.PlayerConfiguration = class { let abrMaxHeight = Infinity; + const device = shaka.device.DeviceFactory.getDevice(); + // Some browsers implement the Network Information API, which allows // retrieving information about a user's network connection. if (navigator.connection) { @@ -92,36 +94,17 @@ shaka.util.PlayerConfiguration = class { updateExpirationTime: 1, preferredKeySystems: [], keySystemsMapping: {}, - // The Xbox One browser does not detect DRM key changes signalled by a - // change in the PSSH in media segments. We need to parse PSSH from media - // segments to detect key changes. - parseInbandPsshEnabled: shaka.util.Platform.isXboxOne(), + parseInbandPsshEnabled: false, minHdcpVersion: '', - ignoreDuplicateInitData: !shaka.util.Platform.isTizen2(), + ignoreDuplicateInitData: true, defaultAudioRobustnessForWidevine: 'SW_SECURE_CRYPTO', defaultVideoRobustnessForWidevine: 'SW_SECURE_DECODE', }; - // The Xbox One and PS4 only support the Playready DRM, so they should - // prefer that key system by default to improve startup performance. - if (shaka.util.Platform.isXboxOne() || - shaka.util.Platform.isPS4()) { - drm.preferredKeySystems.push('com.microsoft.playready'); - } - - // Other browsers different than Edge only supports HW PlayReady with the - // recommendation keysystem on Windows, so we do a direct mapping here. - if (shaka.util.Platform.isWindows() && !shaka.util.Platform.isEdge()) { - drm.keySystemsMapping = { - 'com.microsoft.playready': - 'com.microsoft.playready.recommendation', - }; - } - let codecSwitchingStrategy = shaka.config.CodecSwitchingStrategy.RELOAD; let multiTypeVariantsAllowed = false; if (shaka.media.Capabilities.isChangeTypeSupported() && - shaka.util.Platform.supportsSmoothCodecSwitching()) { + device.supportsSmoothCodecSwitching()) { codecSwitchingStrategy = shaka.config.CodecSwitchingStrategy.SMOOTH; multiTypeVariantsAllowed = true; } @@ -186,7 +169,7 @@ shaka.util.PlayerConfiguration = class { mediaPlaylistFullMimeType: 'video/mp2t; codecs="avc1.42E01E, mp4a.40.2"', liveSegmentsDelay: 3, - sequenceMode: shaka.util.Platform.supportsSequenceMode(), + sequenceMode: device.supportsSequenceMode(), ignoreManifestTimestampsInSegmentsMode: false, disableCodecGuessing: false, disableClosedCaptionsDetection: false, @@ -281,47 +264,14 @@ shaka.util.PlayerConfiguration = class { vodDynamicPlaybackRateBufferRatio: 0.5, preloadNextUrlWindow: 30, loadTimeout: 30, - clearDecodingCache: shaka.util.Platform.isPS4() || - shaka.util.Platform.isPS5(), + clearDecodingCache: false, dontChooseCodecs: false, - shouldFixTimestampOffset: shaka.util.Platform.isWebOS() || - shaka.util.Platform.isTizen(), + shouldFixTimestampOffset: false, avoidEvictionOnQuotaExceededError: false, crossBoundaryStrategy: shaka.config.CrossBoundaryStrategy.KEEP, returnToEndOfLiveWindowWhenOutside: false, }; - // WebOS, Tizen, Chromecast and Hisense have long hardware pipelines - // that respond slowly to seeking. - // Therefore we should not seek when we detect a stall - // on one of these platforms. Instead, default stallSkip to 0 to force the - // stall detector to pause and play instead. - if (shaka.util.Platform.isWebOS() || - shaka.util.Platform.isTizen() || - shaka.util.Platform.isChromecast() || - shaka.util.Platform.isHisense()) { - streaming.stallSkip = 0; - } - - if (shaka.util.Platform.isLegacyEdge() || - shaka.util.Platform.isXboxOne()) { - streaming.gapPadding = 0.01; - } - - if (shaka.util.Platform.isTizen()) { - streaming.gapPadding = 2; - } - - if (shaka.util.Platform.isWebOS3()) { - streaming.crossBoundaryStrategy = - shaka.config.CrossBoundaryStrategy.RESET; - } - - if (shaka.util.Platform.isTizen3()) { - streaming.crossBoundaryStrategy = - shaka.config.CrossBoundaryStrategy.RESET_TO_ENCRYPTED; - } - const networking = { forceHTTP: false, forceHTTPS: false, @@ -397,7 +347,7 @@ shaka.util.PlayerConfiguration = class { clearBufferSwitch: false, safeMarginSwitch: 0, cacheLoadThreshold: 20, - minTimeToSwitch: shaka.util.Platform.isApple() ? 0.5 : 0, + minTimeToSwitch: 0, preferNetworkInformationBandwidth: false, removeLatencyFromFirstPacketTime: true, }; @@ -446,19 +396,10 @@ shaka.util.PlayerConfiguration = class { durationReductionEmitsUpdateEnd: true, }; - let customPlayheadTracker = false; - let skipPlayDetection = false; - let supportsMultipleMediaElements = true; - if (shaka.util.Platform.isSmartTV()) { - customPlayheadTracker = true; - skipPlayDetection = true; - supportsMultipleMediaElements = false; - } - const ads = { - customPlayheadTracker, - skipPlayDetection, - supportsMultipleMediaElements, + customPlayheadTracker: false, + skipPlayDetection: false, + supportsMultipleMediaElements: true, disableHLSInterstitial: false, disableDASHInterstitial: false, allowPreloadOnDomElements: true, @@ -541,7 +482,7 @@ shaka.util.PlayerConfiguration = class { config.preferredVideoHdrLevel); }; - return config; + return device.adjustConfig(config); } /** @@ -627,7 +568,8 @@ shaka.util.PlayerConfiguration = class { } return false; }); - hdrLevel = shaka.util.Platform.getHdrLevel(someHLG); + const device = shaka.device.DeviceFactory.getDevice(); + hdrLevel = device.getHdrLevel(someHLG); } /** @type {!Array} */ diff --git a/lib/util/stream_utils.js b/lib/util/stream_utils.js index 856001bd3..127180faa 100644 --- a/lib/util/stream_utils.js +++ b/lib/util/stream_utils.js @@ -8,6 +8,8 @@ goog.provide('shaka.util.StreamUtils'); goog.require('goog.asserts'); goog.require('shaka.config.AutoShowText'); +goog.require('shaka.device.DeviceFactory'); +goog.require('shaka.device.IDevice'); goog.require('shaka.lcevc.Dec'); goog.require('shaka.log'); goog.require('shaka.media.Capabilities'); @@ -18,7 +20,6 @@ goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.util.MimeUtils'); goog.require('shaka.util.MultiMap'); goog.require('shaka.util.ObjectUtils'); -goog.require('shaka.util.Platform'); goog.requireType('shaka.drm.DrmEngine'); @@ -426,7 +427,8 @@ shaka.util.StreamUtils = class { goog.asserts.assert(navigator.mediaCapabilities, 'MediaCapabilities should be valid.'); - if (shaka.util.Platform.isXboxOne()) { + if (shaka.device.DeviceFactory.getDevice() + .shouldOverrideDolbyVisionCodecs()) { shaka.util.StreamUtils.overrideDolbyVisionCodecs(manifest.variants); } await shaka.util.StreamUtils.getDecodingInfosForVariants( @@ -521,9 +523,11 @@ shaka.util.StreamUtils = class { return false; } - const isXboxOne = shaka.util.Platform.isXboxOne(); - const isFirefoxAndroid = shaka.util.Platform.isFirefox() && - shaka.util.Platform.isAndroid(); + const device = shaka.device.DeviceFactory.getDevice(); + const isXboxOne = device.getDeviceName() === 'Xbox'; + const isFirefoxAndroid = + device.getDeviceType() === shaka.device.IDevice.DeviceType.MOBILE && + device.getBrowserEngine() === shaka.device.IDevice.BrowserEngine.GECKO; // See: https://github.com/shaka-project/shaka-player/issues/3860 const video = variant.video; @@ -671,7 +675,9 @@ shaka.util.StreamUtils = class { // is not set. This is a workaround for that issue. const TIMEOUT_FOR_DECODING_INFO_IN_SECONDS = 5; let promise; - if (shaka.util.Platform.isAndroid()) { + const device = shaka.device.DeviceFactory.getDevice(); + if (device.getDeviceType() == + shaka.device.IDevice.DeviceType.MOBILE) { promise = shaka.util.Functional.promiseWithTimeout( TIMEOUT_FOR_DECODING_INFO_IN_SECONDS, navigator.mediaCapabilities.decodingInfo(copy), @@ -1067,8 +1073,10 @@ shaka.util.StreamUtils = class { // currently don't support 'fLaC', while 'flac' is supported by most // major browsers. // See https://bugs.chromium.org/p/chromium/issues/detail?id=1422728 + const device = shaka.device.DeviceFactory.getDevice(); + const webkit = shaka.device.IDevice.BrowserEngine.WEBKIT; if (codecs.toLowerCase() == 'flac') { - if (!shaka.util.Platform.isApple()) { + if (device.getBrowserEngine() != webkit) { return 'flac'; } else { return 'fLaC'; @@ -1077,7 +1085,7 @@ shaka.util.StreamUtils = class { // The same is true for 'Opus'. if (codecs.toLowerCase() === 'opus') { - if (!shaka.util.Platform.isApple()) { + if (device.getBrowserEngine() != webkit) { return 'opus'; } else { if (shaka.util.MimeUtils.getContainerType(mimeType) == 'mp4') { @@ -1088,8 +1096,7 @@ shaka.util.StreamUtils = class { } } - if (codecs.toLowerCase() == 'ac-3' && - shaka.util.Platform.requiresEC3InitSegments()) { + if (codecs.toLowerCase() == 'ac-3' && device.requiresEC3InitSegments()) { return 'ec-3'; } diff --git a/lib/util/string_utils.js b/lib/util/string_utils.js index 5601d1a3e..984d8b96e 100644 --- a/lib/util/string_utils.js +++ b/lib/util/string_utils.js @@ -7,11 +7,11 @@ goog.provide('shaka.util.StringUtils'); goog.require('goog.asserts'); +goog.require('shaka.device.DeviceFactory'); goog.require('shaka.log'); goog.require('shaka.util.BufferUtils'); goog.require('shaka.util.Error'); goog.require('shaka.util.Lazy'); -goog.require('shaka.util.Platform'); /** @@ -38,7 +38,8 @@ shaka.util.StringUtils = class { uint8 = uint8.subarray(3); } - if (window.TextDecoder && !shaka.util.Platform.isPS4()) { + if (window.TextDecoder && !shaka.device.DeviceFactory.getDevice() + .shouldAvoidUseTextDecoderEncoder()) { // Use the TextDecoder interface to decode the text. This has the // advantage compared to the previously-standard decodeUriComponent that // it will continue parsing even if it finds an invalid UTF8 character, @@ -208,7 +209,8 @@ shaka.util.StringUtils = class { * @export */ static toUTF8(str) { - if (window.TextEncoder && !shaka.util.Platform.isPS4()) { + if (window.TextEncoder && !shaka.device.DeviceFactory.getDevice() + .shouldAvoidUseTextDecoderEncoder()) { const utf8Encoder = new TextEncoder(); return shaka.util.BufferUtils.toArrayBuffer(utf8Encoder.encode(str)); } else { diff --git a/shaka-player.uncompiled.js b/shaka-player.uncompiled.js index 4a15e98c7..82dcc4600 100644 --- a/shaka-player.uncompiled.js +++ b/shaka-player.uncompiled.js @@ -16,6 +16,16 @@ goog.require('shaka.ads.AdManager'); goog.require('shaka.cast.CastProxy'); goog.require('shaka.cast.CastReceiver'); goog.require('shaka.dash.DashParser'); +goog.require('shaka.device.AppleBrowser'); +goog.require('shaka.device.Chromecast'); +goog.require('shaka.device.DefaultBrowser'); +goog.require('shaka.device.Hisense'); +goog.require('shaka.device.PlayStation'); +goog.require('shaka.device.Tizen'); +goog.require('shaka.device.Vizio'); +goog.require('shaka.device.WebKitSTB'); +goog.require('shaka.device.WebOS'); +goog.require('shaka.device.Xbox'); goog.require('shaka.drm.FairPlay'); goog.require('shaka.hls.HlsParser'); goog.require('shaka.mss.MssParser'); diff --git a/test/ads/interstitial_ad_manager_unit.js b/test/ads/interstitial_ad_manager_unit.js index 167301e94..6b7d115f8 100644 --- a/test/ads/interstitial_ad_manager_unit.js +++ b/test/ads/interstitial_ad_manager_unit.js @@ -24,7 +24,8 @@ describe('Interstitial Ad manager', () => { beforeEach(() => { // Allows us to use a timer instead of requestVideoFrameCallback // (which doesn't work well in all platform tests) - spyOn(shaka.util.Platform, 'isSmartTV').and.returnValue(true); + spyOn(deviceDetected, 'getDeviceType') + .and.returnValue(shaka.device.IDevice.DeviceType.TV); function dependencyInjector(player) { // Create a networking engine that always returns an empty buffer. diff --git a/test/ads_integration.js b/test/ads_integration.js index 85f1306cd..c568aa7b3 100644 --- a/test/ads_integration.js +++ b/test/ads_integration.js @@ -95,7 +95,7 @@ describe('Ads', () => { const streamUri = '/base/test/test/assets/hls-interstitial/main.m3u8'; it('with support for multiple media elements', async () => { - if (shaka.util.Platform.isSmartTV()) { + if (!player.getConfiguration().ads.supportsMultipleMediaElements) { pending('Platform without support for multiple media elements.'); } player.configure('ads.supportsMultipleMediaElements', true); diff --git a/test/cast/cast_receiver_integration.js b/test/cast/cast_receiver_integration.js index 4c0e8026e..e751c6604 100644 --- a/test/cast/cast_receiver_integration.js +++ b/test/cast/cast_receiver_integration.js @@ -10,7 +10,8 @@ // only be run on Chrome and Chromecast. /** @return {boolean} */ const castReceiverIntegrationSupport = - () => shaka.util.Platform.isChrome() || shaka.util.Platform.isChromecast(); + () => deviceDetected.getDeviceName() === 'Chrome' || + deviceDetected.getDeviceType() === shaka.device.IDevice.DeviceType.CAST; filterDescribe('CastReceiver', castReceiverIntegrationSupport, () => { const CastReceiver = shaka.cast.CastReceiver; const CastUtils = shaka.cast.CastUtils; diff --git a/test/cast/cast_receiver_unit.js b/test/cast/cast_receiver_unit.js index 868cac60b..8d2882689 100644 --- a/test/cast/cast_receiver_unit.js +++ b/test/cast/cast_receiver_unit.js @@ -10,7 +10,8 @@ // only be run on Chrome and Chromecast. /** @return {boolean} */ const castReceiverSupport = - () => shaka.util.Platform.isChrome() || shaka.util.Platform.isChromecast(); + () => deviceDetected.getDeviceName() === 'Chrome' || + deviceDetected.getDeviceType() === shaka.device.IDevice.DeviceType.CAST; filterDescribe('CastReceiver', castReceiverSupport, () => { const CastReceiver = shaka.cast.CastReceiver; const CastUtils = shaka.cast.CastUtils; @@ -54,6 +55,8 @@ filterDescribe('CastReceiver', castReceiverSupport, () => { mockReceiverApi = createMockReceiverApi(); mockCanDisplayType = jasmine.createSpy('canDisplayType'); mockCanDisplayType.and.returnValue(false); + spyOn(deviceDetected, 'getDeviceType').and + .returnValue(shaka.device.IDevice.DeviceType.CAST); // We're using quotes to access window.cast because the compiler // knows about lots of Cast-specific APIs we aren't mocking. We diff --git a/test/codec_switching/codec_switching_integration.js b/test/codec_switching/codec_switching_integration.js index c29ce5eaa..8283fc9d2 100644 --- a/test/codec_switching/codec_switching_integration.js +++ b/test/codec_switching/codec_switching_integration.js @@ -96,7 +96,7 @@ describe('Codec Switching', () => { if (!shaka.media.Capabilities.isChangeTypeSupported()) { pending('SourceBuffer.changeType is not supported'); } - if (!shaka.util.Platform.supportsSmoothCodecSwitching()) { + if (!deviceDetected.supportsSmoothCodecSwitching()) { pending('SourceBuffer.changeType is not considered ' + 'reliable on this device'); } @@ -172,7 +172,7 @@ describe('Codec Switching', () => { if (!shaka.media.Capabilities.isChangeTypeSupported()) { pending('SourceBuffer.changeType is not supported'); } - if (!shaka.util.Platform.supportsSmoothCodecSwitching()) { + if (!deviceDetected.supportsSmoothCodecSwitching()) { pending('SourceBuffer.changeType is not considered ' + 'reliable on this device'); } @@ -248,7 +248,7 @@ describe('Codec Switching', () => { if (!shaka.media.Capabilities.isChangeTypeSupported()) { pending('SourceBuffer.changeType is not supported'); } - if (!shaka.util.Platform.supportsSmoothCodecSwitching()) { + if (!deviceDetected.supportsSmoothCodecSwitching()) { pending('SourceBuffer.changeType is not considered ' + 'reliable on this device'); } @@ -325,7 +325,7 @@ describe('Codec Switching', () => { if (!shaka.media.Capabilities.isChangeTypeSupported()) { pending('SourceBuffer.changeType is not supported'); } - if (!shaka.util.Platform.supportsSmoothCodecSwitching()) { + if (!deviceDetected.supportsSmoothCodecSwitching()) { pending('SourceBuffer.changeType is not considered ' + 'reliable on this device'); } diff --git a/test/device/chromecast_unit.js b/test/device/chromecast_unit.js new file mode 100644 index 000000000..06b561057 --- /dev/null +++ b/test/device/chromecast_unit.js @@ -0,0 +1,40 @@ +/*! @license + * Shaka Player + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +describe('Chromecast', () => { + /* eslint-disable @stylistic/max-len */ + // cspell: disable-next-line + const vizio = 'Mozilla/5.0 (X11; Linux aarch64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36 CrKey/1.0.999999 VIZIO SmartCast(Conjure/MTKF-5.1.516.1 FW/0.6.11.1-2 Model/V50C6-J09)'; + // cspell: disable-next-line + const chromecastBuiltinOrOlder = 'Mozilla/5.0 (X11; Linux aarch64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.225 Safari/537.36 CrKey/1.56.500000 DeviceType/Chromecast'; + // cspell: disable-next-line + const chromecastFuchsia = 'Mozilla/5.0 (Fuchsia) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 CrKey/1.56.500000'; + // cspell: disable-next-line + const chromecastAndroid = 'Mozilla/5.0 (Linux; Android 12; Build/STTL.240206.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.0 Safari/537.36 CrKey/1.56.500000 DeviceType/AndroidTV'; + /* eslint-enable @stylistic/max-len */ + + const Chromecast = shaka.device.Chromecast; + const Util = shaka.test.Util; + const originalUserAgent = navigator.userAgent; + const userAgentData = navigator.userAgentData; + + afterEach(() => { + Util.setUserAgent(originalUserAgent); + Util.setUserAgentData(userAgentData); + }); + + it('checks Chromecast OS type', () => { + Util.setUserAgentData(null); + Util.setUserAgent(vizio); + expect(() => new Chromecast()).toThrow(); + Util.setUserAgent(chromecastBuiltinOrOlder); + expect(new Chromecast().getDeviceName()).toContain('Linux'); + Util.setUserAgent(chromecastFuchsia); + expect(new Chromecast().getDeviceName()).toContain('Fuchsia'); + Util.setUserAgent(chromecastAndroid); + expect(new Chromecast().getDeviceName()).toContain('Android'); + }); +}); diff --git a/test/device/tizen_unit.js b/test/device/tizen_unit.js new file mode 100644 index 000000000..72be3c1fd --- /dev/null +++ b/test/device/tizen_unit.js @@ -0,0 +1,40 @@ +/*! @license + * Shaka Player + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +describe('Tizen', () => { + /* eslint-disable @stylistic/max-len */ + // See: https://developer.samsung.com/smarttv/develop/guides/fundamentals/retrieving-platform-information.html + const tizen50 = 'Mozilla/5.0 (SMART-TV; LINUX; Tizen 5.0) AppleWebKit/537.36 (KHTML, like Gecko) Version/5.0 TV Safari/537.36'; + const tizen55 = 'Mozilla/5.0 (SMART-TV; LINUX; Tizen 5.5) AppleWebKit/537.36 (KHTML, like Gecko) 69.0.3497.106.1/5.5 TV Safari/537.36'; + const tizen60 = 'Mozilla/5.0 (SMART-TV; LINUX; Tizen 6.0) AppleWebKit/537.36 (KHTML, like Gecko) 76.0.3809.146/6.0 TV Safari/537.36'; + const tizen65 = 'Mozilla/5.0 (SMART-TV; LINUX; Tizen 6.5) AppleWebKit/537.36 (KHTML, like Gecko) 85.0.4183.93/6.5 TV Safari/537.36'; + const tizen70 = 'Mozilla/5.0 (SMART-TV; LINUX; Tizen 7.0) AppleWebKit/537.36 (KHTML, like Gecko) 94.0.4606.31/7.0 TV Safari/537.36'; + + const webOs3 = 'Mozilla/5.0 (Web0S; Linux/SmartTV) AppleWebKit/537.36 (KHTML, like Gecko) QtWebEngine/5.2.1 Chrome/38.0.2125.122 Safari/537.36 WebAppManager'; + /* eslint-enable @stylistic/max-len */ + + const Util = shaka.test.Util; + const originalUserAgent = navigator.userAgent; + + afterEach(() => { + Util.setUserAgent(originalUserAgent); + }); + + it('checks Tizen version', () => { + Util.setUserAgent(webOs3); + expect(new shaka.device.Tizen().getVersion()).toBe(null); + Util.setUserAgent(tizen50); + expect(new shaka.device.Tizen().getVersion()).toBe(5); + Util.setUserAgent(tizen55); + expect(new shaka.device.Tizen().getVersion()).toBe(5); + Util.setUserAgent(tizen60); + expect(new shaka.device.Tizen().getVersion()).toBe(6); + Util.setUserAgent(tizen65); + expect(new shaka.device.Tizen().getVersion()).toBe(6); + Util.setUserAgent(tizen70); + expect(new shaka.device.Tizen().getVersion()).toBe(7); + }); +}); diff --git a/test/device/webos_unit.js b/test/device/webos_unit.js new file mode 100644 index 000000000..385e73fbc --- /dev/null +++ b/test/device/webos_unit.js @@ -0,0 +1,37 @@ +/*! @license + * Shaka Player + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +describe('WebOS', () => { + /* eslint-disable @stylistic/max-len */ + // See: https://webostv.developer.lge.com/develop/specifications/web-api-and-web-engine#useragent-string + const webOs3 = 'Mozilla/5.0 (Web0S; Linux/SmartTV) AppleWebKit/537.36 (KHTML, like Gecko) QtWebEngine/5.2.1 Chrome/38.0.2125.122 Safari/537.36 WebAppManager'; + const webOs4 = 'Mozilla/5.0 (Web0S; Linux/SmartTV) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.34 Safari/537.36 WebAppManager'; + const webOs5 = 'Mozilla/5.0 (Web0S; Linux/SmartTV) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36 WebAppManager'; + const webOs6 = 'Mozilla/5.0 (Web0S; Linux/SmartTV) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.79 Safari/537.36 WebAppManager'; + + const tizen50 = 'Mozilla/5.0 (SMART-TV; LINUX; Tizen 5.0) AppleWebKit/537.36 (KHTML, like Gecko) Version/5.0 TV Safari/537.36'; + /* eslint-enable @stylistic/max-len */ + + const Util = shaka.test.Util; + const originalUserAgent = navigator.userAgent; + + afterEach(() => { + Util.setUserAgent(originalUserAgent); + }); + + it('checks webOS version', () => { + Util.setUserAgent(tizen50); + expect(new shaka.device.WebOS().getVersion()).toBe(null); + Util.setUserAgent(webOs3); + expect(new shaka.device.WebOS().getVersion()).toBe(3); + Util.setUserAgent(webOs4); + expect(new shaka.device.WebOS().getVersion()).toBe(4); + Util.setUserAgent(webOs5); + expect(new shaka.device.WebOS().getVersion()).toBe(5); + Util.setUserAgent(webOs6); + expect(new shaka.device.WebOS().getVersion()).toBe(6); + }); +}); diff --git a/test/lcevc/lcevc_integration.js b/test/lcevc/lcevc_integration.js index 5cd9ed66d..fe698333f 100644 --- a/test/lcevc/lcevc_integration.js +++ b/test/lcevc/lcevc_integration.js @@ -104,7 +104,7 @@ describe('LCEVC Integration', () => { describe('SEI Integration', () => { it('Should decode LCEVC in FMP4 DASH manifest', async () => { - if (shaka.util.Platform.isTizen() || shaka.util.Platform.isChromecast()) { + if (isPlatformUnsupported()) { pending('Disabled on unsupported platform.'); } @@ -117,7 +117,7 @@ describe('LCEVC Integration', () => { }); it('Should decode LCEVC in FMP4 HLS manifest', async () => { - if (shaka.util.Platform.isTizen() || shaka.util.Platform.isChromecast()) { + if (isPlatformUnsupported()) { pending('Disabled on unsupported platform.'); } @@ -130,7 +130,7 @@ describe('LCEVC Integration', () => { }); it('Should decode LCEVC in TS HLS manifest', async () => { - if (shaka.util.Platform.isTizen() || shaka.util.Platform.isChromecast()) { + if (isPlatformUnsupported()) { pending('Disabled on unsupported platform.'); } @@ -142,4 +142,12 @@ describe('LCEVC Integration', () => { await testPlayback(seiManifests.TS_HLS); }); }); + + /** @return {boolean} */ + function isPlatformUnsupported() { + const DeviceType = shaka.device.IDevice.DeviceType; + return deviceDetected.getDeviceType() === DeviceType.CAST || + (deviceDetected.getDeviceType() === DeviceType.TV && + deviceDetected.getDeviceName() === 'Tizen'); + } }); diff --git a/test/media/content_workarounds_integration.js b/test/media/content_workarounds_integration.js index c70ff407c..f0f353264 100644 --- a/test/media/content_workarounds_integration.js +++ b/test/media/content_workarounds_integration.js @@ -108,7 +108,8 @@ describe('ContentWorkarounds', () => { if (!shakaSupport.drm[keySystem]) { pending('Needed DRM is not supported on this platform'); } - if (shaka.util.Platform.isTizen3()) { + if (deviceDetected.getDeviceName() === 'Tizen' && + deviceDetected.getVersion() === 3) { pending('Tizen 3 currently does not support mixed clear ' + 'encrypted content'); } diff --git a/test/media/content_workarounds_unit.js b/test/media/content_workarounds_unit.js index 3ede91f0d..976aac479 100644 --- a/test/media/content_workarounds_unit.js +++ b/test/media/content_workarounds_unit.js @@ -61,8 +61,8 @@ describe('ContentWorkarounds', () => { } it('faked encryption on Edge returns two init segments', () => { - spyOn(shaka.util.Platform, 'isEdge').and.returnValue(true); - spyOn(shaka.util.Platform, 'isWindows').and.returnValue(true); + spyOn(deviceDetected, 'requiresClearAndEncryptedInitSegments') + .and.returnValue(true); const unencrypted = new Uint8Array([ 0x00, 0x00, 0x00, 0x20, // size diff --git a/test/media/media_source_engine_integration.js b/test/media/media_source_engine_integration.js index 8bf64e013..bbcdfc793 100644 --- a/test/media/media_source_engine_integration.js +++ b/test/media/media_source_engine_integration.js @@ -710,7 +710,7 @@ describe('MediaSourceEngine', () => { it('extracts ID3 metadata from AAC', async () => { if (!MediaSource.isTypeSupported('audio/aac') || - !shaka.util.Platform.supportsSequenceMode()) { + !deviceDetected.supportsSequenceMode()) { pending('Raw AAC codec is not supported by the platform.'); } metadata = shaka.test.TestScheme.DATA['id3-metadata_aac']; diff --git a/test/media/media_source_engine_unit.js b/test/media/media_source_engine_unit.js index 77c0b4939..df27aa35f 100644 --- a/test/media/media_source_engine_unit.js +++ b/test/media/media_source_engine_unit.js @@ -213,10 +213,10 @@ describe('MediaSourceEngine', () => { shaka.media.MediaSourceEngine.prototype.createMediaSource = Util.spyFunc(createMediaSourceSpy); - requiresEncryptionInfoInAllInitSegmentsSpy = spyOn(shaka.util.Platform, + requiresEncryptionInfoInAllInitSegmentsSpy = spyOn(deviceDetected, 'requiresEncryptionInfoInAllInitSegments').and.returnValue(false); - requiresEC3InitSegments = spyOn(shaka.util.Platform, + requiresEC3InitSegments = spyOn(deviceDetected, 'requiresEC3InitSegments').and.returnValue(false); fakeEncryptionSpy = spyOn(shaka.media.ContentWorkarounds, 'fakeEncryption') diff --git a/test/media/playhead_unit.js b/test/media/playhead_unit.js index a131741cb..1f0059cf3 100644 --- a/test/media/playhead_unit.js +++ b/test/media/playhead_unit.js @@ -173,9 +173,10 @@ describe('Playhead', () => { function calculateGap(time) { let jumpTo = time; - if (shaka.util.Platform.isLegacyEdge() || - shaka.util.Platform.isXboxOne() || - shaka.util.Platform.isTizen()) { + if (deviceDetected.getBrowserEngine() === + shaka.device.IDevice.BrowserEngine.EDGE || + deviceDetected.getDeviceName() === 'Xbox' || + deviceDetected.getDeviceName() === 'Tizen') { const gapPadding = shaka.util.PlayerConfiguration.createDefault() .streaming.gapPadding; jumpTo = Math.ceil((jumpTo + gapPadding) * 100) / 100; @@ -695,7 +696,7 @@ describe('Playhead', () => { }); // does not clamp playhead if setLiveSeekableRange is used it('doesn\'t repeatedly re-seek in seeking slow platforms', () => { - if (!shaka.util.Platform.isSeekingSlow()) { + if (!deviceDetected.seekDelay()) { pending('No seeking slow platform'); } video.readyState = HTMLMediaElement.HAVE_METADATA; diff --git a/test/media/streaming_engine_integration.js b/test/media/streaming_engine_integration.js index 600391af0..25bb48864 100644 --- a/test/media/streaming_engine_integration.js +++ b/test/media/streaming_engine_integration.js @@ -302,7 +302,7 @@ describe('StreamingEngine', () => { // Experimentally, we find that playback rates above 2x in this test seem // to cause decoder failures on Tizen 3. This is out of our control, and // seems to be a Tizen bug, so this test is skipped on Tizen completely. - if (shaka.util.Platform.isTizen()) { + if (deviceDetected.getDeviceName() === 'Tizen') { pending('High playbackRate tests cause decoder errors on Tizen 3.'); } diff --git a/test/offline/offline_integration.js b/test/offline/offline_integration.js index 9a39e30d3..34fec446c 100644 --- a/test/offline/offline_integration.js +++ b/test/offline/offline_integration.js @@ -85,7 +85,8 @@ filterDescribe('Offline', supportsStorage, () => { pending('Widevine persistent licenses are not supported'); return; } - if (shaka.util.Platform.isAndroid()) { + if (deviceDetected.getDeviceType() === + shaka.device.IDevice.DeviceType.MOBILE) { pending('Skipping offline DRM tests on Android - crbug.com/1108158'); return; } @@ -121,12 +122,13 @@ filterDescribe('Offline', supportsStorage, () => { pending('Widevine and PlayReady are not supported'); return; } - if (shaka.util.Platform.isAndroid()) { + if (deviceDetected.getDeviceType() === + shaka.device.IDevice.DeviceType.MOBILE) { pending('Skipping offline DRM tests on Android - crbug.com/1108158'); return; } - if (shaka.util.Platform.isXboxOne()) { + if (deviceDetected.getDeviceName() === 'Xbox') { // Axinom won't issue a license for an Xbox One. The error message // from the license server says "Your DRM client's security level is // 150, but the entitlement message requires 2000 or higher." diff --git a/test/offline/storage_playback_integration.js b/test/offline/storage_playback_integration.js index 8292c8833..0c0446271 100644 --- a/test/offline/storage_playback_integration.js +++ b/test/offline/storage_playback_integration.js @@ -160,7 +160,8 @@ filterDescribe('Storage', checkStorageSupport, () => { it('supports MSS download and playback', async () => { // This tests is flaky in some Chromecast devices, so we need omit it // for now. - if (shaka.util.Platform.isChromecast()) { + if (deviceDetected.getDeviceType() === + shaka.device.IDevice.DeviceType.CAST) { pending('Disabled on Chromecast.'); } const url = '/base/test/test/assets/mss-clear/Manifest'; diff --git a/test/player_cross_boundary_integration.js b/test/player_cross_boundary_integration.js index cb84ded0f..8ba123132 100644 --- a/test/player_cross_boundary_integration.js +++ b/test/player_cross_boundary_integration.js @@ -68,7 +68,8 @@ describe('Player Cross Boundary', () => { // PlayReady on Chromecast is deprecated, so we prefer to use the DRM // that is officially supported. - if (shaka.util.Platform.isChromecast()) { + if (deviceDetected.getDeviceType() === + shaka.device.IDevice.DeviceType.CAST) { player.configure({ drm: { preferredKeySystems: ['com.widevine.alpha'], diff --git a/test/player_dolby_vision_integration.js b/test/player_dolby_vision_integration.js index c30b383ee..5879d5d67 100644 --- a/test/player_dolby_vision_integration.js +++ b/test/player_dolby_vision_integration.js @@ -79,7 +79,8 @@ describe('Player Dolby Vision', () => { describe('P8 with fallback to HEVC', () => { it('with DASH', async () => { // This tests is flaky in Safari, so we need omit it for now. - if (shaka.util.Platform.isApple()) { + if (deviceDetected.getBrowserEngine() === + shaka.device.IDevice.BrowserEngine.WEBKIT) { pending('Disabled on Safari.'); } if (!await Util.isTypeSupported('video/mp4; codecs="hvc1.2.4.L90.90"', @@ -91,7 +92,8 @@ describe('Player Dolby Vision', () => { it('with master playlist (HLS)', async () => { // This tests is flaky in Safari, so we need omit it for now. - if (shaka.util.Platform.isApple()) { + if (deviceDetected.getBrowserEngine() === + shaka.device.IDevice.BrowserEngine.WEBKIT) { pending('Disabled on Safari.'); } if (!await Util.isTypeSupported('video/mp4; codecs="hvc1.2.4.L90.90"', @@ -103,7 +105,8 @@ describe('Player Dolby Vision', () => { it('with media playlist (HLS)', async () => { // This tests is flaky in Safari, so we need omit it for now. - if (shaka.util.Platform.isApple()) { + if (deviceDetected.getBrowserEngine() === + shaka.device.IDevice.BrowserEngine.WEBKIT) { pending('Disabled on Safari.'); } if (!await Util.isTypeSupported('video/mp4; codecs="hvc1.2.4.L90.90"', diff --git a/test/player_integration.js b/test/player_integration.js index 17d1faa00..0f7b48e5e 100644 --- a/test/player_integration.js +++ b/test/player_integration.js @@ -635,7 +635,7 @@ describe('Player', () => { }); it('in sequence mode', async () => { - if (!shaka.util.Platform.supportsSequenceMode()) { + if (!deviceDetected.supportsSequenceMode()) { pending('Sequence mode is not supported by the platform.'); } await player.load('test:sintel_sequence_compiled'); diff --git a/test/player_unit.js b/test/player_unit.js index 71010da52..37f170f91 100644 --- a/test/player_unit.js +++ b/test/player_unit.js @@ -752,8 +752,8 @@ describe('Player', () => { it('only applies to DASH streams', async () => { video.canPlayType.and.returnValue('maybe'); - spyOn(shaka.util.Platform, 'anyMediaElement').and.returnValue(video); - spyOn(shaka.util.Platform, 'supportsMediaSource').and.returnValue(true); + spyOn(shaka.util.Dom, 'anyMediaElement').and.returnValue(video); + spyOn(deviceDetected, 'supportsMediaSource').and.returnValue(true); // Make sure player.load() resolves for src= spyOn(shaka.util.MediaReadyState, 'waitForReadyState').and.callFake( (mediaElement, readyState, eventManager, callback) => { @@ -773,7 +773,7 @@ describe('Player', () => { it('does not apply to non-DASH streams', async () => { video.canPlayType.and.returnValue('maybe'); - spyOn(shaka.util.Platform, 'supportsMediaSource').and.returnValue(true); + spyOn(deviceDetected, 'supportsMediaSource').and.returnValue(true); player.configure({ streaming: { @@ -808,9 +808,9 @@ describe('Player', () => { it('only applies to HLS streams', async () => { video.canPlayType.and.returnValue('maybe'); - spyOn(shaka.util.Platform, 'anyMediaElement').and.returnValue(video); - spyOn(shaka.util.Platform, 'supportsMediaSource').and.returnValue(true); - spyOn(shaka.util.Platform, 'isApple').and.returnValue(false); + spyOn(shaka.util.Dom, 'anyMediaElement').and.returnValue(video); + spyOn(deviceDetected, 'supportsMediaSource').and.returnValue(true); + spyOn(deviceDetected, 'getBrowserEngine').and.returnValue('UNKNOWN'); // Make sure player.load() resolves for src= spyOn(shaka.util.MediaReadyState, 'waitForReadyState').and.callFake( (mediaElement, readyState, eventManager, callback) => { @@ -830,8 +830,8 @@ describe('Player', () => { it('does not apply to non-HLS streams', async () => { video.canPlayType.and.returnValue('maybe'); - spyOn(shaka.util.Platform, 'supportsMediaSource').and.returnValue(true); - spyOn(shaka.util.Platform, 'isApple').and.returnValue(false); + spyOn(deviceDetected, 'supportsMediaSource').and.returnValue(true); + spyOn(deviceDetected, 'getBrowserEngine').and.returnValue('UNKNOWN'); player.configure({ streaming: { diff --git a/test/polyfill/media_capabilities_unit.js b/test/polyfill/media_capabilities_unit.js index dad1f8321..dbeade2ce 100644 --- a/test/polyfill/media_capabilities_unit.js +++ b/test/polyfill/media_capabilities_unit.js @@ -104,6 +104,8 @@ describe('MediaCapabilities', () => { it('should check codec support when MediaDecodingConfiguration.type ' + 'is "media-source"', async () => { expect(window['MediaSource']['isTypeSupported']).toBeDefined(); + spyOn(deviceDetected, 'getDeviceType').and + .returnValue(shaka.device.IDevice.DeviceType.DESKTOP); shaka.polyfill.MediaCapabilities.install(); await navigator.mediaCapabilities.decodingInfo(mockDecodingConfig); @@ -113,9 +115,8 @@ describe('MediaCapabilities', () => { it('should check codec support when MediaDecodingConfiguration.type ' + 'is "file"', async () => { - const supportsMediaTypeSpy = - spyOn(shaka['util']['Platform'], - 'supportsMediaType').and.returnValue(true); + const supportsMediaTypeSpy = spyOn(deviceDetected, 'supportsMediaType') + .and.returnValue(true); mockDecodingConfig.type = 'file'; shaka.polyfill.MediaCapabilities.install(); await navigator.mediaCapabilities.decodingInfo(mockDecodingConfig); @@ -193,22 +194,17 @@ describe('MediaCapabilities', () => { pending('Unable to delete window.cast'); } - spyOn(shaka['util']['Platform'], 'isAndroid').and.returnValue(false); - spyOn(shaka['util']['Platform'], 'isFuchsia').and.returnValue(false); - - const isChromecastSpy = - spyOn(shaka['util']['Platform'], - 'isChromecast').and.returnValue(true); + const isChromecastSpy = spyOn(deviceDetected, 'getDeviceType').and + .returnValue(shaka.device.IDevice.DeviceType.CAST); expect(window['MediaSource']['isTypeSupported']).toBeDefined(); shaka.polyfill.MediaCapabilities.install(); await navigator.mediaCapabilities.decodingInfo(mockDecodingConfig); expect(mockCanDisplayType).not.toHaveBeenCalled(); - // 1 (during install()) + // 1 (for video config check) + // 1 (for audio config check). - expect(isChromecastSpy).toHaveBeenCalledTimes(3); + expect(isChromecastSpy).toHaveBeenCalledTimes(2); // 1 (fallback in canCastDisplayType()) + // 1 (mockDecodingConfig.audio). expect(supportMap.has(mockDecodingConfig.video.contentType)) @@ -221,21 +217,17 @@ describe('MediaCapabilities', () => { async () => { // We only set the cast namespace, but not the canDisplayType() API. window['cast'] = {}; - spyOn(shaka['util']['Platform'], 'isAndroid').and.returnValue(false); - spyOn(shaka['util']['Platform'], 'isFuchsia').and.returnValue(false); - const isChromecastSpy = - spyOn(shaka['util']['Platform'], - 'isChromecast').and.returnValue(true); + const isChromecastSpy = spyOn(deviceDetected, 'getDeviceType').and + .returnValue(shaka.device.IDevice.DeviceType.CAST); expect(window['MediaSource']['isTypeSupported']).toBeDefined(); shaka.polyfill.MediaCapabilities.install(); await navigator.mediaCapabilities.decodingInfo(mockDecodingConfig); expect(mockCanDisplayType).not.toHaveBeenCalled(); - // 1 (during install()) + // 1 (for video config check) + // 1 (for audio config check). - expect(isChromecastSpy).toHaveBeenCalledTimes(3); + expect(isChromecastSpy).toHaveBeenCalledTimes(2); // 1 (fallback in canCastDisplayType()) + // 1 (mockDecodingConfig.audio). expect(supportMap.has(mockDecodingConfig.video.contentType)) @@ -252,11 +244,8 @@ describe('MediaCapabilities', () => { window['cast'] = { __platform__: {canDisplayType: mockCanDisplayType}, }; - spyOn(shaka['util']['Platform'], 'isAndroid').and.returnValue(false); - spyOn(shaka['util']['Platform'], 'isFuchsia').and.returnValue(false); - const isChromecastSpy = - spyOn(shaka['util']['Platform'], - 'isChromecast').and.returnValue(true); + const isChromecastSpy = spyOn(deviceDetected, 'getDeviceType').and + .returnValue(shaka.device.IDevice.DeviceType.CAST); expect(window['MediaSource']['isTypeSupported']).toBeDefined(); // Tests an HDR stream's extended MIME type is correctly provided. @@ -283,10 +272,9 @@ describe('MediaCapabilities', () => { shaka.polyfill.MediaCapabilities.install(); await navigator.mediaCapabilities.decodingInfo(mockDecodingConfig); - // 1 (during install()) + // 1 (for video config check) + // 1 (for audio config check). - expect(isChromecastSpy).toHaveBeenCalledTimes(3); + expect(isChromecastSpy).toHaveBeenCalledTimes(2); // 1 (mockDecodingConfig.audio). expect(supportMap.has(chromecastType)).toBe(true); // Called once in canCastDisplayType. diff --git a/test/test/boot.js b/test/test/boot.js index 0bbdc127e..32ca5e286 100644 --- a/test/test/boot.js +++ b/test/test/boot.js @@ -379,8 +379,15 @@ function configureJasmineEnvironment() { }); } - // Reset decoding config cache after each test. + const originalDevice = shaka.device.DeviceFactory.getDevice(); + goog.asserts.assert(originalDevice, 'device must be non-null'); + window.dump(originalDevice.toString()); + window.deviceDetected = originalDevice; + afterEach(/** @suppress {accessControls} */ () => { + goog.asserts.assert(originalDevice, 'device must be non-null'); + window.deviceDetected = originalDevice; + // Reset decoding config cache after each test. shaka.util.StreamUtils.clearDecodingConfigCache(); shaka.media.Capabilities.MediaSourceTypeSupportMap.clear(); }); diff --git a/test/test/externs/device.js b/test/test/externs/device.js new file mode 100644 index 000000000..660409ea6 --- /dev/null +++ b/test/test/externs/device.js @@ -0,0 +1,16 @@ +/*! @license + * Shaka Player + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview Externs for device used in tests. + * @externs + */ + + +/** + * @type {!shaka.device.IDevice} + */ +var deviceDetected; diff --git a/test/test/util/layout_tests.js b/test/test/util/layout_tests.js index 3300f0417..262d9ff48 100644 --- a/test/test/util/layout_tests.js +++ b/test/test/util/layout_tests.js @@ -177,7 +177,9 @@ shaka.test.TextLayoutTests = class extends shaka.test.LayoutTests { static async supported() { // We only do this in our lab, where we control device a11y settings that // impact these tests heavily. - if (shaka.util.Platform.isApple() && getClientArg('runningInVM')) { + if (deviceDetected.getBrowserEngine() === + shaka.device.IDevice.BrowserEngine.WEBKIT && + getClientArg('runningInVM')) { return false; } diff --git a/test/test/util/ui_utils.js b/test/test/util/ui_utils.js index 6e65609d5..08f2a99a1 100644 --- a/test/test/util/ui_utils.js +++ b/test/test/util/ui_utils.js @@ -174,8 +174,8 @@ shaka.test.UiUtils = class { // Some platforms have issues with audio-only playbacks on muted video // elements. Don't mute them. // Fuchsia reference: https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/platform/media/web_media_player_impl.cc;l=3535;drc=d23075f3 - if (!shaka.util.Platform.isTizen() && - !shaka.util.Platform.isFuchsiaCastDevice()) { + if (deviceDetected.getDeviceName() !== 'Tizen' && + deviceDetected.getDeviceName() !== 'Chromecast with Fuchsia') { video.muted = true; } video.width = 600; diff --git a/test/test/util/util.js b/test/test/util/util.js index 653a0cccc..4d2e20076 100644 --- a/test/test/util/util.js +++ b/test/test/util/util.js @@ -398,6 +398,9 @@ shaka.test.Util = class { const codecs = StreamUtils.getCorrectVideoCodecs( MimeUtils.getCodecs(mimetype)); const baseMimeType = MimeUtils.getBasicType(mimetype); + if (codecs.startsWith('hvc1.') && deviceDetected.disableHEVCSupport()) { + return false; + } // VideoConfiguration mediaDecodingConfig.video = { contentType: MimeUtils.getFullOrConvertedType( @@ -420,6 +423,39 @@ shaka.test.Util = class { await navigator.mediaCapabilities.decodingInfo(mediaDecodingConfig); return result.supported; } + + /** @param {string} userAgent */ + static setUserAgent(userAgent) { + shaka.test.Util.setNavigatorProperty('userAgent', userAgent); + } + + /** @param {?Object} userAgentData */ + static setUserAgentData(userAgentData) { + shaka.test.Util.setNavigatorProperty('userAgentData', userAgentData); + } + + /** @param {string} vendor */ + static setVendor(vendor) { + shaka.test.Util.setNavigatorProperty('vendor', vendor); + } + + /** @param {string} platform */ + static setPlatform(platform) { + shaka.test.Util.setNavigatorProperty('platform', platform); + } + + /** @param {number} maxTouchPoints */ + static setMaxTouchPoints(maxTouchPoints) { + shaka.test.Util.setNavigatorProperty('maxTouchPoints', maxTouchPoints); + } + + /** + * @param {string} key + * @param {*} value + */ + static setNavigatorProperty(key, value) { + Object.defineProperty(navigator, key, {value, configurable: true}); + } }; /** diff --git a/test/test/util/waiter.js b/test/test/util/waiter.js index e3687c4af..1841e5846 100644 --- a/test/test/util/waiter.js +++ b/test/test/util/waiter.js @@ -449,7 +449,8 @@ shaka.test.Waiter = class { // to complete without timing out. // We also use it on all platforms (except Tizen) because it reduces the // time it takes for tests to run. - if (mediaElement.playbackRate == 1 && !shaka.util.Platform.isTizen()) { + if (mediaElement.playbackRate == 1 && + deviceDetected.getDeviceName() !== 'Tizen') { mediaElement.playbackRate = 3; } } diff --git a/test/transmuxer/transmuxer_integration.js b/test/transmuxer/transmuxer_integration.js index a257d33dd..6410c276f 100644 --- a/test/transmuxer/transmuxer_integration.js +++ b/test/transmuxer/transmuxer_integration.js @@ -81,7 +81,7 @@ describe('Transmuxer Player', () => { pending('Codec MP3 is not supported by the platform.'); } // This tests is flaky in some Tizen devices, so we need omit it for now. - if (shaka.util.Platform.isTizen()) { + if (deviceDetected.getDeviceName() === 'Tizen') { pending('Disabled on Tizen.'); } await player.load('/base/test/test/assets/hls-raw-mp3/playlist.m3u8'); @@ -177,7 +177,7 @@ describe('Transmuxer Player', () => { pending('Codec MP3 is not supported by the platform.'); } // This tests is flaky in some Tizen devices, so we need omit it for now. - if (shaka.util.Platform.isTizen()) { + if (deviceDetected.getDeviceName() === 'Tizen') { pending('Disabled on Tizen.'); } await player.load('/base/test/test/assets/hls-ts-mp3/manifest.m3u8'); @@ -375,7 +375,7 @@ describe('Transmuxer Player', () => { pending('Codec MP3 is not supported by the platform.'); } // This tests is flaky in some Tizen devices, so we need omit it for now. - if (shaka.util.Platform.isTizen()) { + if (deviceDetected.getDeviceName() === 'Tizen') { pending('Disabled on Tizen.'); } @@ -400,7 +400,7 @@ describe('Transmuxer Player', () => { pending('Codec AC-3 is not supported by the platform.'); } // This tests is flaky in some Tizen devices, so we need omit it for now. - if (shaka.util.Platform.isTizen()) { + if (deviceDetected.getDeviceName() === 'Tizen') { pending('Disabled on Tizen.'); } diff --git a/test/ui/ui_unit.js b/test/ui/ui_unit.js index f883607c1..81650efc6 100644 --- a/test/ui/ui_unit.js +++ b/test/ui/ui_unit.js @@ -404,9 +404,10 @@ describe('UI', () => { UiUtils.confirmElementFound(videoContainer, 'shaka-seek-bar'); - // The default settings vary in mobile/desktop/SmartTV context. - if (shaka.util.Platform.isMobile() || - shaka.util.Platform.isSmartTV()) { + // The default settings vary in mobile/desktop context. + const deviceType = deviceDetected.getDeviceType(); + if (deviceType == shaka.device.IDevice.DeviceType.MOBILE || + deviceType == shaka.device.IDevice.DeviceType.TV) { UiUtils.confirmElementFound(videoContainer, 'shaka-play-button-container'); UiUtils.confirmElementFound(videoContainer, 'shaka-play-button'); diff --git a/test/util/platform_unit.js b/test/util/platform_unit.js deleted file mode 100644 index 6c708f03b..000000000 --- a/test/util/platform_unit.js +++ /dev/null @@ -1,287 +0,0 @@ -/*! @license - * Shaka Player - * Copyright 2016 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -describe('Platform', () => { - const originalUserAgent = navigator.userAgent; - const originalUserAgentData = navigator.userAgentData; - const originalVendor = navigator.vendor; - const originalPlatform = navigator.platform; - const originalMaxTouchPoints = navigator.maxTouchPoints; - - /* eslint-disable @stylistic/max-len */ - const macSafari = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.3 Safari/605.1.15'; - const ipadSafari = 'Mozilla/5.0 (iPad; CPU OS 18_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.2 Mobile/15E148 Safari/604.1'; - const iosChrome = 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/56.0.2924.75 Mobile/14E5239e Safari/602.1'; - // See: https://developer.samsung.com/smarttv/develop/guides/fundamentals/retrieving-platform-information.html - const tizen50 = 'Mozilla/5.0 (SMART-TV; LINUX; Tizen 5.0) AppleWebKit/537.36 (KHTML, like Gecko) Version/5.0 TV Safari/537.36'; - const tizen55 = 'Mozilla/5.0 (SMART-TV; LINUX; Tizen 5.5) AppleWebKit/537.36 (KHTML, like Gecko) 69.0.3497.106.1/5.5 TV Safari/537.36'; - const tizen60 = 'Mozilla/5.0 (SMART-TV; LINUX; Tizen 6.0) AppleWebKit/537.36 (KHTML, like Gecko) 76.0.3809.146/6.0 TV Safari/537.36'; - const tizen65 = 'Mozilla/5.0 (SMART-TV; LINUX; Tizen 6.5) AppleWebKit/537.36 (KHTML, like Gecko) 85.0.4183.93/6.5 TV Safari/537.36'; - const tizen70 = 'Mozilla/5.0 (SMART-TV; LINUX; Tizen 7.0) AppleWebKit/537.36 (KHTML, like Gecko) 94.0.4606.31/7.0 TV Safari/537.36'; - - // See: https://webostv.developer.lge.com/develop/specifications/web-api-and-web-engine#useragent-string - const webOs3 = 'Mozilla/5.0 (Web0S; Linux/SmartTV) AppleWebKit/537.36 (KHTML, like Gecko) QtWebEngine/5.2.1 Chrome/38.0.2125.122 Safari/537.36 WebAppManager'; - const webOs4 = 'Mozilla/5.0 (Web0S; Linux/SmartTV) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.34 Safari/537.36 WebAppManager'; - const webOs5 = 'Mozilla/5.0 (Web0S; Linux/SmartTV) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36 WebAppManager'; - const webOs6 = 'Mozilla/5.0 (Web0S; Linux/SmartTV) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.79 Safari/537.36 WebAppManager'; - - // cspell: disable-next-line - const vizio = 'Mozilla/5.0 (X11; Linux aarch64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36 CrKey/1.0.999999 VIZIO SmartCast(Conjure/MTKF-5.1.516.1 FW/0.6.11.1-2 Model/V50C6-J09)'; - // cspell: disable-next-line - const chromecastBuiltinOrOlder = 'Mozilla/5.0 (X11; Linux aarch64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.225 Safari/537.36 CrKey/1.56.500000 DeviceType/Chromecast'; - // cspell: disable-next-line - const chromecastFuchsia = 'Mozilla/5.0 (Fuchsia) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 CrKey/1.56.500000'; - // cspell: disable-next-line - const chromecastAndroid = 'Mozilla/5.0 (Linux; Android 12; Build/STTL.240206.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.0 Safari/537.36 CrKey/1.56.500000 DeviceType/AndroidTV'; - /* eslint-enable @stylistic/max-len */ - - afterEach(() => { - setUserAgent(originalUserAgent); - setUserAgentData(originalUserAgentData); - setVendor(originalVendor); - setPlatform(originalPlatform); - setMaxTouchPoints(originalMaxTouchPoints); - }); - - describe('Apple', () => { - beforeEach(() => { - setVendor('Apple Computer, Inc.'); - setMaxTouchPoints(0); - }); - - it('checks Safari version', () => { - setUserAgentData(null); - setPlatform('MacIntel'); - setUserAgent(macSafari); - expect(shaka.util.Platform.safariVersion()).toBe(18); - setUserAgent(ipadSafari); - expect(shaka.util.Platform.safariVersion()).toBe(18); - setUserAgent(iosChrome); - expect(shaka.util.Platform.safariVersion()).toBe(10); - setUserAgent(webOs6); - expect(shaka.util.Platform.safariVersion()).toBe(null); - - setVendor('Google Inc.'); - setUserAgent(macSafari); - expect(shaka.util.Platform.safariVersion()).toBe(null); - setUserAgent(ipadSafari); - expect(shaka.util.Platform.safariVersion()).toBe(null); - setUserAgent(iosChrome); - expect(shaka.util.Platform.safariVersion()).toBe(null); - setUserAgent(webOs6); - expect(shaka.util.Platform.safariVersion()).toBe(null); - }); - - it('checks is iOS', () => { - setUserAgent(macSafari); - expect(shaka.util.Platform.isIOS()).toBe(false); - setUserAgent(ipadSafari); - expect(shaka.util.Platform.isIOS()).toBe(true); - setUserAgent(iosChrome); - expect(shaka.util.Platform.isIOS()).toBe(true); - setUserAgent(webOs6); - expect(shaka.util.Platform.isIOS()).toBe(false); - }); - - it('checks is Mac', () => { - setUserAgentData({platform: 'macOS'}); - expect(shaka.util.Platform.isMac()).toBe(true); - - setUserAgentData(null); - setPlatform('MacIntel'); - expect(shaka.util.Platform.isMac()).toBe(true); - - setPlatform('Win32'); - expect(shaka.util.Platform.isMac()).toBe(false); - }); - - it('checks is Webkit STB', () => { - setUserAgent(ipadSafari); - setUserAgentData({platform: 'macOS'}); - expect(shaka.util.Platform.isWebkitSTB()).toBe(false); - - setUserAgentData(null); - setPlatform('MacIntel'); - expect(shaka.util.Platform.isWebkitSTB()).toBe(false); - - setPlatform('Win32'); - expect(shaka.util.Platform.isWebkitSTB()).toBe(false); - - setUserAgent(macSafari); - expect(shaka.util.Platform.isWebkitSTB()).toBe(true); - - setVendor('Google Inc.'); - expect(shaka.util.Platform.isWebkitSTB()).toBe(false); - }); - }); - - describe('Samsung', () => { - it('checks is Tizen 5', () => { - setUserAgent(webOs3); - expect(shaka.util.Platform.isTizen5()).toBe(false); - setUserAgent(tizen50); - expect(shaka.util.Platform.isTizen5()).toBe(true); - setUserAgent(tizen55); - expect(shaka.util.Platform.isTizen5()).toBe(true); - setUserAgent(tizen60); - expect(shaka.util.Platform.isTizen5()).toBe(false); - setUserAgent(tizen65); - expect(shaka.util.Platform.isTizen5()).toBe(false); - setUserAgent(tizen70); - expect(shaka.util.Platform.isTizen5()).toBe(false); - }); - - it('checks is Tizen 5.0', () => { - setUserAgent(webOs3); - expect(shaka.util.Platform.isTizen5_0()).toBe(false); - setUserAgent(tizen50); - expect(shaka.util.Platform.isTizen5_0()).toBe(true); - setUserAgent(tizen55); - expect(shaka.util.Platform.isTizen5_0()).toBe(false); - setUserAgent(tizen60); - expect(shaka.util.Platform.isTizen5_0()).toBe(false); - setUserAgent(tizen65); - expect(shaka.util.Platform.isTizen5_0()).toBe(false); - setUserAgent(tizen70); - expect(shaka.util.Platform.isTizen5_0()).toBe(false); - }); - - it('checks is Tizen 6', () => { - setUserAgent(webOs3); - expect(shaka.util.Platform.isTizen6()).toBe(false); - setUserAgent(tizen50); - expect(shaka.util.Platform.isTizen6()).toBe(false); - setUserAgent(tizen55); - expect(shaka.util.Platform.isTizen6()).toBe(false); - setUserAgent(tizen60); - expect(shaka.util.Platform.isTizen6()).toBe(true); - setUserAgent(tizen65); - expect(shaka.util.Platform.isTizen6()).toBe(true); - setUserAgent(tizen70); - expect(shaka.util.Platform.isTizen6()).toBe(false); - }); - }); - - describe('LG', () => { - it('checks is webOS 3', () => { - setUserAgent(tizen50); - expect(shaka.util.Platform.isWebOS3()).toBe(false); - setUserAgent(webOs3); - expect(shaka.util.Platform.isWebOS3()).toBe(true); - setUserAgent(webOs4); - expect(shaka.util.Platform.isWebOS3()).toBe(false); - setUserAgent(webOs5); - expect(shaka.util.Platform.isWebOS3()).toBe(false); - setUserAgent(webOs6); - expect(shaka.util.Platform.isWebOS3()).toBe(false); - }); - - it('checks is webOS 4', () => { - setUserAgent(tizen50); - expect(shaka.util.Platform.isWebOS4()).toBe(false); - setUserAgent(webOs3); - expect(shaka.util.Platform.isWebOS4()).toBe(false); - setUserAgent(webOs4); - expect(shaka.util.Platform.isWebOS4()).toBe(true); - setUserAgent(webOs5); - expect(shaka.util.Platform.isWebOS4()).toBe(false); - setUserAgent(webOs6); - expect(shaka.util.Platform.isWebOS4()).toBe(false); - }); - - it('checks is webOS 5', () => { - setUserAgent(tizen50); - expect(shaka.util.Platform.isWebOS5()).toBe(false); - setUserAgent(webOs3); - expect(shaka.util.Platform.isWebOS5()).toBe(false); - setUserAgent(webOs4); - expect(shaka.util.Platform.isWebOS5()).toBe(false); - setUserAgent(webOs5); - expect(shaka.util.Platform.isWebOS5()).toBe(true); - setUserAgent(webOs6); - expect(shaka.util.Platform.isWebOS5()).toBe(false); - }); - - it('checks is webOS 6', () => { - setUserAgent(tizen50); - expect(shaka.util.Platform.isWebOS6()).toBe(false); - setUserAgent(webOs3); - expect(shaka.util.Platform.isWebOS6()).toBe(false); - setUserAgent(webOs4); - expect(shaka.util.Platform.isWebOS6()).toBe(false); - setUserAgent(webOs5); - expect(shaka.util.Platform.isWebOS6()).toBe(false); - setUserAgent(webOs6); - expect(shaka.util.Platform.isWebOS6()).toBe(true); - }); - }); - - it('checks is Vizio', () => { - setUserAgent(vizio); - expect(shaka.util.Platform.isVizio()).toBe(true); - expect(shaka.util.Platform.isChromecast()).toBe(false); - }); - - it('checks is Chromecast Fuchsia', () => { - setUserAgent(chromecastFuchsia); - setUserAgentData(null); - expect(shaka.util.Platform.isVizio()).toBe(false); - expect(shaka.util.Platform.isChromecast()).toBe(true); - expect(shaka.util.Platform.isAndroidCastDevice()).toBe(false); - expect(shaka.util.Platform.isFuchsia()).toBe(true); - }); - - it('checks is Chromecast Android', () => { - setUserAgent(chromecastAndroid); - setUserAgentData(null); - expect(shaka.util.Platform.isVizio()).toBe(false); - expect(shaka.util.Platform.isChromecast()).toBe(true); - expect(shaka.util.Platform.isAndroidCastDevice()).toBe(true); - expect(shaka.util.Platform.isFuchsia()).toBe(false); - }); - - it('checks is Chromecast', () => { - setUserAgent(chromecastBuiltinOrOlder); - setUserAgentData(null); - expect(shaka.util.Platform.isVizio()).toBe(false); - expect(shaka.util.Platform.isChromecast()).toBe(true); - expect(shaka.util.Platform.isAndroidCastDevice()).toBe(false); - expect(shaka.util.Platform.isFuchsia()).toBe(false); - }); - - /** @param {string} userAgent */ - function setUserAgent(userAgent) { - setNavigatorProperty('userAgent', userAgent); - } - - /** @param {?Object} userAgentData */ - function setUserAgentData(userAgentData) { - setNavigatorProperty('userAgentData', userAgentData); - } - - /** @param {string} vendor */ - function setVendor(vendor) { - setNavigatorProperty('vendor', vendor); - } - - /** @param {string} platform */ - function setPlatform(platform) { - setNavigatorProperty('platform', platform); - } - - /** @param {number} maxTouchPoints */ - function setMaxTouchPoints(maxTouchPoints) { - setNavigatorProperty('maxTouchPoints', maxTouchPoints); - } - - /** - * @param {string} key - * @param {*} value - */ - function setNavigatorProperty(key, value) { - Object.defineProperty(navigator, key, {value, configurable: true}); - } -}); diff --git a/test/util/stream_utils_unit.js b/test/util/stream_utils_unit.js index 15efcd87c..3ebf58fe2 100644 --- a/test/util/stream_utils_unit.js +++ b/test/util/stream_utils_unit.js @@ -901,7 +901,7 @@ describe('StreamUtils', () => { it('should filter variants by the best available bandwidth' + ' for audio language', () => { // This test is flaky in some Tizen devices, due to codec restrictions. - if (shaka.util.Platform.isTizen()) { + if (deviceDetected.getDeviceName() === 'Tizen') { pending('Skip flaky test in Tizen'); } manifest = shaka.test.ManifestGenerator.generate((manifest) => { @@ -959,7 +959,7 @@ describe('StreamUtils', () => { pending('Codec HEVC is not supported by the platform.'); } // This test is flaky in some Tizen devices, due to codec restrictions. - if (shaka.util.Platform.isTizen()) { + if (deviceDetected.getDeviceName() === 'Tizen') { pending('Skip flaky test in Tizen'); } manifest = shaka.test.ManifestGenerator.generate((manifest) => { diff --git a/ui/controls.js b/ui/controls.js index 099623ce2..f8f69fdcd 100644 --- a/ui/controls.js +++ b/ui/controls.js @@ -11,6 +11,8 @@ goog.provide('shaka.ui.ControlsPanel'); goog.require('goog.asserts'); goog.require('shaka.ads.Utils'); goog.require('shaka.cast.CastProxy'); +goog.require('shaka.device.DeviceFactory'); +goog.require('shaka.device.IDevice'); goog.require('shaka.log'); goog.require('shaka.ui.AdInfo'); goog.require('shaka.ui.BigPlayButton'); @@ -28,7 +30,6 @@ goog.require('shaka.util.EventManager'); goog.require('shaka.util.FakeEvent'); goog.require('shaka.util.FakeEventTarget'); goog.require('shaka.util.IDestroyable'); -goog.require('shaka.util.Platform'); goog.require('shaka.util.Timer'); goog.requireType('shaka.Player'); @@ -641,9 +642,10 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget { // When the preferVideoFullScreenInVisionOS configuration value applies, // we avoid using document fullscreen, even if it is available. const video = /** @type {HTMLVideoElement} */(this.localVideo_); - if (video.webkitSupportsFullscreen) { - if (this.config_.preferVideoFullScreenInVisionOS && - shaka.util.Platform.isVisionOS()) { + if (video.webkitSupportsFullscreen && + this.config_.preferVideoFullScreenInVisionOS) { + const device = shaka.device.DeviceFactory.getDevice(); + if (device.getDeviceType() == shaka.device.IDevice.DeviceType.VR) { return false; } } diff --git a/ui/remote_button.js b/ui/remote_button.js index af5478a37..3556391be 100644 --- a/ui/remote_button.js +++ b/ui/remote_button.js @@ -8,6 +8,7 @@ goog.provide('shaka.ui.RemoteButton'); goog.require('shaka.Player'); +goog.require('shaka.device.DeviceFactory'); goog.require('shaka.ui.Controls'); goog.require('shaka.ui.Element'); goog.require('shaka.ui.Enums'); @@ -16,7 +17,6 @@ goog.require('shaka.ui.Localization'); goog.require('shaka.ui.OverflowMenu'); goog.require('shaka.ui.Utils'); goog.require('shaka.util.Dom'); -goog.require('shaka.util.Platform'); goog.requireType('shaka.ui.Controls'); @@ -34,7 +34,7 @@ shaka.ui.RemoteButton = class extends shaka.ui.Element { super(parent, controls); /** @private {boolean} */ - this.isAirPlay_ = shaka.util.Platform.isApple(); + this.isAirPlay_ = shaka.device.DeviceFactory.getDevice().supportsAirPlay(); /** @private {!HTMLButtonElement} */ this.remoteButton_ = shaka.util.Dom.createButton(); @@ -148,7 +148,7 @@ shaka.ui.RemoteButton = class extends shaka.ui.Element { if (this.player) { const disableRemote = this.video.disableRemotePlayback; let canCast = true; - if (shaka.util.Platform.isApple()) { + if (shaka.device.DeviceFactory.getDevice().supportsAirPlay()) { const loadMode = this.player.getLoadMode(); const mseMode = loadMode == shaka.Player.LoadMode.MEDIA_SOURCE; if (mseMode && this.player.getManifestType() != 'HLS') { diff --git a/ui/ui.js b/ui/ui.js index 4ed6cafdb..bdb04d40f 100644 --- a/ui/ui.js +++ b/ui/ui.js @@ -11,6 +11,8 @@ goog.provide('shaka.ui.Overlay.TrackLabelFormat'); goog.require('goog.asserts'); goog.require('shaka.Player'); +goog.require('shaka.device.DeviceFactory'); +goog.require('shaka.device.IDevice'); goog.require('shaka.log'); goog.require('shaka.polyfill'); goog.require('shaka.ui.Controls'); @@ -19,7 +21,6 @@ goog.require('shaka.util.ConfigUtils'); goog.require('shaka.util.Dom'); goog.require('shaka.util.FakeEvent'); goog.require('shaka.util.IDestroyable'); -goog.require('shaka.util.Platform'); /** * @implements {shaka.util.IDestroyable} @@ -125,7 +126,8 @@ shaka.ui.Overlay = class { * @export */ isMobile() { - return shaka.util.Platform.isMobile(); + const device = shaka.device.DeviceFactory.getDevice(); + return device.getDeviceType() == shaka.device.IDevice.DeviceType.MOBILE; } @@ -137,7 +139,8 @@ shaka.ui.Overlay = class { * @export */ isSmartTV() { - return shaka.util.Platform.isSmartTV(); + const device = shaka.device.DeviceFactory.getDevice(); + return device.getDeviceType() == shaka.device.IDevice.DeviceType.TV; } diff --git a/ui/vr_manager.js b/ui/vr_manager.js index a7dbeb2b3..7a860bd45 100644 --- a/ui/vr_manager.js +++ b/ui/vr_manager.js @@ -7,6 +7,8 @@ goog.provide('shaka.ui.VRManager'); +goog.require('shaka.device.DeviceFactory'); +goog.require('shaka.device.IDevice'); goog.require('shaka.log'); goog.require('shaka.ui.VRWebgl'); goog.require('shaka.util.Dom'); @@ -14,7 +16,6 @@ goog.require('shaka.util.EventManager'); goog.require('shaka.util.FakeEvent'); goog.require('shaka.util.FakeEventTarget'); goog.require('shaka.util.IReleasable'); -goog.require('shaka.util.Platform'); goog.requireType('shaka.Player'); @@ -373,7 +374,11 @@ shaka.ui.VRManager = class extends shaka.util.FakeEventTarget { } // The user interface is not intended for devices that are controlled with // a remote control, and WebGL may run slowly on these devices. - if (shaka.util.Platform.isSmartTV()) { + const device = shaka.device.DeviceFactory.getDevice(); + const deviceType = device.getDeviceType(); + if (deviceType == shaka.device.IDevice.DeviceType.TV || + deviceType == shaka.device.IDevice.DeviceType.CONSOLE || + deviceType == shaka.device.IDevice.DeviceType.CAST) { return null; } const webglContexts = [