Files
shaka-player/lib/device/abstract_device.js
T
Ivan 921206dc1d feat: transmux in a worker (#9914)
This PR introduces a Web Worker for transmuxing resolving
https://github.com/shaka-project/shaka-player/issues/1735

- The worker bundle is compiled separately
- The build output is embedded as a string constant and then wrapped in
a Blob to create an inline Worker URL (HLS.js does this very similarly)
- `TransmuxerProxy` is created wrapping a real transmuxer, but no worker
is started yet - on the first `transmux()` call, it checks if the device
supports worker transmuxing
- For each transmux() call: the buffer is copied, then zero-copy
transferred to the worker. A PublicPromise is stored under a reqId with
a timeout timer, and the main thread awaits it.
- The worker transmuxes and posts back transmuxed (or error). The shared
message listener routes the response to the right proxy instance by id,
which resolves the promise and cancels the timer.
- When the last proxy instance is destroyed, the worker is terminated
and the blob URL is revoked.
  loaded inside the worker.
- Some low-end devices have been excluded since their Worker support is
questionable

There most likely is a better way to do this - please let me know
2026-05-27 21:51:03 +02:00

400 lines
8.1 KiB
JavaScript

/*! @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<!shaka.device.IDevice.DeviceType>} */
this.abstractDeviceType_ = new shaka.util.Lazy(() => {
const ua = navigator.userAgent.toLowerCase();
const rules = [
{
type: shaka.device.IDevice.DeviceType.VR,
regex: /oculus|quest/,
},
{
type: shaka.device.IDevice.DeviceType.TV,
regex: /smart[ _]?tv|android ?tv/,
},
{
type: shaka.device.IDevice.DeviceType.MOBILE,
regex: /iphone|ipad|ipod|android/,
},
];
for (const rule of rules) {
if (rule.regex.test(ua)) {
return rule.type;
}
}
const uaData = navigator.userAgentData;
const platform = uaData?.platform?.toLowerCase() || '';
if (uaData) {
if (uaData.mobile) {
return shaka.device.IDevice.DeviceType.MOBILE;
}
// Android not mobile, possibly Android TV.
if (platform.includes('android')) {
return shaka.device.IDevice.DeviceType.TV;
}
return shaka.device.IDevice.DeviceType.DESKTOP;
}
return shaka.device.IDevice.DeviceType.DESKTOP;
});
/** @private {!shaka.util.Lazy<shaka.device.IDevice.BrowserEngine>} */
this.browserEngine_ = new shaka.util.Lazy(() => {
const ua = navigator.userAgent;
if (navigator.vendor.includes('Apple') &&
(ua.includes('Version/') || ua.includes('OS/'))) {
return shaka.device.IDevice.BrowserEngine.WEBKIT;
}
if (ua.includes('Edge/')) {
return shaka.device.IDevice.BrowserEngine.EDGE;
}
if (ua.includes('Chrome/')) {
return shaka.device.IDevice.BrowserEngine.CHROMIUM;
}
if (ua.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;
}
/**
* Return the chromium browser version
* @return {?number}
* @private
*/
getChromeVersion_() {
let browserVersion = null;
const match = navigator.userAgent.match(/Chrome\/(\d+)/);
if (match) {
browserVersion = parseInt(match[1], /* base= */ 10);
}
return browserVersion;
}
/**
* @override
*/
requiresDvvcWorkaround(stream) {
const browserEngine = this.getBrowserEngine();
if (browserEngine != shaka.device.IDevice.BrowserEngine.CHROMIUM) {
return false;
}
const chromeVersion = this.getChromeVersion_();
return stream.type === 'video' &&
chromeVersion != null &&
chromeVersion < 94 &&
stream.supplementalCodecs != stream.codecs &&
(/dvh1|dvhe/).test(stream.supplementalCodecs);
}
/**
* @override
*/
supportsSequenceMode() {
return true;
}
/**
* @override
*/
supportsSmoothCodecSwitching(keySystem) {
const sourceBuffer = window.ManagedSourceBuffer || window.SourceBuffer;
return !!sourceBuffer &&
// eslint-disable-next-line no-restricted-syntax
!!sourceBuffer.prototype && !!sourceBuffer.prototype.changeType;
}
/**
* @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;
}
if (deviceType === shaka.device.IDevice.DeviceType.TV) {
config.textDisplayer.suspendRenderingWhenHidden = false;
}
return config;
}
/**
* @override
*/
supportsOfflineStorage() {
return !!window.indexedDB;
}
/**
* @override
*/
rejectCodecs() {
return [];
}
/**
* @override
*/
getHdrLevel(preferHLG) {
if (window.matchMedia !== undefined &&
(window.matchMedia('(color-gamut: p3)').matches ||
window.matchMedia('(dynamic-range: high)').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
*/
supportsCbcsWithoutEncryptionSchemeSupport() {
return false;
}
/**
* @override
*/
needWaitForEncryptedEvent(keySystem) {
return keySystem === 'com.apple.fps';
}
/**
* @override
*/
supportsContainerChangeType() {
return true;
}
/**
* @override
*/
getRemote(video) {
const deviceType = this.getDeviceType();
// Remote operation is possible with the following types of devices.
// For example, CAST is not included since it doesn't make sense to
// perform remote operations from a CAST device.
const validTypes = [
shaka.device.IDevice.DeviceType.DESKTOP,
shaka.device.IDevice.DeviceType.MOBILE,
shaka.device.IDevice.DeviceType.VR,
shaka.device.IDevice.DeviceType.APPLE_VR,
];
if (validTypes.includes(deviceType)) {
return video.remote;
}
return null;
}
/**
* @override
*/
supportsWorkerTransmux() {
return typeof Worker !== 'undefined';
}
/**
* @override
*/
toString() {
return `Device: ${this.getDeviceName()} v${this.getVersion()}; ` +
`Type: ${this.getDeviceType()}`;
}
};