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 {!Object.<string, number>}
  267. */
  268. shaka.cast.CastUtils.PlayerGetterMethods = {
  269. // NOTE: The 'drmInfo' property is not proxied, as it is very large.
  270. 'getAssetUri': 2,
  271. 'getAudioLanguages': 4,
  272. 'getAudioLanguagesAndRoles': 4,
  273. 'getBufferFullness': 1,
  274. 'getBufferedInfo': 2,
  275. 'getExpiration': 2,
  276. '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. 'getPlaybackRate': 2,
  281. 'getTextLanguages': 4,
  282. 'getTextLanguagesAndRoles': 4,
  283. 'getImageTracks': 2,
  284. 'getThumbnails': 2,
  285. 'isAudioOnly': 10,
  286. 'isBuffering': 1,
  287. 'isInProgress': 1,
  288. 'isLive': 10,
  289. 'isTextTrackVisible': 1,
  290. 'keySystem': 10,
  291. 'seekRange': 1,
  292. 'getLoadMode': 10,
  293. 'getManifestType': 10,
  294. 'isFullyLoaded': 1,
  295. };
  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 {!Object.<string, number>}
  301. */
  302. shaka.cast.CastUtils.LargePlayerGetterMethods = {
  303. // NOTE: The 'getSharedConfiguration' property is not proxied as it would
  304. // not be possible to share a reference.
  305. 'getConfiguration': 4,
  306. 'getStats': 5,
  307. 'getTextTracks': 2,
  308. 'getVariantTracks': 2,
  309. };
  310. /**
  311. * Player getter methods that are proxied while casting, but only when casting
  312. * a livestream.
  313. * The key is the method, the value is the frequency of updates.
  314. * Frequency 1 translates to every update; frequency 2 to every 2 updates, etc.
  315. * @const {!Object.<string, number>}
  316. */
  317. shaka.cast.CastUtils.PlayerGetterMethodsThatRequireLive = {
  318. 'getPlayheadTimeAsDate': 1,
  319. 'getPresentationStartTimeAsDate': 20,
  320. 'getSegmentAvailabilityDuration': 20,
  321. };
  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. ['getConfiguration', 'configure'],
  329. ];
  330. /**
  331. * Player getter and setter methods that are used to transfer state after
  332. * load() is resolved.
  333. * @const {!Array.<!Array.<string>>}
  334. */
  335. shaka.cast.CastUtils.PlayerInitAfterLoadState = [
  336. ['isTextTrackVisible', 'setTextTrackVisibility'],
  337. ];
  338. /**
  339. * Player methods with no return value that are proxied while casting.
  340. * @const {!Array.<string>}
  341. */
  342. shaka.cast.CastUtils.PlayerVoidMethods = [
  343. 'addChaptersTrack',
  344. 'addTextTrackAsync',
  345. 'addThumbnailsTrack',
  346. 'cancelTrickPlay',
  347. 'configure',
  348. 'getChapters',
  349. 'getChaptersTracks',
  350. 'resetConfiguration',
  351. 'retryStreaming',
  352. 'selectAudioLanguage',
  353. 'selectTextLanguage',
  354. 'selectTextTrack',
  355. 'selectVariantTrack',
  356. 'selectVariantsByLabel',
  357. 'setTextTrackVisibility',
  358. 'trickPlay',
  359. 'updateStartTime',
  360. 'goToLive',
  361. ];
  362. /**
  363. * Player methods returning a Promise that are proxied while casting.
  364. * @const {!Array.<string>}
  365. */
  366. shaka.cast.CastUtils.PlayerPromiseMethods = [
  367. 'attach',
  368. 'attachCanvas',
  369. 'detach',
  370. // The manifestFactory parameter of load is not supported.
  371. 'load',
  372. 'unload',
  373. ];
  374. /**
  375. * @typedef {{
  376. * video: Object,
  377. * player: Object,
  378. * manifest: ?string,
  379. * startTime: ?number
  380. * }}
  381. * @property {Object} video
  382. * Dictionary of video properties to be set.
  383. * @property {Object} player
  384. * Dictionary of player setters to be called.
  385. * @property {?string} manifest
  386. * The currently-selected manifest, if present.
  387. * @property {?number} startTime
  388. * The playback start time, if currently playing.
  389. */
  390. shaka.cast.CastUtils.InitStateType;
  391. /**
  392. * The namespace for Shaka messages on the cast bus.
  393. * @const {string}
  394. */
  395. shaka.cast.CastUtils.SHAKA_MESSAGE_NAMESPACE = 'urn:x-cast:com.google.shaka.v2';
  396. /**
  397. * The namespace for generic messages on the cast bus.
  398. * @const {string}
  399. */
  400. shaka.cast.CastUtils.GENERIC_MESSAGE_NAMESPACE =
  401. 'urn:x-cast:com.google.cast.media';