@eluvio/elv-player-js 2.0.22 → 2.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,12 @@
1
1
  import EluvioPlayerParameters from "./PlayerParameters.js";
2
2
  import {InitializeFairPlayStream} from "./FairPlay.js";
3
-
3
+ import Cast from "./Cast";
4
4
  import {Utils} from "@eluvio/elv-client-js";
5
5
  import PlayerControls from "./Controls.js";
6
6
  import {MergeDefaultParameters} from "../ui/Common";
7
7
 
8
+ const isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent);
9
+
8
10
  const PlayerProfiles = {
9
11
  default: {
10
12
  label: "Default",
@@ -40,6 +42,7 @@ export class EluvioPlayer {
40
42
  }
41
43
 
42
44
  constructor({target, video, parameters, SetErrorMessage}) {
45
+ this.latest = true;
43
46
  this.loading = true;
44
47
  this.target = target;
45
48
  this.video = video;
@@ -53,8 +56,12 @@ export class EluvioPlayer {
53
56
  this.canPlay = false;
54
57
  this.isLive = false;
55
58
  this.dvrEnabled = false;
59
+ this.dvrAvailable = false;
56
60
  this.behindLiveEdge = false;
57
61
  this.publicMetadataUrl = undefined;
62
+ this.airplayAvailable = false;
63
+ this.chromecastAvailable = false;
64
+ this.casting = false;
58
65
 
59
66
  try {
60
67
  // If custom HLS parameters are specified, set profile to custom
@@ -84,11 +91,9 @@ export class EluvioPlayer {
84
91
  configUrl: this.clientOptions.network
85
92
  });
86
93
 
87
- this.clientOptions.client.SetStaticToken({
88
- token:
89
- this.clientOptions.staticToken ||
90
- this.clientOptions.client.utils.B64(JSON.stringify({qspace_id: await this.clientOptions.client.ContentSpaceId()}))
91
- });
94
+ if(this.clientOptions.staticToken) {
95
+ this.clientOptions.client.SetStaticToken({token: this.clientOptions.staticToken});
96
+ }
92
97
 
93
98
  return this.clientOptions.client;
94
99
  })();
@@ -151,9 +156,9 @@ export class EluvioPlayer {
151
156
 
152
157
  if(this.sourceOptions.contentInfo.liveDVR === EluvioPlayerParameters.liveDVR.ON && offeringProperties.dvr_available) {
153
158
  options.dvr = 1;
154
- this.dvrEnabled = true;
159
+ this.dvrAvailable = true;
155
160
  } else {
156
- this.dvrEnabled = false;
161
+ this.dvrAvailable = false;
157
162
  }
158
163
 
159
164
  if(offeringProperties.live) {
@@ -200,6 +205,10 @@ export class EluvioPlayer {
200
205
  const versionHash = playoutUrl.split("/").find(segment => segment.startsWith("hq__"));
201
206
 
202
207
  return {
208
+ playoutParameters: {
209
+ ...playoutParameters,
210
+ options
211
+ },
203
212
  protocol,
204
213
  drm,
205
214
  playoutUrl,
@@ -294,6 +303,8 @@ export class EluvioPlayer {
294
303
 
295
304
  this.__Reset();
296
305
 
306
+
307
+
297
308
  this.initialized = false;
298
309
  this.loading = true;
299
310
  this.initTime = Date.now();
@@ -331,6 +342,24 @@ export class EluvioPlayer {
331
342
  }
332
343
  }
333
344
 
345
+ // Handle Chromecast
346
+ this.castHandler = new Cast({
347
+ player: this,
348
+ onReady: () => {
349
+ this.chromecastAvailable = true;
350
+ this.__SettingsUpdate();
351
+ },
352
+ onUpdate: () => {
353
+ this.__SettingsUpdate();
354
+ }
355
+ });
356
+
357
+ // Detect Airplay availability
358
+ this.__RegisterVideoEventListener("webkitplaybacktargetavailabilitychanged", event => {
359
+ this.airplayAvailable = event.availability === "available";
360
+ this.__SettingsUpdate();
361
+ });
362
+
334
363
  this.__RegisterVideoEventListener("play", () => {
335
364
  this.reloads = 0;
336
365
  this.playbackStarted = true;
@@ -350,7 +379,7 @@ export class EluvioPlayer {
350
379
  });
351
380
 
352
381
  this.__RegisterVideoEventListener("seeking", () => {
353
- if(!this.isLive || !this.dvrEnabled) { return; }
382
+ if(!this.isLive || !this.dvrAvailable) { return; }
354
383
  const diff = this.video.duration - this.video.currentTime;
355
384
  this.behindLiveEdge = diff > 15;
356
385
  this.__SettingsUpdate();
@@ -363,7 +392,7 @@ export class EluvioPlayer {
363
392
  this.__RegisterVideoEventListener("ended", () => this.controls && this.controls.CollectionPlayNext({autoplay: true}));
364
393
  }
365
394
 
366
- let { versionHash, playoutUrl, protocol, drm, drms, multiviewOptions } = await this.__PlayoutOptions();
395
+ let { versionHash, playoutUrl, protocol, drm, drms, multiviewOptions, playoutParameters } = await this.__PlayoutOptions();
367
396
 
368
397
  this.contentHash = versionHash;
369
398
 
@@ -376,6 +405,13 @@ export class EluvioPlayer {
376
405
 
377
406
  this.authorizationToken = authorizationToken;
378
407
 
408
+ this.playoutUrl = playoutUrl.toString();
409
+
410
+ this.castHandler.SetMedia({
411
+ playoutOptions: this.sourceOptions.playoutOptions,
412
+ playoutParameters
413
+ });
414
+
379
415
  if(this.__destroyed) { return; }
380
416
 
381
417
  if(protocol === "hls") {
@@ -480,7 +516,8 @@ export class EluvioPlayer {
480
516
  async __InitializeHLS({playoutUrl, authorizationToken, drm, drms, multiviewOptions}) {
481
517
  this.HLS = (await import("hls.js")).default;
482
518
 
483
- if([EluvioPlayerParameters.drms.FAIRPLAY, EluvioPlayerParameters.drms.SAMPLE_AES].includes(drm) || !this.HLS.isSupported()) {
519
+ const nativeHLSSupported = this.video.canPlayType("application/vnd.apple.mpegURL");
520
+ if((nativeHLSSupported && isIOS) || [EluvioPlayerParameters.drms.FAIRPLAY, EluvioPlayerParameters.drms.SAMPLE_AES].includes(drm) || !this.HLS.isSupported()) {
484
521
  // HLS JS NOT SUPPORTED - Handle native player
485
522
  this.nativeHLS = true;
486
523
 
@@ -550,7 +587,7 @@ export class EluvioPlayer {
550
587
  ...customProfileSettings
551
588
  };
552
589
 
553
- if(this.dvrEnabled) {
590
+ if(this.dvrAvailable) {
554
591
  delete this.hlsOptions.liveMaxLatencyDuration;
555
592
  delete this.hlsOptions.liveMaxLatencyDurationCount;
556
593
  }
@@ -670,6 +707,14 @@ export class EluvioPlayer {
670
707
 
671
708
  this.hlsPlayer = hlsPlayer;
672
709
  this.player = hlsPlayer;
710
+
711
+ if(nativeHLSSupported) {
712
+ /* Add alternate source for airplay */
713
+ const source = document.createElement("source");
714
+ source.src = this.playoutUrl.toString();
715
+ this.video.appendChild(source);
716
+ this.video.disableRemotePlayback = false;
717
+ }
673
718
  }
674
719
  }
675
720
 
@@ -981,6 +1026,7 @@ export class EluvioPlayer {
981
1026
  }
982
1027
 
983
1028
  __DestroyPlayer() {
1029
+ this.castHandler.Disconnect();
984
1030
  this.__destroyed = true;
985
1031
  this.__Reset();
986
1032
  }
@@ -1014,6 +1060,7 @@ export class EluvioPlayer {
1014
1060
  }
1015
1061
  });
1016
1062
 
1063
+ this.__settingsListeners = [];
1017
1064
  this.__listenerDisposers = [];
1018
1065
  this.__showPlayerProfileForm = false;
1019
1066
 
@@ -1035,8 +1082,12 @@ export class EluvioPlayer {
1035
1082
  this.canPlay = false;
1036
1083
  this.isLive = false;
1037
1084
  this.behindLiveEdge = false;
1085
+ this.dvrAvailable = false;
1038
1086
  this.dvrEnabled = false;
1039
1087
  this.publicMetadataUrl = undefined;
1088
+ this.airplayAvailable = false;
1089
+ this.chromecastAvailable = false;
1090
+ this.casting = false;
1040
1091
  }
1041
1092
 
1042
1093
  async __HardReload(error, delay=6000) {
@@ -1113,6 +1164,17 @@ export class EluvioPlayer {
1113
1164
  }
1114
1165
  }
1115
1166
  }
1167
+
1168
+ __SetCasting(casting) {
1169
+ if(casting) {
1170
+ this.controls.SetDVREnabled(false);
1171
+ this.controls.Pause();
1172
+ }
1173
+
1174
+ this.casting = casting;
1175
+
1176
+ this.__SettingsUpdate();
1177
+ }
1116
1178
  }
1117
1179
 
1118
1180
  EluvioPlayer.EluvioPlayerParameters = EluvioPlayerParameters;
@@ -32,3 +32,5 @@ export const RotateIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\
32
32
  export const ContentBadgeIcon = `<svg width=\"30\" height=\"30\" viewBox=\"0 0 30 30\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M15 27.5C15 27.5 25 22.5 25 15V6.25L15 2.5L5 6.25V15C5 22.5 15 27.5 15 27.5Z\" stroke=\"currentColor\" stroke-width=\"2.14286\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M19.715 11.3574L13.8221 17.8396L11.1436 14.8931\" stroke=\"currentColor\" stroke-width=\"2.14286\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>`;
33
33
  export const ContentCredentialsIcon = `<svg width=\"26\" height=\"26\" viewBox=\"0 0 26 26\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M23.7508 12.9997V23.7508H13.0003C7.05943 23.7508 2.24919 18.9406 2.24919 12.9997C2.24919 7.05888 7.05943 2.24864 13.0003 2.24864C18.9411 2.24864 23.7508 7.05888 23.7508 12.9997ZM0 12.9997C0 5.82396 5.82396 0 12.9997 0C20.1755 0 26 5.82396 26 12.9997V25.9995H13.0003C5.82396 25.9995 0 20.1755 0 12.9997ZM5.174 13.5197C5.174 16.1976 6.98078 18.4599 9.85371 18.4599C12.2198 18.4599 13.8185 16.9 14.2084 14.8589H11.8686C11.5698 15.7951 10.8155 16.367 9.85371 16.367C8.39758 16.367 7.44886 15.2227 7.44886 13.5202C7.44886 11.8178 8.39813 10.6735 9.85371 10.6735C10.7899 10.6735 11.5305 11.2066 11.8429 12.0903H14.1959C13.7797 10.1011 12.1941 8.58055 9.85371 8.58055C6.96822 8.58001 5.174 10.8418 5.174 13.5197ZM17.3419 8.83999H15.132V18.2131H17.4331V13.3253C17.4331 12.4022 17.6931 11.8041 18.1349 11.4273C18.5249 11.0761 19.0318 10.8942 19.8642 10.8942H20.4491V8.72256H19.8773C18.668 8.72256 17.8624 9.16442 17.3424 9.8406V8.82633V8.83999H17.3419Z\" fill=\"currentColor\"/></svg>`;
34
34
  export const CopyIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-copy\"><rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\"></rect><path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\"></path></svg>`;
35
+ export const AirplayIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-airplay\"><path d=\"M5 17H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-1\"></path><polygon points=\"12 15 17 21 7 21 12 15\"></polygon></svg>`;
36
+ export const ChromecastIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-cast\"><path d=\"M2 16.1A5 5 0 0 1 5.9 20M2 12.05A9 9 0 0 1 9.95 20M2 8V6a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-6\"></path><line x1=\"2\" y1=\"20\" x2=\"2.01\" y2=\"20\"></line></svg>`;
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-airplay"><path d="M5 17H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-1"></path><polygon points="12 15 17 21 7 21 12 15"></polygon></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cast"><path d="M2 16.1A5 5 0 0 1 5.9 20M2 12.05A9 9 0 0 1 9.95 20M2 8V6a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-6"></path><line x1="2" y1="20" x2="2.01" y2="20"></line></svg>
@@ -543,6 +543,57 @@
543
543
  }
544
544
  }
545
545
 
546
+ .dvr-toggle {
547
+ --border-color: #a2a3a3;
548
+
549
+ display: flex;
550
+ height: 30px;
551
+ position: relative;
552
+
553
+ &__border {
554
+ border: 2px solid var(--border-color);
555
+ border-radius: 5px;
556
+ height: 100%;
557
+ inset: 0;
558
+ opacity: 0.5;
559
+ position: absolute;
560
+ z-index: 0;
561
+ }
562
+
563
+ &__live,
564
+ &__dvr {
565
+ align-items: center;
566
+ border-radius: 5px;
567
+ color: var(--border-color);
568
+ cursor: pointer!important;
569
+ display: flex;
570
+ font-size: 16px;
571
+ font-weight: 500;
572
+ height: 100%;
573
+ justify-content: center;
574
+ opacity: 0.5;
575
+ transition: background-color 0.25s ease, color 0.25s ease, opacity 0.25s ease;
576
+ width: 75px;
577
+ z-index: 1;
578
+
579
+ &--active {
580
+ color: var(--color-text);
581
+ opacity: 1;
582
+ }
583
+ }
584
+
585
+ &__live {
586
+ &--active {
587
+ background-color: #ea3323;
588
+ }
589
+ }
590
+
591
+ &__dvr {
592
+ &--active {
593
+ background-color: #4666ac;
594
+ }
595
+ }
596
+ }
546
597
 
547
598
  /* Player size/orientation adjustments */
548
599
 
@@ -597,6 +648,20 @@
597
648
  height: 15px;
598
649
  }
599
650
  }
651
+
652
+ .dvr-toggle {
653
+ height: 25px;
654
+
655
+ &__border {
656
+ border-width: 1px;
657
+ }
658
+
659
+ &__live,
660
+ &__dvr {
661
+ font-size: 10px;
662
+ width: 40px;
663
+ }
664
+ }
600
665
  }
601
666
 
602
667
  :global(.__eluvio-player--size-sm) {
@@ -347,6 +347,7 @@
347
347
 
348
348
  .menu-control-container {
349
349
  position: relative;
350
+ z-index: var(--layer-menu);
350
351
 
351
352
  .icon-button {
352
353
  height: 45px;
@@ -89,6 +89,20 @@
89
89
  }
90
90
  }
91
91
 
92
+ google-cast-launcher {
93
+ --disconnected-color: var(--color-button)!important;
94
+ --connected-color: var(--color-text-highlight)!important;
95
+
96
+ animation: 0.5s fadein ease!important;
97
+ cursor: pointer;
98
+ height: 30px!important;
99
+ width: 30px!important;
100
+
101
+ * {
102
+ transition: color 0.15s ease;
103
+ }
104
+ }
105
+
92
106
  /* Content Info */
93
107
 
94
108
  .info-container {
@@ -342,6 +356,7 @@
342
356
 
343
357
  .menu-control-container {
344
358
  position: relative;
359
+ z-index: var(--layer-menu);
345
360
 
346
361
  .icon-button {
347
362
  border: 1px solid transparent;
@@ -45,6 +45,22 @@
45
45
  }
46
46
  }
47
47
 
48
+ .cast-indicator-container {
49
+ align-items: center;
50
+ display: flex;
51
+ inset: 0;
52
+ justify-content: center;
53
+ position: absolute;
54
+ z-index: var(--layer-center-button);
55
+
56
+ svg {
57
+ color: #FFF;
58
+ height: 50px;
59
+ width: 50px;
60
+ }
61
+ }
62
+
63
+
48
64
  .controls {
49
65
  z-index: var(--layer-controls);
50
66
  }
@@ -35,6 +35,8 @@ const iconSource = {
35
35
  ContentBadgeIcon: Path.resolve(__dirname, "../static/icons/svgs/content-badge.svg"),
36
36
  ContentCredentialsIcon: Path.resolve(__dirname, "../static/icons/svgs/content-credentials.svg"),
37
37
  CopyIcon: Path.resolve(__dirname, "../static/icons/svgs/copy.svg"),
38
+ AirplayIcon: Path.resolve(__dirname, "../static/icons/svgs/airplay.svg"),
39
+ ChromecastIcon: Path.resolve(__dirname, "../static/icons/svgs/cast.svg")
38
40
  };
39
41
 
40
42
  let iconFile = "// WARNING: Do not edit this file manually\n"
@@ -61,23 +61,30 @@ export const UserActionIndicator = ({action}) => {
61
61
  };
62
62
 
63
63
  export const SeekBar = ({player, videoState, setRecentUserAction, className=""}) => {
64
- const [currentTime, setCurrentTime] = useState(player.video.currentTime);
64
+ const [currentTime, setCurrentTime] = useState(player.controls.GetCurrentTime());
65
65
  const [bufferFraction, setBufferFraction] = useState(0);
66
66
  const [seekKeydownHandler, setSeekKeydownHandler] = useState(undefined);
67
+ const [dvrEnabled, setDVREnabled] = useState(player.controls.IsDVREnabled());
67
68
 
68
69
  useEffect(() => {
69
70
  setSeekKeydownHandler(SeekSliderKeyDown(player, setRecentUserAction));
70
71
 
71
- const disposeVideoTimeObserver = ObserveVideoTime({video: player.video, setCurrentTime, rate: 60});
72
+ const disposeVideoTimeObserver = ObserveVideoTime({player, setCurrentTime, rate: 60});
72
73
  const disposeVideoBufferObserver = ObserveVideoBuffer({video: player.video, setBufferFraction});
74
+ const disposeSettingsListener = player.controls.RegisterSettingsListener(() => {
75
+ if(!player.controls) { return; }
76
+
77
+ setDVREnabled(player.controls.IsDVREnabled())
78
+ });
73
79
 
74
80
  return () => {
75
81
  disposeVideoTimeObserver && disposeVideoTimeObserver();
76
82
  disposeVideoBufferObserver && disposeVideoBufferObserver();
83
+ disposeSettingsListener && disposeSettingsListener();
77
84
  };
78
- }, []);
85
+ }, [player && player.controls]);
79
86
 
80
- if(player.isLive && !player.dvrEnabled) {
87
+ if(player.isLive && !dvrEnabled) {
81
88
  return null;
82
89
  }
83
90
 
@@ -85,7 +92,7 @@ export const SeekBar = ({player, videoState, setRecentUserAction, className=""})
85
92
  <div className={`${className} ${CommonStyles["seek-container"]} ${className}`}>
86
93
  <progress
87
94
  max={1}
88
- value={bufferFraction}
95
+ value={player.casting ? 0 : bufferFraction}
89
96
  className={CommonStyles["seek-buffer"]}
90
97
  />
91
98
  <progress
@@ -288,6 +295,38 @@ export const SettingsMenu = ({player, Hide, className=""}) => {
288
295
  );
289
296
  };
290
297
 
298
+ export const DVRToggle = ({player}) => {
299
+ const [dvrEnabled, setDVREnabled] = useState(player.dvrEnabled);
300
+
301
+ useEffect(() => {
302
+ const disposer = player.controls.RegisterSettingsListener(() => {
303
+ if(!player.controls) { return; }
304
+
305
+ setDVREnabled(player.controls.IsDVREnabled())
306
+ });
307
+
308
+ return () => disposer && disposer();
309
+ }, [player && player.controls]);
310
+
311
+ return (
312
+ <div className={CommonStyles["dvr-toggle"]}>
313
+ <button
314
+ onClick={() => player.controls.SetDVREnabled(false)}
315
+ className={`${CommonStyles["dvr-toggle__live"]} ${!dvrEnabled ? CommonStyles["dvr-toggle__live--active"] : ""}`}
316
+ >
317
+ LIVE
318
+ </button>
319
+ <button
320
+ onClick={() => player.controls.SetDVREnabled(true)}
321
+ className={`${CommonStyles["dvr-toggle__dvr"]} ${dvrEnabled ? CommonStyles["dvr-toggle__dvr--active"] : ""}`}
322
+ >
323
+ DVR
324
+ </button>
325
+ <div className={CommonStyles["dvr-toggle__border"]}/>
326
+ </div>
327
+ );
328
+ };
329
+
291
330
  export const Copy = async (value) => {
292
331
  try {
293
332
  value = (value || "").toString();
@@ -49,12 +49,14 @@ export const RegisterModal = ({element, Hide}) => {
49
49
  };
50
50
  };
51
51
 
52
- export const ObserveVideo = ({target, video, setVideoState}) => {
52
+ export const ObserveVideo = ({player, setVideoState}) => {
53
53
  const UpdateVideoState = function () {
54
- const buffer = video.buffered;
54
+ if(!player || !player.controls) { return; }
55
+
56
+ const buffer = player.video.buffered;
55
57
  let end = 0;
56
58
  for(let i = 0; i < buffer.length; i++) {
57
- if(buffer.start(i) > video.currentTime) { continue; }
59
+ if(buffer.start(i) > player.controls.GetCurrentTime()) { continue; }
58
60
 
59
61
  if(buffer.end(i) > end) {
60
62
  end = buffer.end(i);
@@ -62,11 +64,11 @@ export const ObserveVideo = ({target, video, setVideoState}) => {
62
64
  }
63
65
 
64
66
  setVideoState({
65
- playing: !video.paused,
66
- duration: video.duration,
67
- volume: video.volume,
68
- muted: video.muted,
69
- rate: video.playbackRate,
67
+ playing: player.controls.IsPlaying(),
68
+ duration: player.controls.GetDuration(),
69
+ volume: player.controls.GetVolume(),
70
+ muted: player.controls.IsMuted(),
71
+ rate: player.controls.GetPlaybackRate(),
70
72
  fullscreen: !!(document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement)
71
73
  });
72
74
  };
@@ -80,12 +82,15 @@ export const ObserveVideo = ({target, video, setVideoState}) => {
80
82
  "ratechange"
81
83
  ];
82
84
 
83
- events.map(event => video.addEventListener(event, UpdateVideoState));
84
- target.addEventListener("fullscreenchange", UpdateVideoState);
85
+ events.map(event => player.video.addEventListener(event, UpdateVideoState));
86
+ player.target.addEventListener("fullscreenchange", UpdateVideoState);
87
+
88
+ const castListenerDisposer = player.castHandler.RegisterListener(UpdateVideoState);
85
89
 
86
90
  return () => {
87
- events.map(event => video.removeEventListener(event, UpdateVideoState));
88
- target.removeEventListener("fullscreenchange", UpdateVideoState);
91
+ events.map(event => player.video.removeEventListener(event, UpdateVideoState));
92
+ player.target.removeEventListener("fullscreenchange", UpdateVideoState);
93
+ castListenerDisposer && castListenerDisposer();
89
94
  };
90
95
  };
91
96
 
@@ -113,10 +118,12 @@ export const ObserveVideoBuffer = ({video, setBufferFraction}) => {
113
118
  return () => video.removeEventListener("progress", UpdateBufferState);
114
119
  };
115
120
 
116
- export const ObserveVideoTime = ({video, rate=10, setCurrentTime}) => {
121
+ export const ObserveVideoTime = ({player, rate=10, setCurrentTime}) => {
117
122
  // Current time doesn't update quickly enough from events for smooth movement - use interval instead
118
123
  const currentTimeInterval = setInterval(() => {
119
- setCurrentTime(video.currentTime);
124
+ if(!player.controls) { return; }
125
+
126
+ setCurrentTime(player.controls.GetCurrentTime());
120
127
  }, 1000 / rate);
121
128
 
122
129
  return () => {
@@ -334,10 +341,13 @@ export const ObserveKeydown = ({player, setRecentUserAction}) => {
334
341
  index: playbackRates.active.index + (event.key === "<" ? -1 : 1)
335
342
  });
336
343
  }
337
- setRecentUserAction({
338
- action: result.increase ? ACTIONS.PLAYBACK_RATE_UP : ACTIONS.PLAYBACK_RATE_DOWN,
339
- text: `${result.rate.toFixed(2)}x`
340
- });
344
+
345
+ if(result) {
346
+ setRecentUserAction({
347
+ action: result.increase ? ACTIONS.PLAYBACK_RATE_UP : ACTIONS.PLAYBACK_RATE_DOWN,
348
+ text: `${result.rate.toFixed(2)}x`
349
+ });
350
+ }
341
351
  break;
342
352
  case "c":
343
353
  result = player.controls.ToggleTextTrack();
@@ -19,6 +19,7 @@ import {Spinner, UserActionIndicator} from "./Components.jsx";
19
19
  import TVControls from "./TVControls.jsx";
20
20
  import PlayerProfileForm from "./PlayerProfileForm.jsx";
21
21
  import {ImageUrl, MergeDefaultParameters} from "./Common.js";
22
+ import {ChromecastIcon} from "../static/icons/Icons.js";
22
23
 
23
24
  const Poster = ({player}) => {
24
25
  const [imageUrl, setImageUrl] = useState(undefined);
@@ -64,6 +65,7 @@ const PlayerUI = ({target, parameters, InitCallback, ErrorCallback, Unmount, Res
64
65
  const [recentlyInteracted, setRecentlyInteracted] = useState(true);
65
66
  const [recentUserAction, setRecentUserAction] = useState(undefined);
66
67
  const [allowRotation, setAllowRotation] = useState(undefined);
68
+ const [casting, setCasting] = useState(false);
67
69
  const videoRef = useRef();
68
70
 
69
71
  const playerSet = !!player;
@@ -110,6 +112,7 @@ const PlayerUI = ({target, parameters, InitCallback, ErrorCallback, Unmount, Res
110
112
  setPlaybackStarted(newPlayer.playbackStarted);
111
113
  setPlayerInitialized(!newPlayer.loading);
112
114
  setShowPlayerProfileForm(newPlayer.__showPlayerProfileForm);
115
+ setCasting(newPlayer && newPlayer.casting);
113
116
  }
114
117
  );
115
118
 
@@ -225,6 +228,7 @@ const PlayerUI = ({target, parameters, InitCallback, ErrorCallback, Unmount, Res
225
228
  muted={[EluvioPlayerParameters.muted.ON, EluvioPlayerParameters.muted.WHEN_NOT_VISIBLE].includes(parameters.playerOptions.muted)}
226
229
  controls={parameters.playerOptions.controls === EluvioPlayerParameters.controls.DEFAULT}
227
230
  loop={parameters.playerOptions.loop === EluvioPlayerParameters.loop.ON}
231
+ crossOrigin="anonymous"
228
232
  className={PlayerStyles.video}
229
233
  />
230
234
  {
@@ -237,6 +241,12 @@ const PlayerUI = ({target, parameters, InitCallback, ErrorCallback, Unmount, Res
237
241
  <Spinner className={PlayerStyles["spinner"]} />
238
242
  </div>
239
243
  }
244
+ {
245
+ !casting ? null :
246
+ <div className={PlayerStyles["cast-indicator-container"]}>
247
+ <div dangerouslySetInnerHTML={{ __html: ChromecastIcon }} />
248
+ </div>
249
+ }
240
250
  {
241
251
  !errorMessage ? null :
242
252
  <div className={PlayerStyles["error-message"]}>{ errorMessage }</div>
@@ -9,7 +9,14 @@ import {ImageUrl, PlayerClick, Time} from "./Common.js";
9
9
  import EluvioPlayerParameters from "../player/PlayerParameters.js";
10
10
 
11
11
  import EluvioLogo from "../static/images/Logo.png";
12
- import {CollectionMenu, ContentVerificationMenu, SeekBar, SettingsMenu, SVG, VolumeControls} from "./Components.jsx";
12
+ import {
13
+ CollectionMenu,
14
+ ContentVerificationMenu,
15
+ DVRToggle,
16
+ SeekBar,
17
+ SettingsMenu,
18
+ SVG
19
+ } from "./Components.jsx";
13
20
 
14
21
  export const IconButton = ({icon, ...props}) => {
15
22
  return (
@@ -21,7 +28,7 @@ const TimeIndicator = ({player, videoState}) => {
21
28
  const [currentTime, setCurrentTime] = useState(player.video.currentTime);
22
29
 
23
30
  useEffect(() => {
24
- const disposeVideoTimeObserver = ObserveVideoTime({video: player.video, setCurrentTime, rate: 10});
31
+ const disposeVideoTimeObserver = ObserveVideoTime({player, setCurrentTime, rate: 10});
25
32
 
26
33
  return () => disposeVideoTimeObserver && disposeVideoTimeObserver();
27
34
  }, []);
@@ -250,7 +257,7 @@ const TVControls = ({player, playbackStarted, canPlay, recentlyInteracted, setRe
250
257
  useEffect(() => {
251
258
  setPlayerClickHandler(PlayerClick({player, setRecentUserAction}));
252
259
 
253
- const disposeVideoObserver = ObserveVideo({target: player.target, video: player.video, setVideoState});
260
+ const disposeVideoObserver = ObserveVideo({player, setVideoState});
254
261
 
255
262
  return () => disposeVideoObserver && disposeVideoObserver();
256
263
  }, []);
@@ -290,7 +297,7 @@ const TVControls = ({player, playbackStarted, canPlay, recentlyInteracted, setRe
290
297
  // Take focus off of this button because it should no longer be selectable after playback starts
291
298
  player.target.firstChild.focus();
292
299
  }}
293
- className={`${ControlStyles["center-play-button"]} ${ControlStyles["icon-button--drop-shadow-focus"]} ${canPlay && !playbackStarted ? "" : ControlStyles["center-play-button--hidden"]}`}
300
+ className={`${ControlStyles["center-play-button"]} ${ControlStyles["icon-button--drop-shadow-focus"]} ${(canPlay && !playbackStarted && !player.casting) ? "" : ControlStyles["center-play-button--hidden"]}`}
294
301
  />
295
302
  {
296
303
  showInfo ?
@@ -309,13 +316,8 @@ const TVControls = ({player, playbackStarted, canPlay, recentlyInteracted, setRe
309
316
  <div className={ControlStyles["spacer"]}/>
310
317
  {
311
318
  !player.isLive ? null :
312
- player.dvrEnabled ?
313
- <button
314
- onClick={() => player.controls.Seek({time: player.controls.GetDuration() - 2})}
315
- className={`${ControlStyles["live-indicator"]} ${player.isLive && player.behindLiveEdge ? ControlStyles["live-indicator--faded"] : ""}`}
316
- >
317
- Live
318
- </button> :
319
+ player.controls.IsDVRAvailable() ?
320
+ <DVRToggle player={player} /> :
319
321
  <div className={ControlStyles["live-indicator"]}>
320
322
  Live
321
323
  </div>
@@ -343,6 +345,18 @@ const TVControls = ({player, playbackStarted, canPlay, recentlyInteracted, setRe
343
345
  <CenterButtons player={player} videoState={videoState}/>
344
346
  <div className={ControlStyles["bottom-right-controls"]}>
345
347
  <ContentVerificationControls player={player} />
348
+ {
349
+ !player.airplayAvailable ? null :
350
+ <IconButton
351
+ aria-label="Airplay"
352
+ onClick={() => player.video.webkitShowPlaybackTargetPicker()}
353
+ icon={Icons.AirplayIcon}
354
+ />
355
+ }
356
+ {
357
+ !player.chromecastAvailable ? null :
358
+ <google-cast-launcher></google-cast-launcher>
359
+ }
346
360
  {
347
361
  !player.controls.GetOptions().hasAnyOptions ? null :
348
362
  <MenuButton