• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

silvermine / videojs-chromecast / 4766541855

pending completion
4766541855

push

github

GitHub
Merge pull request #158 from pbredenberg/pb/ci-migrate-to-github-actions

0 of 171 branches covered (0.0%)

Branch coverage included in aggregate %.

14 of 356 relevant lines covered (3.93%)

0.2 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

3.02
/src/js/tech/ChromecastTech.js
1
var ChromecastSessionManager = require('../chromecast/ChromecastSessionManager'),
1✔
2
    ChromecastTechUI = require('./ChromecastTechUI');
1✔
3

4
/**
5
 * Registers the ChromecastTech Tech with Video.js. Calls {@link
6
 * http://docs.videojs.com/Tech.html#.registerTech}, which will add a Tech called
7
 * `chromecast` to the list of globally registered Video.js Tech implementations.
8
 *
9
 * [Video.js Tech](http://docs.videojs.com/Tech.html) are initialized and used
10
 * automatically by Video.js Player instances. Whenever a new source is set on the player,
11
 * the player iterates through the list of available Tech to determine which to use to
12
 * play the source.
13
 *
14
 * @param videojs {object} A reference to
15
 * {@link http://docs.videojs.com/module-videojs.html|Video.js}
16
 * @see http://docs.videojs.com/Tech.html#.registerTech
17
 */
