mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-14 15:56:38 +03:00
6d76a135e5
Add support for HLS com.apple.streamingkeydelivery through MSE/EME implementation. Close #3346 ## Tests Tested on: - Mac 11.6 Safari 15.2 - iOS 15.2 Safari 15.2 - Mac 11.6 Chrome 96 (for potential regressions on Widevine keySystem) | Mode | DRM API | TS | CMAF (mono-key and multi-keys) |---|---|---|---| | file | EME | ✅ | ✅ | | file | Legacy-prefixed | ✅ | ✅ | | media-source | EME | **mux-js**: `encrypted` never fired<br />**real MSE**: `encrypted` event received, but with incorrect `sinf` initData (*1) | ✅ | | media-source | Legacy-prefixed | **mux-js**: `webkitneedkey` never fired<br/>**real MSE**: TBD | 🔴 fails to append media segment to SourceBuffer (init segment ok) `(video:4) – "failed fetch and append: code=3015"` | ## Support table | Mode | DRM API | TS | CMAF (mono-key and multi-keys) |---|---|---|---| | file | EME | ✅ | ✅ | | file | Legacy-prefixed | ✅ | ✅ | | media-source | EME | 🚫 `4040: HLS_MSE_ENCRYPTED_MP2T_NOT_SUPPORTED` | ✅ | | media-source | Legacy-prefixed | 🚫 `4041: HLS_MSE_ENCRYPTED_LEGACY_APPLE_MEDIA_KEYS_NOT_SUPPORTED` |🚫 `4041: HLS_MSE_ENCRYPTED_LEGACY_APPLE_MEDIA_KEYS_NOT_SUPPORTED` | ⚠️ Use EME APIs with multi-keys CMAF makes the video stalling with the audio continuing alone after a short time (~3 minutes in the stream, could be shorter, could be longer). Didn't find an explanation to that yet. I've observed the same behaviour with hls.js code so I don't think this is a player issue.
351 lines
9.1 KiB
JavaScript
351 lines
9.1 KiB
JavaScript
/*! @license
|
|
* Shaka Player
|
|
* Copyright 2016 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
goog.provide('shaka.util.Platform');
|
|
|
|
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() {
|
|
// 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 (!window.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 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 Google Chromecast.
|
|
*
|
|
* @return {boolean}
|
|
*/
|
|
static isChromecast() {
|
|
return shaka.util.Platform.userAgentContains_('CrKey');
|
|
}
|
|
|
|
/**
|
|
* Check if the current platform is Google Chrome.
|
|
*
|
|
* @return {boolean}
|
|
*/
|
|
static isChrome() {
|
|
// The Edge user agent will also contain the "Chrome" keyword, so we need
|
|
// to make sure this is not Edge.
|
|
return shaka.util.Platform.userAgentContains_('Chrome') &&
|
|
!shaka.util.Platform.isEdge();
|
|
}
|
|
|
|
/**
|
|
* 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 !!navigator.vendor && navigator.vendor.includes('Apple')
|
|
&& !shaka.util.Platform.isTizen();
|
|
}
|
|
|
|
/**
|
|
* 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');
|
|
}
|
|
|
|
/**
|
|
* Returns a major version number for Safari, 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()) {
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Check if the current platform is Apple Safari
|
|
* or Safari-based iOS browsers.
|
|
*
|
|
* @return {boolean}
|
|
*/
|
|
static isSafari() {
|
|
return !!shaka.util.Platform.safariVersion();
|
|
}
|
|
|
|
/**
|
|
* Guesses if the platform is a mobile one (iOS or Android).
|
|
*
|
|
* @return {boolean}
|
|
*/
|
|
static isMobile() {
|
|
if (/(?:iPhone|iPad|iPod|Android)/.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.isApple() && navigator.maxTouchPoints > 1;
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @return {boolean}
|
|
* @see https://github.com/google/shaka-player/issues/2759
|
|
*/
|
|
static requiresEncryptionInfoInAllInitSegments() {
|
|
const Platform = shaka.util.Platform;
|
|
return Platform.isTizen() || Platform.isXboxOne();
|
|
}
|
|
|
|
/**
|
|
* Returns true if MediaKeys is polyfilled
|
|
*
|
|
* @return {boolean}
|
|
*/
|
|
static isMediaKeysPolyfilled() {
|
|
if (window.shakaMediaKeysPolyfill) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/** @private {shaka.util.Timer} */
|
|
shaka.util.Platform.cacheExpirationTimer_ = null;
|
|
|
|
/** @private {HTMLMediaElement} */
|
|
shaka.util.Platform.cachedMediaElement_ = null;
|