Source: lib/device/chromecast.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2025 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.device.Chromecast');
  7. goog.require('shaka.device.AbstractDevice');
  8. goog.require('shaka.device.DeviceFactory');
  9. goog.require('shaka.device.IDevice');
  10. goog.require('shaka.log');
  11. goog.require('shaka.util.Lazy');
  12. /**
  13. * @final
  14. */
  15. shaka.device.Chromecast = class extends shaka.device.AbstractDevice {
  16. constructor() {
  17. if (!shaka.device.Chromecast.isChromecast_()) {
  18. throw new Error('Not a Chromecast device!');
  19. }
  20. super();
  21. /** @private {!shaka.util.Lazy<?number>} */
  22. this.version_ = new shaka.util.Lazy(() => {
  23. // Looking for something like "Chrome/106.0.0.0"
  24. const match = navigator.userAgent.match(/Chrome\/(\d+)/);
  25. if (match) {
  26. return parseInt(match[1], /* base= */ 10);
  27. }
  28. return null;
  29. });
  30. /** @private {!shaka.util.Lazy<shaka.device.Chromecast.OsType_>} */
  31. this.osType_ = new shaka.util.Lazy(() => {
  32. let fieldToCheck = (navigator.userAgentData &&
  33. navigator.userAgentData.platform) || navigator.userAgent;
  34. fieldToCheck = fieldToCheck.toLowerCase();
  35. if (fieldToCheck.includes('android')) {
  36. return shaka.device.Chromecast.OsType_.ANDROID;
  37. } else if (fieldToCheck.includes('fuchsia')) {
  38. return shaka.device.Chromecast.OsType_.FUCHSIA;
  39. } else {
  40. return shaka.device.Chromecast.OsType_.LINUX;
  41. }
  42. });
  43. }
  44. /**
  45. * @override
  46. */
  47. getVersion() {
  48. return this.version_.value();
  49. }
  50. /**
  51. * @override
  52. */
  53. getDeviceName() {
  54. return 'Chromecast with ' + this.osType_.value();
  55. }
  56. /**
  57. * @override
  58. */
  59. getDeviceType() {
  60. return shaka.device.IDevice.DeviceType.CAST;
  61. }
  62. /**
  63. * @override
  64. */
  65. getBrowserEngine() {
  66. return shaka.device.IDevice.BrowserEngine.CHROMIUM;
  67. }
  68. /**
  69. * @override
  70. */
  71. supportsMediaCapabilities() {
  72. return super.supportsMediaCapabilities() &&
  73. this.osType_.value() !== shaka.device.Chromecast.OsType_.LINUX;
  74. }
  75. /**
  76. * @override
  77. */
  78. supportsSmoothCodecSwitching() {
  79. return this.osType_.value() !== shaka.device.Chromecast.OsType_.LINUX;
  80. }
  81. /**
  82. * @override
  83. */
  84. seekDelay() {
  85. switch (this.osType_.value()) {
  86. case shaka.device.Chromecast.OsType_.ANDROID:
  87. return 0;
  88. case shaka.device.Chromecast.OsType_.FUCHSIA:
  89. return 3;
  90. default:
  91. return 1;
  92. }
  93. }
  94. /**
  95. * @override
  96. */
  97. async detectMaxHardwareResolution() {
  98. // In our tests, the original Chromecast seems to have trouble decoding
  99. // above 1080p. It would be a waste to select a higher res anyway, given
  100. // that the device only outputs 1080p to begin with.
  101. // Chromecast has an extension to query the device/display's resolution.
  102. const hasCanDisplayType = window.cast && cast.__platform__ &&
  103. cast.__platform__.canDisplayType;
  104. // Some hub devices can only do 720p. Default to that.
  105. const maxResolution = {width: 1280, height: 720};
  106. try {
  107. if (hasCanDisplayType && await cast.__platform__.canDisplayType(
  108. 'video/mp4; codecs="avc1.640028"; width=3840; height=2160')) {
  109. // The device and display can both do 4k. Assume a 4k limit.
  110. maxResolution.width = 3840;
  111. maxResolution.height = 2160;
  112. } else if (hasCanDisplayType && await cast.__platform__.canDisplayType(
  113. 'video/mp4; codecs="avc1.640028"; width=1920; height=1080')) {
  114. // Most Chromecasts can do 1080p.
  115. maxResolution.width = 1920;
  116. maxResolution.height = 1080;
  117. }
  118. } catch (error) {
  119. // This shouldn't generally happen. Log the error.
  120. shaka.log.alwaysError('Failed to check canDisplayType:', error);
  121. // Now ignore the error and let the 720p default stand.
  122. }
  123. return maxResolution;
  124. }
  125. /**
  126. * @override
  127. */
  128. adjustConfig(config) {
  129. super.adjustConfig(config);
  130. // Chromecast has long hardware pipeline that respond slowly to seeking.
  131. // Therefore we should not seek when we detect a stall on this platform.
  132. // Instead, default stallSkip to 0 to force the stall detector to pause
  133. // and play instead.
  134. config.streaming.stallSkip = 0;
  135. return config;
  136. }
  137. /**
  138. * @override
  139. */
  140. supportsOfflineStorage() {
  141. return false;
  142. }
  143. /**
  144. * Check if the current platform is Vizio TV.
  145. * @return {boolean}
  146. * @private
  147. */
  148. static isChromecast_() {
  149. return navigator.userAgent.includes('CrKey') &&
  150. !navigator.userAgent.includes('VIZIO SmartCast');
  151. }
  152. };
  153. /**
  154. * @private
  155. * @enum {string}
  156. */
  157. shaka.device.Chromecast.OsType_ = {
  158. ANDROID: 'Android',
  159. FUCHSIA: 'Fuchsia',
  160. LINUX: 'Linux',
  161. };
  162. if (shaka.device.Chromecast.isChromecast_()) {
  163. shaka.device.DeviceFactory.registerDeviceFactory(
  164. () => new shaka.device.Chromecast());
  165. }