Source: lib/cast/cast_utils.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.cast.CastUtils');
  7. goog.require('shaka.media.TimeRangesUtils');
  8. goog.require('shaka.util.FakeEvent');
  9. /**
  10. * @summary A set of cast utility functions and variables shared between sender
  11. * and receiver.
  12. */
  13. shaka.cast.CastUtils = class {
  14. /**
  15. * Serialize as JSON, but specially encode things JSON will not otherwise
  16. * represent.
  17. * @param {?} thing
  18. * @return {string}
  19. */
  20. static serialize(thing) {
  21. return JSON.stringify(thing, (key, value) => {
  22. if (typeof value == 'function') {
  23. // Functions can't be (safely) serialized.
  24. return undefined;
  25. }
  26. if (value instanceof Event || value instanceof shaka.util.FakeEvent) {
  27. // Events don't serialize to JSON well because of the DOM objects
  28. // and other complex objects they contain, so we strip these out.
  29. // Note that using Object.keys or JSON.stringify directly on the event
  30. // will not capture its properties. We must use a for loop.
  31. const simpleEvent = {};
  32. for (const eventKey in value) {
  33. const eventValue = value[eventKey];
  34. if (eventValue && typeof eventValue == 'object') {
  35. if (eventKey == 'detail') {
  36. // Keep the detail value, because it contains important
  37. // information for diagnosing errors.
  38. simpleEvent[eventKey] = eventValue;
  39. }
  40. // Strip out non-null object types because they are complex and we
  41. // don't need them.
  42. } else if (eventKey in Event) {
  43. // Strip out keys that are found on Event itself because they are
  44. // class-level constants we don't need, like Event.MOUSEMOVE == 16.
  45. } else {
  46. simpleEvent[eventKey] = eventValue;
  47. }
  48. }
  49. return simpleEvent;
  50. }
  51. if (value instanceof Error) {
  52. // Errors don't serialize to JSON well, either. TypeError, for example,
  53. // turns in "{}", leading to messages like "Error UNKNOWN.UNKNOWN" when
  54. // deserialized on the sender and displayed in the demo app.
  55. return shaka.cast.CastUtils.unpackError_(value);
  56. }
  57. if (value instanceof TimeRanges) {
  58. // TimeRanges must be unpacked into plain data for serialization.
  59. return shaka.cast.CastUtils.unpackTimeRanges_(value);
  60. }
  61. if (ArrayBuffer.isView(value) &&
  62. /** @type {TypedArray} */(value).BYTES_PER_ELEMENT === 1) {
  63. // Some of our code cares about Uint8Arrays actually being Uint8Arrays,
  64. // so this gives them special treatment.
  65. return shaka.cast.CastUtils.unpackUint8Array_(
  66. /** @type {!Uint8Array} */(value));
  67. }
  68. if (typeof value == 'number') {
  69. // NaN and infinity cannot be represented directly in JSON.
  70. if (isNaN(value)) {
  71. return 'NaN';
  72. }
  73. if (isFinite(value)) {
  74. return value;
  75. }
  76. if (value < 0) {
  77. return '-Infinity';
  78. }
  79. return 'Infinity';
  80. }
  81. return value;
  82. });
  83. }
  84. /**
  85. * Deserialize JSON using our special encodings.
  86. * @param {string} str
  87. * @return {?}
  88. */
  89. static deserialize(str) {
  90. return JSON.parse(str, (key, value) => {
  91. if (value == 'NaN') {
  92. return NaN;
  93. } else if (value == '-Infinity') {
  94. return -Infinity;
  95. } else if (value == 'Infinity') {
  96. return Infinity;
  97. } else if (value && typeof value == 'object' &&
  98. value['__type__'] == 'TimeRanges') {
  99. // TimeRanges objects have been unpacked and sent as plain data.
  100. // Simulate the original TimeRanges object.
  101. return shaka.cast.CastUtils.simulateTimeRanges_(value);
  102. } else if (value && typeof value == 'object' &&
  103. value['__type__'] == 'Uint8Array') {
  104. return shaka.cast.CastUtils.makeUint8Array_(value);
  105. } else if (value && typeof value == 'object' &&
  106. value['__type__'] == 'Error') {
  107. return shaka.cast.CastUtils.makeError_(value);
  108. }
  109. return value;
  110. });
  111. }
  112. /**
  113. * @param {!TimeRanges} ranges
  114. * @return {!Object}
  115. * @private
  116. */
  117. static unpackTimeRanges_(ranges) {
  118. const obj = {
  119. '__type__': 'TimeRanges', // a signal to deserialize
  120. 'length': ranges.length,
  121. 'start': [],
  122. 'end': [],
  123. };
  124. const TimeRangesUtils = shaka.media.TimeRangesUtils;
  125. for (const {start, end} of TimeRangesUtils.getBufferedInfo(ranges)) {
  126. obj['start'].push(start);
  127. obj['end'].push(end);
  128. }
  129. return obj;
  130. }
  131. /**
  132. * Creates a simulated TimeRanges object from data sent by the cast receiver.
  133. * @param {?} obj
  134. * @return {{
  135. * length: number,
  136. * start: function(number): number,
  137. * end: function(number): number
  138. * }}
  139. * @private
  140. */
  141. static simulateTimeRanges_(obj) {
  142. return {
  143. length: obj.length,
  144. // NOTE: a more complete simulation would throw when |i| was out of range,
  145. // but for simplicity we will assume a well-behaved application that uses
  146. // length instead of catch to stop iterating.
  147. start: (i) => { return obj.start[i]; },
  148. end: (i) => { return obj.end[i]; },
  149. };
  150. }
  151. /**
  152. * @param {!Uint8Array} array
  153. * @return {!Object}
  154. * @private
  155. */
  156. static unpackUint8Array_(array) {
  157. return {
  158. '__type__': 'Uint8Array', // a signal to deserialize
  159. 'entries': Array.from(array),
  160. };
  161. }
  162. /**
  163. * Creates a Uint8Array object from data sent by the cast receiver.
  164. * @param {?} obj
  165. * @return {!Uint8Array}
  166. * @private
  167. */
  168. static makeUint8Array_(obj) {
  169. return new Uint8Array(/** @type {!Array<number>} */ (obj['entries']));
  170. }
  171. /**
  172. * @param {!Error} error
  173. * @return {!Object}
  174. * @private
  175. */
  176. static unpackError_(error) {
  177. // None of the properties in TypeError are enumerable, but there are some
  178. // common Error properties we expect. We also enumerate any enumerable
  179. // properties and "own" properties of the type, in case there is an Error
  180. // subtype with additional properties we don't know about in advance.
  181. const properties = new Set(['name', 'message', 'stack']);
  182. for (const key in error) {
  183. properties.add(key);
  184. }
  185. for (const key of Object.getOwnPropertyNames(error)) {
  186. properties.add(key);
  187. }
  188. const contents = {};
  189. for (const key of properties) {
  190. contents[key] = error[key];
  191. }
  192. return {
  193. '__type__': 'Error', // a signal to deserialize
  194. 'contents': contents,
  195. };
  196. }
  197. /**
  198. * Creates an Error object from data sent by the cast receiver.
  199. * @param {?} obj
  200. * @return {!Error}
  201. * @private
  202. */
  203. static makeError_(obj) {
  204. const contents = obj['contents'];
  205. const error = new Error(contents['message']);
  206. for (const key in contents) {
  207. error[key] = contents[key];
  208. }
  209. return error;
  210. }
  211. };
  212. /**
  213. * HTMLMediaElement events that are proxied while casting.
  214. * @const {!Array<string>}
  215. */
  216. shaka.cast.CastUtils.VideoEvents = [
  217. 'ended',
  218. 'play',
  219. 'playing',
  220. 'pause',
  221. 'pausing',
  222. 'ratechange',
  223. 'seeked',
  224. 'seeking',
  225. 'timeupdate',
  226. 'volumechange',
  227. ];
  228. /**
  229. * HTMLMediaElement attributes that are proxied while casting.
  230. * @const {!Array<string>}
  231. */
  232. shaka.cast.CastUtils.VideoAttributes = [
  233. 'buffered',
  234. 'currentTime',
  235. 'duration',
  236. 'ended',
  237. 'loop',
  238. 'muted',
  239. 'paused',
  240. 'playbackRate',
  241. 'seeking',
  242. 'videoHeight',
  243. 'videoWidth',
  244. 'volume',
  245. ];
  246. /**
  247. * HTMLMediaElement attributes that are transferred when casting begins.
  248. * @const {!Array<string>}
  249. */
  250. shaka.cast.CastUtils.VideoInitStateAttributes = [
  251. 'loop',
  252. 'playbackRate',
  253. ];
  254. /**
  255. * HTMLMediaElement methods with no return value that are proxied while casting.
  256. * @const {!Array<string>}
  257. */
  258. shaka.cast.CastUtils.VideoVoidMethods = [
  259. 'pause',
  260. 'play',
  261. ];
  262. /**
  263. * Player getter methods that are proxied while casting.
  264. * The key is the method, the value is the frequency of updates.
  265. * Frequency 1 translates to every update; frequency 2 to every 2 updates, etc.
  266. * @const {!Map<string, number>}
  267. */
  268. shaka.cast.CastUtils.PlayerGetterMethods = new Map()
  269. // NOTE: The 'drmInfo' property is not proxied, as it is very large.
  270. .set('getAssetUri', 2)
  271. .set('getAudioLanguages', 4)
  272. .set('getAudioLanguagesAndRoles', 4)
  273. .set('getBufferFullness', 1)
  274. .set('getBufferedInfo', 2)
  275. .set('getExpiration', 2)
  276. .set('getKeyStatuses', 2)
  277. // NOTE: The 'getManifest' property is not proxied, as it is very large.
  278. // NOTE: The 'getManifestParserFactory' property is not proxied, as it would
  279. // not serialize.
  280. .set('getPlaybackRate', 2)
  281. .set('getTextLanguages', 4)
  282. .set('getTextLanguagesAndRoles', 4)
  283. .set('getImageTracks', 2)
  284. .set('getThumbnails', 2)
  285. .set('isAudioOnly', 10)
  286. .set('isBuffering', 1)
  287. .set('isInProgress', 1)
  288. .set('isLive', 10)
  289. .set('isTextTrackVisible', 1)
  290. .set('keySystem', 10)
  291. .set('seekRange', 1)
  292. .set('getLoadMode', 10)
  293. .set('getManifestType', 10)
  294. .set('isFullyLoaded', 1)
  295. .set('isEnded', 1);
  296. /**
  297. * Player getter methods with data large enough to be sent in their own update
  298. * messages, to reduce the size of each message. The format of this is
  299. * identical to PlayerGetterMethods.
  300. * @const {!Map<string, number>}
  301. */
  302. shaka.cast.CastUtils.LargePlayerGetterMethods = new Map()
  303. // NOTE: The 'getSharedConfiguration' property is not proxied as it would
  304. // not be possible to share a reference.
  305. .set('getConfiguration', 4)
  306. .set('getConfigurationForLowLatency', 4)
  307. .set('getStats', 5)
  308. .set('getAudioTracks', 2)
  309. .set('getTextTracks', 2)
  310. .set('getVariantTracks', 2);
  311. /**
  312. * Player getter methods that are proxied while casting, but only when casting
  313. * a livestream.
  314. * The key is the method, the value is the frequency of updates.
  315. * Frequency 1 translates to every update; frequency 2 to every 2 updates, etc.
  316. * @const {!Map<string, number>}
  317. */
  318. shaka.cast.CastUtils.PlayerGetterMethodsThatRequireLive = new Map()
  319. .set('getPlayheadTimeAsDate', 1)
  320. .set('getPresentationStartTimeAsDate', 20)
  321. .set('getSegmentAvailabilityDuration', 20);
  322. /**
  323. * Player getter and setter methods that are used to transfer state when casting
  324. * begins.
  325. * @const {!Array<!Array<string>>}
  326. */
  327. shaka.cast.CastUtils.PlayerInitState = [
  328. [
  329. 'getConfiguration',
  330. 'configure',
  331. 'getConfigurationForLowLatency',
  332. 'configurationForLowLatency',
  333. ],
  334. ];
  335. /**
  336. * Player getter and setter methods that are used to transfer state after
  337. * load() is resolved.
  338. * @const {!Array<!Array<string>>}
  339. */
  340. shaka.cast.CastUtils.PlayerInitAfterLoadState = [
  341. ['isTextTrackVisible', 'setTextTrackVisibility'],
  342. ];
  343. /**
  344. * Player methods with no return value that are proxied while casting.
  345. * @const {!Array<string>}
  346. */
  347. shaka.cast.CastUtils.PlayerVoidMethods = [
  348. 'addChaptersTrack',
  349. 'addTextTrackAsync',
  350. 'addThumbnailsTrack',
  351. 'cancelTrickPlay',
  352. 'configure',
  353. 'configurationForLowLatency',
  354. 'getChapters',
  355. 'getChaptersTracks',
  356. 'resetConfiguration',
  357. 'retryStreaming',
  358. 'selectAudioLanguage',
  359. 'selectAudioTrack',
  360. 'selectTextLanguage',
  361. 'selectTextTrack',
  362. 'selectVariantTrack',
  363. 'selectVariantsByLabel',
  364. 'setTextTrackVisibility',
  365. 'trickPlay',
  366. 'updateStartTime',
  367. 'goToLive',
  368. ];
  369. /**
  370. * Player methods returning a Promise that are proxied while casting.
  371. * @const {!Array<string>}
  372. */
  373. shaka.cast.CastUtils.PlayerPromiseMethods = [
  374. 'attach',
  375. 'attachCanvas',
  376. 'detach',
  377. // The manifestFactory parameter of load is not supported.
  378. 'load',
  379. 'unload',
  380. ];
  381. /**
  382. * @typedef {{
  383. * video: Object,
  384. * player: Object,
  385. * manifest: ?string,
  386. * startTime: ?number
  387. * }}
  388. * @property {Object} video
  389. * Dictionary of video properties to be set.
  390. * @property {Object} player
  391. * Dictionary of player setters to be called.
  392. * @property {?string} manifest
  393. * The currently-selected manifest, if present.
  394. * @property {?number} startTime
  395. * The playback start time, if currently playing.
  396. */
  397. shaka.cast.CastUtils.InitStateType;
  398. /**
  399. * The namespace for Shaka messages on the cast bus.
  400. * @const {string}
  401. */
  402. shaka.cast.CastUtils.SHAKA_MESSAGE_NAMESPACE = 'urn:x-cast:com.google.shaka.v2';
  403. /**
  404. * The namespace for generic messages on the cast bus.
  405. * @const {string}
  406. */
  407. shaka.cast.CastUtils.GENERIC_MESSAGE_NAMESPACE =
  408. 'urn:x-cast:com.google.cast.media';