18
module.exports = function(videojs) {
1✔
19
   var Tech = videojs.getComponent('Tech'),
1✔
20
       SESSION_TIMEOUT = 10 * 1000; // milliseconds
1✔
21

22
   /**
23
    * @module ChomecastTech
24
    */
25

26
   /**
27
    * The Video.js Tech class is the base class for classes that provide media playback
28
    * technology implementations to Video.js such as HTML5, Flash and HLS.
29
    *
30
    * @external Tech
31
    * @see {@link http://docs.videojs.com/Tech.html|Tech}
32
    */
33

34
   /** @lends ChromecastTech.prototype */
35
   class ChromecastTech extends Tech {
36

37
      /**
38
       * Implements Video.js playback {@link http://docs.videojs.com/tutorial-tech_.html|Tech}
39
       * for {@link https://developers.google.com/cast/|Google's Chromecast}.
40
       *
41
       * @constructs ChromecastTech
42
       * @extends external:Tech
43
       * @param options {object} The options to use for configuration
44
       * @see {@link https://developers.google.com/cast/|Google Cast}
45
       */
46
      constructor(options) {
47
         super(options);
×
48

49
         this.featuresVolumeControl = true;
×
50
         this.featuresPlaybackRate = false;
×
51
         this.movingMediaElementInDOM = false;
×
52
         this.featuresFullscreenResize = true;
×
53
         this.featuresTimeupdateEvents = true;
×
54
         this.featuresProgressEvents = false;
×
55
         // Text tracks are not supported in this version
56
         this.featuresNativeTextTracks = false;
×
57
         this.featuresNativeAudioTracks = false;
×
58
         this.featuresNativeVideoTracks = false;
×
59

60
         // Give ChromecastTech class instances a reference to videojs
61
         this.videojs = videojs;
×
62
         this._eventListeners = [];
×
63

64
         this.videojsPlayer = this.videojs(options.playerId);
×
65
         this._chromecastSessionManager = this.videojsPlayer.chromecastSessionManager;
×
66

67
         this._ui.updatePoster(this.videojsPlayer.poster());
×
68

69
         this._remotePlayer = this._chromecastSessionManager.getRemotePlayer();
×
70
         this._remotePlayerController = this._chromecastSessionManager.getRemotePlayerController();
×
71
         this._listenToPlayerControllerEvents();
×
72
         this.on('dispose', this._removeAllEventListeners.bind(this));
×
73

74
         this._hasPlayedAnyItem = false;
×
75
         this._requestTitle = options.requestTitleFn || function() { /* noop */ };
×
76
         this._requestSubtitle = options.requestSubtitleFn || function() { /* noop */ };
×
77
         this._requestCustomData = options.requestCustomDataFn || function() { /* noop */ };
×
78
         // See `currentTime` function
79
         this._initialStartTime = options.startTime || 0;
×
80

81
         this._playSource(options.source, this._initialStartTime);
×
82
         this.ready(function() {
×
83
            this.setMuted(options.muted);
×
84
         }.bind(this));
85
      }
86

87
      /**
88
       * Creates a DOMElement that Video.js displays in its player UI while this Tech is
89
       * active.
90
       *
91
       * @returns {DOMElement}
92
       * @see {@link http://docs.videojs.com/Tech.html#createEl}
93
       */
94
      createEl() {
95
         // We have to initialize the UI here, because the super.constructor
96
         // calls `createEl`, which references `this._ui`.
97
         this._ui = this._ui || new ChromecastTechUI();
×
98

99
         return this._ui.getDOMElement();
×
100
      }
101

102
      /**
103
       * Resumes playback if a media item is paused or restarts an item from
104
       * its beginning if the item has played and ended.
105
       *
106
       * @see {@link http://docs.videojs.com/Player.html#play}
107
       */
108
      play() {
109
         if (!this.paused()) {
×
110
            return;
×
111
         }
112
         if (this.ended() && !this._isMediaLoading) {
×
113
            // Restart the current item from the beginning
114
            this._playSource({ src: this.videojsPlayer.src() }, 0);
×
115
         } else {
116
            this._remotePlayerController.playOrPause();
×
117
         }
118
      }
119

120
      /**
121
       * Pauses playback if the player is not already paused and if the current media item
122
       * has not ended yet.
123
       *
124
       * @see {@link http://docs.videojs.com/Player.html#pause}
125
       */
126
      pause() {
127
         if (!this.paused() && this._remotePlayer.canPause) {
×
128
            this._remotePlayerController.playOrPause();
×
129
         }
130
      }
131

132
      /**
133
       * Returns whether or not the player is "paused". Video.js'
134
       * definition of "paused" is "playback paused" OR "not playing".
135
       *
136
       * @returns {boolean} true if playback is paused
137
       * @see {@link http://docs.videojs.com/Player.html#paused}
138
       */
139
      paused() {
140
         return this._remotePlayer.isPaused || this.ended() || this._remotePlayer.playerState === null;
×
141
      }
142

143
      /**
144
       * Stores the given source and begins playback, starting at the beginning
145
       * of the media item.
146
       *
147
       * @param source {object} the source to store and play
148
       * @see {@link http://docs.videojs.com/Player.html#src}
149
       */
150
      setSource(source) {
151
         if (this._currentSource && this._currentSource.src === source.src && this._currentSource.type === source.type) {
×
152
            // Skip setting the source if the `source` argument is the
153
            // same as what's already been set. This `setSource` function
154
            // calls `this._playSource` which sends a "load media" request
155
            // to the Chromecast PlayerController. Because this function
156
            // may be called multiple times in rapid succession with the same `source`
157
            // argument, we need to de-duplicate calls with the same `source` argument to
158
            // prevent overwhelming the Chromecast PlayerController with expensive "load
159
            // media" requests, which it itself does not de-duplicate.
160
            return;
×
161
         }
162
         // We cannot use `this.videojsPlayer.currentSource()` because the
163
         // value returned by that function is not the same as what's returned
164
         // by the Video.js Player's middleware after they are run. Also, simply
165
         // using `this.videojsPlayer.src()` does not include mimetype information
166
         // which we pass to the Chromecast player.
167
         this._currentSource = source;
×
168
         this._playSource(source, 0);
×
169
      }
170

171
      /**
172
       * Plays the given source, beginning at an optional starting time.
173
       *
174
       * @private
175
       * @param source {object} the source to play
176
       * @param [startTime] The time to start playback at, in seconds
177
       * @see {@link http://docs.videojs.com/Player.html#src}
178
       */
179
      _playSource(source, startTime) {
180
         var castSession = this._getCastSession(),
×
181
             mediaInfo = new chrome.cast.media.MediaInfo(source.src, source.type),
×
182
             title = this._requestTitle(source),
×
183
             subtitle = this._requestSubtitle(source),
×
184
             poster = this.poster(),
×
185
             customData = this._requestCustomData(source),
×
186
             request;
187

188
         this.trigger('waiting');
×
189
         this._clearSessionTimeout();
×
190

191
         mediaInfo.metadata = new chrome.cast.media.GenericMediaMetadata();
×
192
         mediaInfo.metadata.metadataType = chrome.cast.media.MetadataType.GENERIC;
×
193
         mediaInfo.metadata.title = title;
×
194
         mediaInfo.metadata.subtitle = subtitle;
×
195
         mediaInfo.streamType = this.videojsPlayer.liveTracker && this.videojsPlayer.liveTracker.isLive()
×
196
            ? chrome.cast.media.StreamType.LIVE
197
            : chrome.cast.media.StreamType.BUFFERED;
198

199
         if (poster) {
×
200
            mediaInfo.metadata.images = [ { url: poster } ];
×
201
         }
202
         if (customData) {
×
203
            mediaInfo.customData = customData;
×
204
         }
205

206
         this._ui.updateTitle(title);
×
207
         this._ui.updateSubtitle(subtitle);
×
208

209
         request = new chrome.cast.media.LoadRequest(mediaInfo);
×
210
         request.autoplay = true;
×
211
         request.currentTime = startTime;
×
212

213
         this._isMediaLoading = true;
×
214
         this._hasPlayedCurrentItem = false;
×
215
         castSession.loadMedia(request)
×
216
            .then(function() {
217
               if (!this._hasPlayedAnyItem) {
×
218
                  // `triggerReady` is required here to notify the Video.js
219
                  // player that the Tech has been initialized and is ready.
220
                  this.triggerReady();
×
221
               }
222
               this.trigger('loadstart');
×
223
               this.trigger('loadeddata');
×
224
               this.trigger('play');
×
225
               this.trigger('playing');
×
226
               this._hasPlayedAnyItem = true;
×
227
               this._isMediaLoading = false;
×
228
               this._getMediaSession().addUpdateListener(this._onMediaSessionStatusChanged.bind(this));
×
229
            }.bind(this), this._triggerErrorEvent.bind(this));
230
      }
231

232
      /**
233
       * Manually updates the current time. The playback position will jump to
234
       * the given time and continue playing if the item was playing when `setCurrentTime`
235
       * was called, or remain paused if the item was paused.
236
       *
237
       * @param time {number} the playback time position to jump to
238
       * @see {@link http://docs.videojs.com/Tech.html#setCurrentTime}
239
       */
240
      setCurrentTime(time) {
241
         var duration = this.duration();
×
242

243
         if (time > duration || !this._remotePlayer.canSeek) {
×
244
            return;
×
245
         }
246
         // Seeking to any place within (approximately) 1 second of the end of the item
247
         // causes the Video.js player to get stuck in a BUFFERING state. To work around
248
         // this, we only allow seeking to within 1 second of the end of an item.
249
         this._remotePlayer.currentTime = Math.min(duration - 1, time);
×
250
         this._remotePlayerController.seek();
×
251
         this._triggerTimeUpdateEvent();
×
252
      }
253

254
      /**
255
       * Returns the current playback time position.
256
       *
257
       * @returns {number} the current playback time position
258
       * @see {@link http://docs.videojs.com/Player.html#currentTime}
259
       */
260
      currentTime() {
261
         // There is a brief period of time when Video.js has switched to the chromecast
262
         // Tech, but chromecast has not yet loaded its first media item. During
263
         // that time, Video.js calls this `currentTime` function to update
264
         // its player UI. In that period, `this._remotePlayer.currentTime`
265
         // will be 0 because the media has not loaded yet. To prevent the
266
         // UI from using a 0 second currentTime, we use the currentTime passed
267
         // in to the first media item that was provided to the Tech until
268
         // chromecast plays its first item.
269
         if (!this._hasPlayedAnyItem) {
×
270
            return this._initialStartTime;
×
271
         }
272
         return this._remotePlayer.currentTime;
×
273
      }
274

275
      /**
276
       * Returns the duration of the current media item, or `0` if the source
277
       * is not set or if the duration of the item is not available from the
278
       * Chromecast API yet.
279
       *
280
       * @returns {number} the duration of the current media item
281
       * @see {@link http://docs.videojs.com/Player.html#duration}
282
       */
283
      duration() {
284
         // There is a brief period of time when Video.js has switched to the chromecast
285
         // Tech, but chromecast has not yet loaded its first media item.
286
         // During that time, Video.js calls this `duration` function to update its player
287
         // UI. In that period, `this._remotePlayer.duration` will be 0 because the media
288
         // has not loaded yet. To prevent the UI from using a 0 second duration, we
289
         // use the duration passed in to the first media item that was provided to
290
         // the Tech until chromecast plays its first item.
291
         if (!this._hasPlayedAnyItem) {
×
292
            return this.videojsPlayer.duration();
×
293
         }
294
         return this._remotePlayer.duration;
×
295
      }
296

297
      /**
298
       * Returns whether or not the current media item has finished playing.
299
       * Returns `false` if a media item has not been loaded, has not been played,
300
       * or has not yet finished playing.
301
       *
302
       * @returns {boolean} true if the current media item has finished playing
303
       * @see {@link http://docs.videojs.com/Player.html#ended}
304
       */
305
      ended() {
306
         var mediaSession = this._getMediaSession();
×
307

308
         if (!mediaSession && this._hasMediaSessionEnded) {
×
309
            return true;
×
310
         }
311

312
         return mediaSession ? (mediaSession.idleReason === chrome.cast.media.IdleReason.FINISHED) : false;
×
313
      }
314

315
      /**
316
       * Returns the current volume level setting as a decimal number between `0` and `1`.
317
       *
318
       * @returns {number} the current volume level
319
       * @see {@link http://docs.videojs.com/Player.html#volume}
320
       */
321
      volume() {
322
         return this._remotePlayer.volumeLevel;
×
323
      }
324

325
      /**
326
       * Sets the current volume level. Volume level is a decimal number
327
       * between `0` and `1`, where `0` is muted and `1` is the loudest volume level.
328
       *
329
       * @param volumeLevel {number}
330
       * @returns {number} the current volume level
331
       * @see {@link http://docs.videojs.com/Player.html#volume}
332
       */
333
      setVolume(volumeLevel) {
334
         this._remotePlayer.volumeLevel = volumeLevel;
×
335
         this._remotePlayerController.setVolumeLevel();
×
336
         // This event is triggered by the listener on
337
         // `RemotePlayerEventType.VOLUME_LEVEL_CHANGED`, but waiting for
338
         // that event to fire in response to calls to `setVolume` introduces
339
         // noticeable lag in the updating of the player UI's volume slider bar,
340
         // which makes user interaction with the volume slider choppy.
341
         this._triggerVolumeChangeEvent();
×
342
      }
343

344
      /**
345
       * Returns whether or not the player is currently muted.
346
       *
347
       * @returns {boolean} true if the player is currently muted
348
       * @see {@link http://docs.videojs.com/Player.html#muted}
349
       */
350
      muted() {
351
         return this._remotePlayer.isMuted;
×
352
      }
353

354
      /**
355
       * Mutes or un-mutes the player. Does nothing if the player is currently
356
       * muted and the `isMuted` parameter is true or if the player is not muted and
357
       * `isMuted` is false.
358
       *
359
       * @param isMuted {boolean} whether or not the player should be muted
360
       * @see {@link http://docs.videojs.com/Html5.html#setMuted} for an example
361
       */
362
      setMuted(isMuted) {
363
         if ((this._remotePlayer.isMuted && !isMuted) || (!this._remotePlayer.isMuted && isMuted)) {
×
364
            this._remotePlayerController.muteOrUnmute();
×
365
         }
366
      }
367

368
      /**
369
       * Gets the URL to the current poster image.
370
       *
371
       * @returns {string} URL to the current poster image or `undefined` if none exists
372
       * @see {@link http://docs.videojs.com/Player.html#poster}
373
       */
374
      poster() {
375
         return this._ui.getPoster();
×
376
      }
377

378
      /**
379
       * Sets the URL to the current poster image. The poster image shown
380
       * in the Chromecast Tech UI view is updated with this new URL.
381
       *
382
       * @param poster {string} the URL to the new poster image
383
       * @see {@link http://docs.videojs.com/Tech.html#setPoster}
384
       */
385
      setPoster(poster) {
386
         this._ui.updatePoster(poster);
×
387
      }
388

389
      /**
390
       * This function is "required" when implementing {@link external:Tech}
391
       * and is supposed to return a mock
392
       * {@link https://developer.mozilla.org/en-US/docs/Web/API/TimeRanges|TimeRanges}
393
       * object that represents the portions of the current media item that have been
394
       * buffered. However, the Chromecast API does not currently provide a way
395
       * to determine how much the media item has buffered, so we always
396
       * return `undefined`.
397
       *
398
       * Returning `undefined` is safe: the player will simply not display
399
       * the buffer amount indicator in the scrubber UI.
400
       *
401
       * @returns {undefined} always returns `undefined`
402
       * @see {@link http://docs.videojs.com/Player.html#buffered}
403
       */
404
      buffered() {
405
         return undefined;
×
406
      }
407

408
      /**
409
       * This function is "required" when implementing {@link external:Tech}
410
       * and is supposed to return a mock
411
       * {@link https://developer.mozilla.org/en-US/docs/Web/API/TimeRanges|TimeRanges}
412
       * object that represents the portions of the current media item that has playable
413
       * content. However, the Chromecast API does not currently provide a
414
       * way to determine how much the media item has playable content, so
415
       * we'll just assume the entire video is an available seek target.
416
       *
417
       * The risk here lies with live streaming, where there may exist a sliding window of
418
       * playable content and seeking is only possible within the last X number of
419
       * minutes, rather than for the entire video.
420
       *
421
       * Unfortunately we have no way of detecting when this is the case. Returning
422
       * anything other than the full range of the video means that we lose the ability
423
       * to seek during VOD.
424
       *
425
       * @returns {TimeRanges} always returns a `TimeRanges` object with one
426
       * `TimeRange` that starts at `0` and ends at the `duration` of the
427
       * current media item
428
       * @see {@link http://docs.videojs.com/Player.html#seekable}
429
       */
430
      seekable() {
431
         // TODO Investigate if there's a way to detect
432
         // if the source is live, so that we can
433
         // possibly adjust the seekable `TimeRanges` accordingly.
434
         return this.videojs.createTimeRange(0, this.duration());
×
435
      }
436

437
      /**
438
       * Returns whether the native media controls should be shown (`true`) or hidden
439
       * (`false`). Not applicable to this Tech.
440
       *
441
       * @returns {boolean} always returns `false`
442
       * @see {@link http://docs.videojs.com/Html5.html#controls} for an example
443
       */
444
      controls() {
445
         return false;
×
446
      }
447

448
      /**
449
       * Returns whether or not the browser should show the player
450
       * "inline" (non-fullscreen) by default. This function always
451
       * returns true to tell the browser that non-fullscreen playback is preferred.
452
       *
453
       * @returns {boolean} always returns `true`
454
       * @see {@link http://docs.videojs.com/Html5.html#playsinline} for an example
455
       */
456
      playsinline() {
457
         return true;
×
458
      }
459

460
      /**
461
       * Returns whether or not fullscreen is supported by this Tech.
462
       * Always returns `true` because fullscreen is always supported.
463
       *
464
       * @returns {boolean} always returns `true`
465
       * @see {@link http://docs.videojs.com/Html5.html#supportsFullScreen} for an example
466
       */
467
      supportsFullScreen() {
468
         return true;
×
469
      }
470

471
      /**
472
       * Sets a flag that determines whether or not the media should automatically begin
473
       * playing on page load. This is not supported because a Chromecast session must be
474
       * initiated by casting via the casting menu and cannot autoplay.
475
       *
476
       * @see {@link http://docs.videojs.com/Html5.html#setAutoplay} for an example
477
       */
478
      setAutoplay() {
479
         // Not supported
480
      }
481

482
      /**
483
       * @returns {number} the chromecast player's playback rate, if available. Otherwise,
484
       * the return value defaults to `1`.
485
       */
486
      playbackRate() {
487
         var mediaSession = this._getMediaSession();
×
488

489
         return mediaSession ? mediaSession.playbackRate : 1;
×
490
      }
491

492
      /**
493
       * Does nothing. Changing the playback rate is not supported.
494
       */
495
      setPlaybackRate() {
496
         // Not supported
497
      }
498

499
      /**
500
       * Does nothing. Satisfies calls to the missing preload method.
501
       */
502
      preload() {
503
         // Not supported
504
      }
505

506
      /**
507
       * Causes the Tech to begin loading the current source. `load`
508
       * is not supported in this ChromecastTech because setting the
509
       * source on the `Chromecast` automatically causes it to begin loading.
510
       */
511
      load() {
512
         // Not supported
513
      }
514

515
      /**
516
       * Gets the Chromecast equivalent of HTML5 Media Element's `readyState`.
517
       *
518
       * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState
519
       */
520
      readyState() {
521
         if (this._remotePlayer.playerState === 'IDLE' || this._remotePlayer.playerState === 'BUFFERING') {
×
522
            return 0; // HAVE_NOTHING
×
523
         }
524
         return 4;
×
525
      }
526

527
      /**
528
       * Wires up event listeners for
529
       * [RemotePlayerController](https://developers.google.com/cast/docs/reference/chrome/cast.framework.RemotePlayerController)
530
       * events.
531
       *
532
       * @private
533
       */
534
      _listenToPlayerControllerEvents() {
535
         var eventTypes = cast.framework.RemotePlayerEventType;
×
536

537
         this._addEventListener(this._remotePlayerController, eventTypes.PLAYER_STATE_CHANGED, this._onPlayerStateChanged, this);
×
538
         this._addEventListener(this._remotePlayerController, eventTypes.VOLUME_LEVEL_CHANGED, this._triggerVolumeChangeEvent, this);
×
539
         this._addEventListener(this._remotePlayerController, eventTypes.IS_MUTED_CHANGED, this._triggerVolumeChangeEvent, this);
×
540
         this._addEventListener(this._remotePlayerController, eventTypes.CURRENT_TIME_CHANGED, this._triggerTimeUpdateEvent, this);
×
541
         this._addEventListener(this._remotePlayerController, eventTypes.DURATION_CHANGED, this._triggerDurationChangeEvent, this);
×
542
      }
543

544
      /**
545
       * Registers an event listener on the given target object.
546
       * Because many objects in the Chromecast API are either singletons
547
       * or must be shared between instances of `ChromecastTech` for the
548
       * lifetime of the player, we must unbind the listeners when this Tech
549
       * instance is destroyed to prevent memory leaks. To do that, we need to keep
550
       * a reference to listeners that are added to global objects so that we can
551
       * use those references to remove the listener when this Tech is destroyed.
552
       *
553
       * @param target {object} the object to register the event listener on
554
       * @param type {string} the name of the event
555
       * @param callback {Function} the listener's callback function that
556
       * executes when the event is emitted
557
       * @param context {object} the `this` context to use when executing the `callback`
558
       * @private
559
       */
560
      _addEventListener(target, type, callback, context) {
561
         var listener;
562

563
         listener = {
×
564
            target: target,
565
            type: type,
566
            callback: callback,
567
            context: context,
568
            listener: callback.bind(context),
569
         };
570
         target.addEventListener(type, listener.listener);
×
571
         this._eventListeners.push(listener);
×
572
      }
573

574
      /**
575
       * Removes all event listeners that were registered with global objects during the
576
       * lifetime of this Tech. See {@link _addEventListener} for more information
577
       * about why this is necessary.
578
       *
579
       * @private
580
       */
581
      _removeAllEventListeners() {
582
         while (this._eventListeners.length > 0) {
×
583
            this._removeEventListener(this._eventListeners[0]);
×
584
         }
585
         this._eventListeners = [];
×
586
      }
587

588
      /**
589
       * Removes a single event listener that was registered with global objects
590
       * during the lifetime of this Tech. See {@link _addEventListener} for
591
       * more information about why this is necessary.
592
       *
593
       * @private
594
       */
595
      _removeEventListener(listener) {
596
         var index = -1,
×
597
             pass = false,
×
598
             i;
599

600
         listener.target.removeEventListener(listener.type, listener.listener);
×
601

602
         for (i = 0; i < this._eventListeners.length; i++) {
×
603
            pass = this._eventListeners[i].target === listener.target &&
×
604
                  this._eventListeners[i].type === listener.type &&
605
                  this._eventListeners[i].callback === listener.callback &&
606
                  this._eventListeners[i].context === listener.context;
607

608
            if (pass) {
×
609
               index = i;
×
610
               break;
×
611
            }
612
         }
613

614
         if (index !== -1) {
×
615
            this._eventListeners.splice(index, 1);
×
616
         }
617
      }
618

619
      /**
620
       * Handles Chromecast player state change events. The player may "change state" when
621
       * paused, played, buffering, etc.
622
       *
623
       * @private
624
       */
625
      _onPlayerStateChanged() {
626
         var states = chrome.cast.media.PlayerState,
×
627
             playerState = this._remotePlayer.playerState;
×
628

629
         if (playerState === states.PLAYING) {
×
630
            this._hasPlayedCurrentItem = true;
×
631
            this.trigger('play');
×
632
            this.trigger('playing');
×
633
         } else if (playerState === states.PAUSED) {
×
634
            this.trigger('pause');
×
635
         } else if ((playerState === states.IDLE && this.ended()) || (playerState === null && this._hasPlayedCurrentItem)) {
×
636
            this._hasPlayedCurrentItem = false;
×
637
            this._closeSessionOnTimeout();
×
638
            this.trigger('ended');
×
639
            this._triggerTimeUpdateEvent();
×
640
         } else if (playerState === states.BUFFERING) {
×
641
            this.trigger('waiting');
×
642
         }
643
      }
644

645
      /**
646
       * Handles Chromecast MediaSession state change events. The only property sent
647
       * to this event is whether the session is alive. This is useful for determining
648
       * if an item has ended as the MediaSession will fire this event with `false` then
649
       * be immediately destroyed. This means that we cannot trust `idleReason` to show
650
       * whether an item has ended since we may no longer have access to the MediaSession.
651
       *
652
       * @private
653
       */
654
      _onMediaSessionStatusChanged(isAlive) {
655
         this._hasMediaSessionEnded = !!isAlive;
×
656
      }
657

658
      /**
659
       * Ends the session after a certain number of seconds of inactivity.
660
       *
661
       * If the Chromecast player is in the "IDLE" state after an item has ended, and no
662
       * further items are queued up to play, the session is considered inactive. Once a
663
       * period of time (currently 10 seconds) has elapsed with no activity, we manually
664
       * end the session to prevent long periods of a blank Chromecast screen that is
665
       * shown at the end of item playback.
666
       *
667
       * @private
668
       */
669
      _closeSessionOnTimeout() {
670
         // Ensure that there's never more than one session timeout active
671
         this._clearSessionTimeout();
×
672
         this._sessionTimeoutID = setTimeout(function() {
×
673
            var castSession = this._getCastSession();
×
674

675
            if (castSession) {
×
676
               castSession.endSession(true);
×
677
            }
678
            this._clearSessionTimeout();
×
679
         }.bind(this), SESSION_TIMEOUT);
680
      }
681

682
      /**
683
       * Stops the timeout that is waiting during a period of inactivity in order to close
684
       * the session.
685
       *
686
       * @private
687
       * @see _closeSessionOnTimeout
688
       */
689
      _clearSessionTimeout() {
690
         if (this._sessionTimeoutID) {
×
691
            clearTimeout(this._sessionTimeoutID);
×
692
            this._sessionTimeoutID = false;
×
693
         }
694
      }
695

696
      /**
697
       * @private
698
       * @return {object} the current CastContext, if one exists
699
       */
700
      _getCastContext() {
701
         return this._chromecastSessionManager.getCastContext();
×
702
      }
703

704
      /**
705
       * @private
706
       * @return {object} the current CastSession, if one exists
707
       */
708
      _getCastSession() {
709
         return this._getCastContext().getCurrentSession();
×
710
      }
711

712
      /**
713
       * @private
714
       * @return {object} the current MediaSession, if one exists
715
       * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.Media
716
       */
717
      _getMediaSession() {
718
         var castSession = this._getCastSession();
×
719

720
         return castSession ? castSession.getMediaSession() : null;
×
721
      }
722

723
      /**
724
       * Triggers a 'volumechange' event
725
       * @private
726
       * @see http://docs.videojs.com/Player.html#event:volumechange
727
       */
728
      _triggerVolumeChangeEvent() {
729
         this.trigger('volumechange');
×
730
      }
731

732
      /**
733
       * Triggers a 'timeupdate' event
734
       * @private
735
       * @see http://docs.videojs.com/Player.html#event:timeupdate
736
       */
737
      _triggerTimeUpdateEvent() {
738
         this.trigger('timeupdate');
×
739
      }
740

741
      /**
742
       * Triggers a 'durationchange' event
743
       * @private
744
       * @see http://docs.videojs.com/Player.html#event:durationchange
745
       */
746
      _triggerDurationChangeEvent() {
747
         this.trigger('durationchange');
×
748
      }
749

750
      /**
751
       * Triggers an 'error' event
752
       * @private
753
       * @see http://docs.videojs.com/Player.html#event:error
754
       */
755
      _triggerErrorEvent() {
756
         this.trigger('error');
×
757
      }
758
   }
759

760
   // Required for Video.js Tech implementations.
761
   // TODO Consider a more comprehensive check based on mimetype.
762
   ChromecastTech.canPlaySource = () => {
1✔
763
      return ChromecastSessionManager.isChromecastConnected();
×
764
   };
765

766
   ChromecastTech.isSupported = () => {
1✔
767
      return ChromecastSessionManager.isChromecastConnected();
×
768
   };
769

770
   videojs.registerTech('chromecast', ChromecastTech);
1✔
771
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc