Source: lib/device/chromecast.js

/*! @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<?number>} */
    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<shaka.device.Chromecast.OsType_>} */
    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());
}