@playkit-js/rapt 0.4.10-canary.0-878cdb7

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.
@@ -0,0 +1,866 @@
1
+ import { Dispatcher } from "./helpers/Dispatcher";
2
+ import { KipEvent } from "./helpers/KipEvents";
3
+ import { KalturaPlayer, PlayersFactory } from "./PlayersFactory";
4
+ import { PlayersBufferManager } from "./PlayersBufferManager";
5
+ import { PlayersDomManager } from "./PlayersDomManager";
6
+ import { log } from "./helpers/logger";
7
+
8
+ declare var Rapt: any;
9
+
10
+ export const KipFullscreen = {
11
+ FULL_SCREEN_CLICKED: "fullScreenClicked",
12
+ ENTER_FULL_SCREEN: "enterFullScreen",
13
+ EXIT_FULL_SCREEN: "exitFullScreen",
14
+ };
15
+
16
+ export interface RaptNode {
17
+ onEnded?: [];
18
+ id: string;
19
+ entryId: string;
20
+ name: string;
21
+ customData?: any;
22
+ prefetchNodeIds?: string[];
23
+ startFrom?: number;
24
+ }
25
+
26
+ export enum Persistency {
27
+ mute = "mute",
28
+ textStyle = "textStyle",
29
+ captions = "captions",
30
+ volume = "volume",
31
+ audioTrack = "audioTrack",
32
+ rate = "rate",
33
+ }
34
+
35
+ /**
36
+ * This class manages players, and places and interact with the Rapt engine layer
37
+ * This class creates and manages BufferManager
38
+ */
39
+ export class PlayersManager extends Dispatcher {
40
+ private playersBufferManager: PlayersBufferManager;
41
+ private playersFactory: PlayersFactory;
42
+ private activePlayer: KalturaPlayer = null;
43
+ private clickedHotspotId: string = undefined;
44
+ private activeNode: RaptNode = null;
45
+ static playerTickInterval: number = 250;
46
+ public raptEngine: any;
47
+ private analyticsModel: any = undefined;
48
+ private isAvailable: boolean;
49
+ private firstPlay: boolean = true;
50
+ private playerWidth: number = NaN;
51
+ private playerHeight: number = NaN;
52
+ private resizeInterval: number = NaN;
53
+ private endFlag: boolean = false;
54
+
55
+ constructor(
56
+ private config: any,
57
+ private playerLibrary: any,
58
+ readonly raptProjectId: string,
59
+ private raptData: any,
60
+ private domManager: PlayersDomManager,
61
+ private impressionAnalyticEventSent: boolean = false,
62
+ private playRequestedAnalyticEventSent: boolean = false,
63
+ private playAnalyticEventSent: boolean = false
64
+ ) {
65
+ super();
66
+ this.isAvailable =
67
+ this.initPlayersFactory() && this.initPlayersBufferManager();
68
+
69
+ if (this.isAvailable) {
70
+ setTimeout(() => {
71
+ this.isAvailable = this.initRaptEngine();
72
+ // responsiveness resize support
73
+ this.resizeInterval = window.setInterval(
74
+ this.handleWindowResized.bind(this),
75
+ 250
76
+ );
77
+ });
78
+ }
79
+ }
80
+
81
+ private initPlayersBufferManager(): boolean {
82
+ // create the PlayersBufferManager
83
+ this.playersBufferManager = new PlayersBufferManager(
84
+ this.raptData,
85
+ this.playersFactory
86
+ );
87
+ if (
88
+ (this.config.rapt &&
89
+ this.config.rapt.hasOwnProperty("bufferNextNodes") &&
90
+ this.config.rapt.bufferNextNodes === false) ||
91
+ this.config.rapt.syncVideos
92
+ ) {
93
+ this.playersBufferManager.disable();
94
+ } else {
95
+ const bufferingEvents = [
96
+ "buffer:prebuffer",
97
+ "buffer:bufferend",
98
+ "buffer:bufferstart",
99
+ "buffer:allbuffered",
100
+ ];
101
+ for (let bufferEvent of bufferingEvents) {
102
+ this.playersBufferManager.addListener(bufferEvent, (event) => {
103
+ this.dispatch(event);
104
+ });
105
+ }
106
+ }
107
+ return true;
108
+ }
109
+
110
+ public getActiveKalturaPlayer(): any {
111
+ if (this.activePlayer) {
112
+ return this.activePlayer.player;
113
+ }
114
+ return null;
115
+ }
116
+
117
+ public getActiveNode(): RaptNode {
118
+ return this.activeNode;
119
+ }
120
+
121
+ private initPlayersFactory(): boolean {
122
+ /**
123
+ * This function interrupts the player kava beacons, alter some attributes (in case of need) and prevents some
124
+ * of the events.
125
+ * @param model
126
+ */
127
+ const analyticsInterruptFunc = (model: any): boolean => {
128
+ if (!this.analyticsModel) {
129
+ this.analyticsModel = model; // store model data for future use of Rapt sending data
130
+ }
131
+ model.rootEntryId = this.raptProjectId;
132
+ model.nodeId = this.activeNode.id;
133
+ model.entryId = this.activeNode.entryId;
134
+ switch (model.eventType) {
135
+ case 14:
136
+ model.eventType = 99; // on event 14 (video-end) convert to event 99. ON RAPT CONTEXT ONLY!
137
+ break;
138
+ case 11:
139
+ case 12:
140
+ case 13:
141
+ return false; // don't send quartiles events
142
+ case 1:
143
+ // send this once
144
+ if (this.impressionAnalyticEventSent) {
145
+ return false;
146
+ } else {
147
+ this.impressionAnalyticEventSent = true;
148
+ model.entryId = this.raptProjectId;
149
+ break;
150
+ }
151
+ case 2:
152
+ // send this once
153
+ if (this.playRequestedAnalyticEventSent) {
154
+ return false;
155
+ } else {
156
+ this.playRequestedAnalyticEventSent = true;
157
+ model.entryId = this.raptProjectId;
158
+ break;
159
+ }
160
+ case 3:
161
+ // send this once
162
+ if (this.playAnalyticEventSent) {
163
+ return false;
164
+ } else {
165
+ this.playAnalyticEventSent = true;
166
+ model.entryId = this.raptProjectId;
167
+ break;
168
+ }
169
+ }
170
+ return true;
171
+ };
172
+
173
+ // create a PlayersFactory instance
174
+ this.playersFactory = new PlayersFactory(
175
+ this.domManager,
176
+ this.raptProjectId,
177
+ this.playerLibrary,
178
+ analyticsInterruptFunc,
179
+ this.config
180
+ );
181
+ // listen to fullscreen events from the players
182
+ this.playersFactory.addListener(KipFullscreen.FULL_SCREEN_CLICKED, () => {
183
+ this.toggleFullscreenState();
184
+ });
185
+
186
+ return true;
187
+ }
188
+
189
+ private initRaptEngine(): boolean {
190
+ const { id: raptEngineId, domElement } = this.domManager.createDomElement(
191
+ "div",
192
+ "rapt-engine",
193
+ "kiv-rapt-engine"
194
+ );
195
+ // create the rapt-engine layer. We must use this.element because of rapt delegate names
196
+ this.element = domElement;
197
+ this.element.setAttribute("tabindex", "-1");
198
+ const { nodes, settings } = this.raptData;
199
+ const startNodeId = settings.startNodeId;
200
+ // retrieve the 1st node
201
+ const firstNode: RaptNode = nodes.find(function (element: any) {
202
+ return element.id === startNodeId;
203
+ });
204
+
205
+ if (!firstNode) {
206
+ this.dispatch({ type: KipEvent.FIRST_PLAY_ERROR });
207
+ return false;
208
+ }
209
+
210
+ // apply google-analytics track id if exist
211
+ const options: any = {};
212
+ if (this.config.gaTrackId) {
213
+ options.$ga = this.config.gaTrackId;
214
+ }
215
+ this.raptEngine = new Rapt.Engine(this, options);
216
+ this.raptEngine.load(this.raptData);
217
+
218
+ this.resizeEngine();
219
+
220
+ setInterval(() => this.syncRaptStatus(), PlayersManager.playerTickInterval);
221
+ return true;
222
+ }
223
+
224
+ private handleWindowResized() {
225
+ // onResize is a window event - make sure we run this only when we change the player div
226
+ const currentWidth = this.domManager.getContainer().offsetWidth;
227
+ const currentHeight = this.domManager.getContainer().offsetHeight;
228
+ if (
229
+ currentWidth !== this.playerWidth ||
230
+ currentHeight !== this.playerHeight
231
+ ) {
232
+ this.playerWidth = currentWidth;
233
+ this.playerHeight = currentHeight;
234
+ this.resizeEngine();
235
+ }
236
+ }
237
+
238
+ private syncRaptStatus() {
239
+ if (!this.activePlayer) {
240
+ return;
241
+ }
242
+ const currentPlayingVideoElement: any = this.activePlayer.player.getVideoElement();
243
+ if (currentPlayingVideoElement) {
244
+ this.raptEngine.update({
245
+ currentTime: currentPlayingVideoElement.currentTime,
246
+ duration: currentPlayingVideoElement.duration,
247
+ ended: currentPlayingVideoElement.ended,
248
+ videoWidth: currentPlayingVideoElement.videoWidth,
249
+ videoHeight: currentPlayingVideoElement.videoHeight,
250
+ paused: currentPlayingVideoElement.paused,
251
+ readyState: currentPlayingVideoElement.readyState,
252
+ });
253
+ }
254
+ }
255
+
256
+ private toggleFullscreenState() {
257
+ const doc: any = document;
258
+ if (doc.fullscreenElement || doc.webkitFullscreenElement) {
259
+ if (document.exitFullscreen) {
260
+ doc.exitFullscreen();
261
+ } else if (doc.webkitExitFullscreen) {
262
+ doc.webkitExitFullscreen();
263
+ }
264
+ } else {
265
+ this.domManager.requestFullscreen();
266
+ }
267
+ }
268
+
269
+ // re-render the rapt engine according to current dimension of the main container
270
+ private resizeEngine() {
271
+ const raptContainer = this.domManager.getContainer();
272
+ this.raptEngine.resize({
273
+ width: raptContainer.offsetWidth,
274
+ height: raptContainer.offsetHeight,
275
+ });
276
+ }
277
+
278
+ public destroy() {
279
+ this.removeListeners();
280
+ clearInterval(this.resizeInterval);
281
+ }
282
+
283
+ ////////////////////////////////////////////
284
+ // store the player and the node
285
+ private updateActiveItems(
286
+ nextPlayer: KalturaPlayer,
287
+ nextNode: RaptNode
288
+ ): void {
289
+ const hasActivePlayer = !!this.activePlayer;
290
+ const isSwitchingPlayer = this.activePlayer !== nextPlayer;
291
+ const isSwitchingRaptNode = this.activeNode !== nextNode;
292
+ log("log", "pm_updateActiveItems", "executed", {
293
+ hasActivePlayer,
294
+ isSwitchingPlayer,
295
+ isSwitchingRaptNode,
296
+ nodeId: nextNode ? nextNode.id : null,
297
+ });
298
+
299
+ if (hasActivePlayer) {
300
+ this.activePlayer.player.pause();
301
+ if (isSwitchingPlayer) {
302
+ // remove listeners
303
+ this.removeListeners();
304
+ if (!this.playersBufferManager.isAvailable()) {
305
+ // TODO 1 must destroy active player
306
+ }
307
+ }
308
+ }
309
+ this.activePlayer = nextPlayer;
310
+ this.activeNode = nextNode;
311
+ if (isSwitchingRaptNode) {
312
+ this.playersBufferManager.switchPlayer(this.activeNode);
313
+ }
314
+ if (isSwitchingPlayer) {
315
+ this.domManager.changeActivePlayer(this.activePlayer);
316
+ // register listeners
317
+ this.addListenersToPlayer();
318
+ }
319
+ }
320
+
321
+ /**
322
+ * This will send the first nodePlay event only if it has analytics model and after the 1st play
323
+ * If it doesn't have a model it will try again later.
324
+ */
325
+ private trySendingFirstEvent(entryId: string) {
326
+ setTimeout(() => {
327
+ if (this.analyticsModel && this.playAnalyticEventSent) {
328
+ this.sendAnalytics(44, { entryId: entryId });
329
+ } else {
330
+ this.trySendingFirstEvent(entryId);
331
+ }
332
+ }, 250);
333
+ }
334
+
335
+ ////////////////////////////////////////////
336
+
337
+ private projectStarted() {
338
+ // project had started - unhide the rapt layer now and hide the large-play-button layer
339
+ this.domManager.showRaptLayer();
340
+ this.domManager.addClass("kiv-initiated");
341
+ this.removeListener("project:start", () => this.projectStarted);
342
+ // make sure we are setting the current player to autoplay. For both non-buffered path and in case we reuse this player as a buffered player
343
+ try {
344
+ this.activePlayer.player.configure({ playback: { autoplay: true } });
345
+ } catch (e) {
346
+ log("log", "pm_projectStarted", "Could not apply autoplay to player", e);
347
+ }
348
+ }
349
+
350
+ // called by Rapt on first-node, user click, defaultPath and external API "jump"
351
+ private switchPlayer(media: any): void {
352
+ let currentPlayerPosition = 0;
353
+ // check if there is a need to sync videos, that there is a player and that we are not
354
+ // switching at the end of the video (might cause an endless loop)
355
+ if (
356
+ this.config.rapt.syncVideos &&
357
+ this.activePlayer &&
358
+ this.activePlayer.player &&
359
+ this.activePlayer.player.currentTime &&
360
+ this.activePlayer.player.duration - this.activePlayer.player.currentTime >
361
+ 5
362
+ ) {
363
+ currentPlayerPosition = this.activePlayer.player.currentTime;
364
+ }
365
+ this.domManager.getContainer().classList.add("rapt-switching");
366
+ setTimeout(() => {
367
+ this.domManager.getContainer().classList.remove("rapt-switching");
368
+ }, 269);
369
+ const newEntryId = media.sources[0].src;
370
+ const nextRaptNode: RaptNode = media.node;
371
+ const raptLayer: HTMLElement = document.querySelector(" .kiv-rapt-engine");
372
+
373
+ // send analytics of nodePlay - event44
374
+ if (this.analyticsModel) {
375
+ this.sendAnalytics(44, { entryId: newEntryId, nodeId: nextRaptNode.id });
376
+ } else {
377
+ // At this point we do not have the 1st player nor analytics model. This will make sure to send the 44 event when
378
+ // we have a model.
379
+ this.trySendingFirstEvent(newEntryId);
380
+ }
381
+ let params: any = {};
382
+ if (this.activeNode) {
383
+ params = {
384
+ entryId: this.activeNode.entryId,
385
+ fromNodeId: this.activeNode.id,
386
+ toNodeId: nextRaptNode.id,
387
+ };
388
+ if (this.clickedHotspotId) {
389
+ params.hotspotId = this.clickedHotspotId;
390
+ // clear saved hotspotId
391
+ this.clickedHotspotId = undefined;
392
+ }
393
+ if (this.activeNode.id) {
394
+ params.nodeId = this.activeNode.id;
395
+ }
396
+ // node play = event 48!
397
+ this.sendAnalytics(48, params);
398
+ }
399
+
400
+ log("log", "pm_switchPlayer", "executed", {
401
+ entryId: newEntryId,
402
+ nodeId: nextRaptNode.id,
403
+ });
404
+ if (this.activePlayer && this.activeNode === nextRaptNode) {
405
+ log(
406
+ "log",
407
+ "pm_switchPlayer",
408
+ "switch to same node, seek to the beginning",
409
+ { entryId: newEntryId }
410
+ );
411
+ // node is "switched" to itself
412
+ const hotspotId = params.hotspotId;
413
+ const clickedHotspot = this.raptData.hotspots.find(
414
+ (item) => item.id === hotspotId
415
+ );
416
+ let startFrom = 0;
417
+
418
+ if (clickedHotspot) {
419
+ // this is a hotspot click - find if the hotspot project:jump has startFrom
420
+ let hotspotsJumpData = clickedHotspot.onClick.find(
421
+ (item) => item.type === "project:jump"
422
+ ).payload;
423
+ if (hotspotsJumpData && hotspotsJumpData.startFrom) {
424
+ startFrom = hotspotsJumpData.startFrom;
425
+ }
426
+ } else {
427
+ // this is a defaultPath - find if the defaultPath project:jump has startFrom
428
+ if (this.activeNode.onEnded) {
429
+ const onEnded: any = this.activeNode.onEnded.find(
430
+ (item: any) => item.type === "project:jump"
431
+ );
432
+ if (onEnded && onEnded.payload && onEnded.payload.startFrom) {
433
+ startFrom = onEnded.payload.startFrom;
434
+ }
435
+ }
436
+ }
437
+ this.activePlayer.player.currentTime = startFrom;
438
+ this.activePlayer.player.play();
439
+ return;
440
+ }
441
+
442
+ // autoplay false detection
443
+ let shouldPlayNow = true;
444
+ if (
445
+ this.firstPlay &&
446
+ this.config.playback &&
447
+ this.config.playback.autoplay === false
448
+ ) {
449
+ shouldPlayNow = false;
450
+ log(
451
+ "log",
452
+ "pm_switchPlayer",
453
+ "setting autoplay to false on first node with playersBufferManager",
454
+ {
455
+ entryId: newEntryId,
456
+ }
457
+ );
458
+ }
459
+ if (!shouldPlayNow && this.firstPlay) {
460
+ // autoplay = false. Hide the rapt layer until the project is started
461
+ this.domManager.hideRaptLayer();
462
+ this.addListener("project:start", () => this.projectStarted());
463
+ }
464
+
465
+ if (this.playersBufferManager.isAvailable()) {
466
+ log(
467
+ "log",
468
+ "pm_switchPlayer",
469
+ "use buffer manager to get player for entry",
470
+ { entryId: newEntryId }
471
+ );
472
+ const seekTo = this.checkSeekTo(nextRaptNode, this.activeNode, params.hotspotId);
473
+ const bufferedPlayer = this.playersBufferManager.getPlayer(
474
+ newEntryId,
475
+ shouldPlayNow,
476
+ this.firstPlay && !shouldPlayNow,
477
+ seekTo
478
+ );
479
+ this.firstPlay = false;
480
+ this.updateActiveItems(bufferedPlayer, nextRaptNode);
481
+ } else {
482
+ log(
483
+ "log",
484
+ "pm_switchPlayer",
485
+ "buffer manager not available, switch media on current player"
486
+ );
487
+
488
+ if (!this.activePlayer) {
489
+ log("log", "pm_switchPlayer", "no player found, create main player", {
490
+ entryId: newEntryId,
491
+ });
492
+
493
+ const newPlayer = this.playersFactory.createPlayer(
494
+ newEntryId,
495
+ shouldPlayNow,
496
+ null,
497
+ this.firstPlay && !shouldPlayNow
498
+ );
499
+ this.firstPlay = false;
500
+ this.updateActiveItems(newPlayer, nextRaptNode);
501
+ } else {
502
+ log("log", "pm_switchPlayer", "switch media on main player", {
503
+ entryId: newEntryId,
504
+ });
505
+ // Even if autoplay was set to false, any next video must be played automaticaly
506
+ if (this.firstPlay && this.config.playback.autoplay === false) {
507
+ this.activePlayer.player.configure({ playback: { autoplay: true } });
508
+ // this is a >= 2nd entry - from here on we do not need the poster for nxt entries
509
+ this.activePlayer.player.configure({ sources: { poster: "" } });
510
+ log(
511
+ "log",
512
+ "pm_switchPlayer",
513
+ "setting autoplay to true (after first load) and poster to off"
514
+ );
515
+ }
516
+ this.firstPlay = false;
517
+ // grab seekto before updateActiveItems
518
+ const seekTo = this.checkSeekTo(nextRaptNode, this.activeNode, params.hotspotId);
519
+ this.updateActiveItems(this.activePlayer, nextRaptNode);
520
+ if (this.config.rapt.syncVideos && currentPlayerPosition) {
521
+ this.activePlayer.player.configure({
522
+ playback: { startTime: currentPlayerPosition },
523
+ });
524
+ }
525
+ // handle seekTo logic
526
+ if (seekTo) {
527
+ this.activePlayer.player.configure({
528
+ playback: { startTime: seekTo },
529
+ });
530
+ } else {
531
+ this.activePlayer.player.configure({ playback: { startTime: 0 } });
532
+ }
533
+
534
+ this.activePlayer.player.loadMedia({
535
+ entryId: newEntryId,
536
+ });
537
+ }
538
+ }
539
+ // reparenting rapt layer on mobile.
540
+ if (this.playersFactory.playerLibrary.core.Env.device.model) {
541
+ this.domManager.reparentRaptLayer(raptLayer);
542
+ }
543
+ }
544
+
545
+ public execute(command: any) {
546
+ if (!this.raptEngine) {
547
+ log(
548
+ "error",
549
+ "pm_execute",
550
+ "Error: Rapt Media commands received before initialization is complete"
551
+ );
552
+ return;
553
+ }
554
+ this.raptEngine.execute(command);
555
+ }
556
+
557
+ private handleTextTrackChanged = (event: any) => {
558
+ this.playersBufferManager.syncPlayersStatus(
559
+ Persistency.captions,
560
+ event.payload.selectedTextTrack._language,
561
+ this.activePlayer.player
562
+ );
563
+ };
564
+
565
+ private handleRateChanged = (event: any) => {
566
+ this.playersBufferManager.syncPlayersStatus(
567
+ Persistency.rate,
568
+ this.activePlayer.player.playbackRate,
569
+ this.activePlayer.player
570
+ );
571
+ };
572
+
573
+ private handleTrackStyleChanged = (event: any) => {
574
+ this.playersBufferManager.syncPlayersStatus(
575
+ Persistency.textStyle,
576
+ this.activePlayer.player.textStyle,
577
+ this.activePlayer.player
578
+ );
579
+ };
580
+
581
+ private handleAudiotrackChanged = (event: any) => {
582
+ this.playersBufferManager.syncPlayersStatus(
583
+ Persistency.audioTrack,
584
+ event.payload.selectedAudioTrack._language,
585
+ this.activePlayer.player
586
+ );
587
+ };
588
+
589
+ private handleVolumeChanged = (event: any) => {
590
+ this.playersBufferManager.syncPlayersStatus(
591
+ Persistency.volume,
592
+ this.activePlayer.player.volume,
593
+ this.activePlayer.player
594
+ );
595
+ };
596
+
597
+ private handleMuteChanged = (event: any) => {
598
+ const isMuted = event.payload.mute;
599
+ this.playersBufferManager.syncPlayersStatus(
600
+ Persistency.mute,
601
+ isMuted,
602
+ this.activePlayer.player
603
+ );
604
+ };
605
+
606
+ removeListeners() {
607
+ if (this.activePlayer && this.activePlayer.player) {
608
+ const player: any = this.activePlayer.player;
609
+ player.removeEventListener(
610
+ player.Event.Core.TEXT_TRACK_CHANGED,
611
+ this.handleTextTrackChanged
612
+ );
613
+ player.removeEventListener(
614
+ player.Event.Core.AUDIO_TRACK_CHANGED,
615
+ this.handleAudiotrackChanged
616
+ );
617
+ player.removeEventListener(
618
+ player.Event.Core.TEXT_STYLE_CHANGED,
619
+ this.handleTrackStyleChanged
620
+ );
621
+ player.removeEventListener(
622
+ player.Event.Core.RATE_CHANGE,
623
+ this.handleRateChanged
624
+ );
625
+ player.removeEventListener(
626
+ player.Event.Core.VOLUME_CHANGE,
627
+ this.handleVolumeChanged
628
+ );
629
+ player.removeEventListener(
630
+ player.Event.Core.MUTE_CHANGE,
631
+ this.handleMuteChanged
632
+ );
633
+ }
634
+ }
635
+
636
+ // this function checks if the current node that points to the next node is expected to have a seekto
637
+ checkSeekTo(nextNode: RaptNode, currentNode: RaptNode, clickedHotspotId: string) {
638
+ try {
639
+ if (!currentNode) {
640
+ return;
641
+ }
642
+ const onEnded = currentNode.onEnded;
643
+ if (onEnded && this.endFlag) {
644
+ const defaultPathWithSeekTo: any = onEnded.find((item: any) => {
645
+ if (
646
+ item.type === "project:jump" &&
647
+ item.payload &&
648
+ item.payload.startFrom
649
+ ) {
650
+ return true;
651
+ }
652
+ return false;
653
+ });
654
+ if (
655
+ defaultPathWithSeekTo &&
656
+ nextNode.id === defaultPathWithSeekTo.payload.destination
657
+ ) {
658
+ return defaultPathWithSeekTo.payload.startFrom;
659
+ }
660
+ }
661
+ // we got here - this is not a defaultPath - we need to find the relevant hotspot and see if it has startFrom
662
+ if (!clickedHotspotId) {
663
+ log(
664
+ "error",
665
+ "pm_checkSeekTo",
666
+ `Error: hotspot with id: ${clickedHotspotId} is undefined. returning startFrom=0`
667
+ );
668
+ return 0;
669
+ }
670
+
671
+ const clickedHotspot = this.raptData.hotspots.find(
672
+ (hs) => hs.id === clickedHotspotId
673
+ );
674
+
675
+ if (clickedHotspot && clickedHotspot.onClick) {
676
+ let hotspotsJumpData = clickedHotspot.onClick.find(
677
+ (item) => item.type === "project:jump"
678
+ ).payload;
679
+ if (hotspotsJumpData &&
680
+ hotspotsJumpData.destination &&
681
+ hotspotsJumpData.destination === nextNode.id &&
682
+ typeof hotspotsJumpData.startFrom === 'number') {
683
+ return hotspotsJumpData.startFrom;
684
+ } else {
685
+ log(
686
+ "error",
687
+ "pm_checkSeekTo",
688
+ `Error: hotspotsJumpData of id ${clickedHotspotId} did not satisfy one or more of the conditions. returning startFrom=0`
689
+ );
690
+ }
691
+ } else {
692
+ log(
693
+ "error",
694
+ "pm_checkSeekTo",
695
+ `Error: Could not find data of hotspot with id: ${clickedHotspotId}. returning startFrom=0`
696
+ );
697
+ }
698
+ return 0;
699
+ } catch (e) {
700
+ return 0;
701
+ }
702
+ }
703
+
704
+ addListenersToPlayer() {
705
+ if (this.activePlayer && this.activePlayer.player) {
706
+ const player: any = this.activePlayer.player;
707
+ player.addEventListener(player.Event.Core.PLAY, () => {
708
+ const tracks = player.getTracks();
709
+ const audioTracks = tracks.find(
710
+ (track) => track._kind !== "subtitles" && !track._bandwidth
711
+ );
712
+ const captionsTracks = tracks.find(
713
+ (track) => track._kind === "subtitles"
714
+ );
715
+ if (
716
+ captionsTracks ||
717
+ (audioTracks &&
718
+ audioTracks._language &&
719
+ audioTracks._language != undefined)
720
+ ) {
721
+ // this video has captions or audio
722
+ const view = player.getView().parentElement.parentElement
723
+ .parentElement;
724
+ view.classList.add("has-extra-tracks");
725
+ }
726
+ });
727
+ player.addEventListener(
728
+ player.Event.Core.TEXT_TRACK_CHANGED,
729
+ this.handleTextTrackChanged
730
+ );
731
+
732
+ player.addEventListener(
733
+ player.Event.Core.AUDIO_TRACK_CHANGED,
734
+ this.handleAudiotrackChanged
735
+ );
736
+
737
+ player.addEventListener(
738
+ player.Event.Core.TEXT_STYLE_CHANGED,
739
+ this.handleTrackStyleChanged
740
+ );
741
+
742
+ player.addEventListener(
743
+ player.Event.Core.RATE_CHANGE,
744
+ this.handleRateChanged
745
+ );
746
+
747
+ player.addEventListener(
748
+ player.Event.Core.VOLUME_CHANGE,
749
+ this.handleVolumeChanged
750
+ );
751
+ player.addEventListener(
752
+ player.Event.Core.MUTE_CHANGE,
753
+ this.handleMuteChanged
754
+ );
755
+ }
756
+ }
757
+
758
+ // Rapt interface - don't change signature //
759
+ private element: HTMLElement; // must be called 'element' because rapt delegate implementation
760
+
761
+ public pause() {
762
+ if (this.activePlayer && this.activePlayer.player) {
763
+ this.activePlayer.player.pause();
764
+ }
765
+ }
766
+
767
+ public play() {
768
+ if (this.activePlayer && this.activePlayer.player) {
769
+ this.activePlayer.player.play();
770
+ }
771
+ }
772
+
773
+ public seek(n) {
774
+ if (this.activePlayer && this.activePlayer.player) {
775
+ this.activePlayer.player.currentTime = n;
776
+ }
777
+ }
778
+
779
+ load(media: any) {
780
+ this.switchPlayer(media);
781
+ }
782
+
783
+ // Rapt interface - don't change signature //
784
+ event(event: any) {
785
+ if (event.type === "node:ended") {
786
+ this.endFlag = true;
787
+ }
788
+ if (event.type === "node:enter") {
789
+ this.endFlag = false;
790
+ }
791
+
792
+ if (event.type === "hotspot:click") {
793
+ // save the hotspot id so we can send it to analytics
794
+ this.clickedHotspotId = event.payload.hotspot.id;
795
+ }
796
+ if (event.type === "browser:open") {
797
+ // track hotspot click
798
+ const additionalData = {
799
+ entryId: this.activeNode.entryId,
800
+ nodeId: this.activeNode.id,
801
+ target: event.payload.href,
802
+ hotspotId: event.context.payload.hotspot.id,
803
+ };
804
+ this.sendAnalytics(47, additionalData);
805
+ }
806
+ if (event.type === "project:ready") {
807
+ this.raptEngine.metadata.account = this.config.partnetId;
808
+ }
809
+ this.dispatch(event);
810
+
811
+ // handle seeks
812
+ if (event.type === "hotspot:click") {
813
+ if (this.raptData.hotspots && this.raptData.hotspots.length) {
814
+ const hs = this.raptData.hotspots.find(
815
+ (item) => item.id === event.payload.hotspot.id
816
+ );
817
+ if (hs.clickSeek) {
818
+ this.seek(hs.clickSeek);
819
+ }
820
+ }
821
+ }
822
+ }
823
+
824
+ /**
825
+ * The function sends an anlytics beacon.
826
+ * @param eventNumber
827
+ * @param attributes - an object of attributes to send on top of the event (will override existing attr
828
+ * @param fieldsToRemove - array of strings that are not required on the event
829
+ */
830
+ sendAnalytics(
831
+ eventNumber: number,
832
+ attributes?: object,
833
+ fieldsToRemove?: string[]
834
+ ) {
835
+ if (!this.analyticsModel) {
836
+ log(
837
+ "error",
838
+ "pm_sendAnalytics",
839
+ "Missing basic event model - cannot send analytics"
840
+ );
841
+ return;
842
+ }
843
+ let tmpModel = Object.assign({}, this.analyticsModel);
844
+ tmpModel.eventType = eventNumber;
845
+
846
+ if (attributes) {
847
+ Object.keys(attributes).forEach((item) => {
848
+ tmpModel[item] = attributes[item];
849
+ });
850
+ }
851
+
852
+ if (fieldsToRemove) {
853
+ fieldsToRemove.forEach((field) => {
854
+ delete tmpModel[field];
855
+ });
856
+ }
857
+ if (
858
+ this.activePlayer &&
859
+ this.activePlayer.player &&
860
+ this.activePlayer.player.currentTime
861
+ ) {
862
+ tmpModel.position = this.activePlayer.player.currentTime;
863
+ }
864
+ this.activePlayer.player.plugins.kava.sendAnalytics(tmpModel);
865
+ }
866
+ }