@eluvio/elv-player-js 2.0.21 → 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,8 +303,12 @@ export class EluvioPlayer {
294
303
 
295
304
  this.__Reset();
296
305
 
306
+
307
+
308
+ this.initialized = false;
297
309
  this.loading = true;
298
310
  this.initTime = Date.now();
311
+ this.restartParameters = restartParameters;
299
312
 
300
313
  this.__SettingsUpdate();
301
314
 
@@ -329,6 +342,24 @@ export class EluvioPlayer {
329
342
  }
330
343
  }
331
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
+
332
363
  this.__RegisterVideoEventListener("play", () => {
333
364
  this.reloads = 0;
334
365
  this.playbackStarted = true;
@@ -348,7 +379,7 @@ export class EluvioPlayer {
348
379
  });
349
380
 
350
381
  this.__RegisterVideoEventListener("seeking", () => {
351
- if(!this.isLive || !this.dvrEnabled) { return; }
382
+ if(!this.isLive || !this.dvrAvailable) { return; }
352
383
  const diff = this.video.duration - this.video.currentTime;
353
384
  this.behindLiveEdge = diff > 15;
354
385
  this.__SettingsUpdate();
@@ -361,7 +392,7 @@ export class EluvioPlayer {
361
392
  this.__RegisterVideoEventListener("ended", () => this.controls && this.controls.CollectionPlayNext({autoplay: true}));
362
393
  }
363
394
 
364
- let { versionHash, playoutUrl, protocol, drm, drms, multiviewOptions } = await this.__PlayoutOptions();
395
+ let { versionHash, playoutUrl, protocol, drm, drms, multiviewOptions, playoutParameters } = await this.__PlayoutOptions();
365
396
 
366
397
  this.contentHash = versionHash;
367
398
 
@@ -374,6 +405,13 @@ export class EluvioPlayer {
374
405
 
375
406
  this.authorizationToken = authorizationToken;
376
407
 
408
+ this.playoutUrl = playoutUrl.toString();
409
+
410
+ this.castHandler.SetMedia({
411
+ playoutOptions: this.sourceOptions.playoutOptions,
412
+ playoutParameters
413
+ });
414
+
377
415
  if(this.__destroyed) { return; }
378
416
 
379
417
  if(protocol === "hls") {
@@ -422,6 +460,9 @@ export class EluvioPlayer {
422
460
  // If Destroy was called during the initialization process, ensure that the player is properly destroyed
423
461
  this.__DestroyPlayer();
424
462
  }
463
+
464
+ this.initialized = true;
465
+ this.restartParameters = undefined;
425
466
  } catch (error) {
426
467
  // If playout failed due to a permission issue, check the content to see if there is a message to display
427
468
  let permissionErrorMessage;
@@ -460,7 +501,7 @@ export class EluvioPlayer {
460
501
  } else if(error.status >= 500) {
461
502
  this.__HardReload(error, 10000);
462
503
  } else {
463
- this.SetErrorMessage(error.displayMessage || "Something went wrong");
504
+ this.__HardReload(error, 20000);
464
505
  }
465
506
 
466
507
  if(this.playerOptions.errorCallback) {
@@ -475,7 +516,8 @@ export class EluvioPlayer {
475
516
  async __InitializeHLS({playoutUrl, authorizationToken, drm, drms, multiviewOptions}) {
476
517
  this.HLS = (await import("hls.js")).default;
477
518
 
478
- 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()) {
479
521
  // HLS JS NOT SUPPORTED - Handle native player
480
522
  this.nativeHLS = true;
481
523
 
@@ -545,7 +587,7 @@ export class EluvioPlayer {
545
587
  ...customProfileSettings
546
588
  };
547
589
 
548
- if(this.dvrEnabled) {
590
+ if(this.dvrAvailable) {
549
591
  delete this.hlsOptions.liveMaxLatencyDuration;
550
592
  delete this.hlsOptions.liveMaxLatencyDurationCount;
551
593
  }
@@ -665,6 +707,14 @@ export class EluvioPlayer {
665
707
 
666
708
  this.hlsPlayer = hlsPlayer;
667
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
+ }
668
718
  }
669
719
  }
670
720
 
@@ -757,12 +807,9 @@ export class EluvioPlayer {
757
807
  ]
758
808
  .map(event => dashPlayer.on(event, () => this.__SettingsUpdate()));
759
809
 
760
- // Subtitle track change
761
- dashPlayer.on(this.Dash.MediaPlayer.events.METRIC_CHANGED, event => {
762
- if(!event || event.mediaType !== "text") { return; }
763
-
764
- this.__SettingsUpdate();
765
- });
810
+ this.video.textTracks.addEventListener("change", () =>
811
+ this.__SettingsUpdate()
812
+ );
766
813
 
767
814
  this.player = dashPlayer;
768
815
  this.dashPlayer = dashPlayer;
@@ -979,6 +1026,7 @@ export class EluvioPlayer {
979
1026
  }
980
1027
 
981
1028
  __DestroyPlayer() {
1029
+ this.castHandler.Disconnect();
982
1030
  this.__destroyed = true;
983
1031
  this.__Reset();
984
1032
  }
@@ -1012,6 +1060,7 @@ export class EluvioPlayer {
1012
1060
  }
1013
1061
  });
1014
1062
 
1063
+ this.__settingsListeners = [];
1015
1064
  this.__listenerDisposers = [];
1016
1065
  this.__showPlayerProfileForm = false;
1017
1066
 
@@ -1033,8 +1082,12 @@ export class EluvioPlayer {
1033
1082
  this.canPlay = false;
1034
1083
  this.isLive = false;
1035
1084
  this.behindLiveEdge = false;
1085
+ this.dvrAvailable = false;
1036
1086
  this.dvrEnabled = false;
1037
1087
  this.publicMetadataUrl = undefined;
1088
+ this.airplayAvailable = false;
1089
+ this.chromecastAvailable = false;
1090
+ this.casting = false;
1038
1091
  }
1039
1092
 
1040
1093
  async __HardReload(error, delay=6000) {
@@ -1087,7 +1140,7 @@ export class EluvioPlayer {
1087
1140
  this.SetErrorMessage(undefined);
1088
1141
  this.__Initialize(
1089
1142
  this.originalParameters,
1090
- !this.video ? null :
1143
+ !this.video || !this.initialized ? this.restartParameters :
1091
1144
  {
1092
1145
  muted: this.video.muted,
1093
1146
  volume: this.video.volume,
@@ -1111,6 +1164,17 @@ export class EluvioPlayer {
1111
1164
  }
1112
1165
  }
1113
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
+ }
1114
1178
  }
1115
1179
 
1116
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>