@vouchfor/embeds 0.0.0-experiment.f97b5b2 → 0.0.0-experiment.fcfd44c

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. package/dist/es/embeds.js +1015 -1384
  2. package/dist/es/embeds.js.map +1 -1
  3. package/dist/es/src/components/DialogEmbed/DialogOverlay.d.ts +20 -0
  4. package/dist/es/src/components/DialogEmbed/DialogPortal.d.ts +36 -0
  5. package/dist/es/src/components/DialogEmbed/index.d.ts +38 -0
  6. package/dist/es/src/components/PlayerEmbed/controllers/event-forwarder.d.ts +15 -0
  7. package/dist/es/{components/Embed → src/components/PlayerEmbed}/controllers/fetcher.d.ts +5 -4
  8. package/dist/es/src/components/PlayerEmbed/controllers/tracking/index.d.ts +36 -0
  9. package/dist/es/src/components/PlayerEmbed/controllers/tracking/utils.d.ts +17 -0
  10. package/dist/es/src/components/PlayerEmbed/index.d.ts +75 -0
  11. package/dist/es/src/components/PlayerEmbed/tests/data.d.ts +4 -0
  12. package/dist/es/src/components/PlayerEmbed/tests/media-data.d.ts +19 -0
  13. package/dist/es/src/index.d.ts +2 -0
  14. package/dist/iife/dialog-embed/embed.iife.js +4042 -0
  15. package/dist/iife/dialog-embed/embed.iife.js.map +1 -0
  16. package/dist/iife/embeds.iife.js +3059 -447
  17. package/dist/iife/embeds.iife.js.map +1 -1
  18. package/dist/iife/player-embed/embed.iife.js +3904 -0
  19. package/dist/iife/player-embed/embed.iife.js.map +1 -0
  20. package/package.json +44 -31
  21. package/src/components/DialogEmbed/Dialog.stories.ts +91 -0
  22. package/src/components/DialogEmbed/DialogOverlay.ts +131 -0
  23. package/src/components/DialogEmbed/DialogPortal.ts +126 -0
  24. package/src/components/DialogEmbed/index.ts +97 -0
  25. package/src/components/PlayerEmbed/MultiEmbed.stories.ts +135 -0
  26. package/src/components/{Embed/Embed.stories.ts → PlayerEmbed/PlayerEmbed.stories.ts} +42 -15
  27. package/src/components/{Embed → PlayerEmbed}/controllers/event-forwarder.ts +6 -5
  28. package/src/components/{Embed → PlayerEmbed}/controllers/fetcher.ts +36 -17
  29. package/src/components/PlayerEmbed/controllers/tracking/index.ts +224 -0
  30. package/src/components/PlayerEmbed/controllers/tracking/utils.ts +95 -0
  31. package/src/components/{Embed → PlayerEmbed}/index.ts +102 -41
  32. package/src/components/PlayerEmbed/tests/PlayerEmbed.spec.ts +87 -0
  33. package/src/components/PlayerEmbed/tests/data.ts +227 -0
  34. package/src/components/PlayerEmbed/tests/media-data.ts +22 -0
  35. package/src/index.ts +2 -1
  36. package/dist/es/components/Embed/controllers/tracking.d.ts +0 -28
  37. package/dist/es/components/Embed/index.d.ts +0 -66
  38. package/dist/es/index.d.ts +0 -1
  39. package/src/components/Embed/controllers/tracking.ts +0 -271
  40. /package/dist/es/{utils → src/utils}/env.d.ts +0 -0
  41. /package/dist/es/{utils → src/utils}/events.d.ts +0 -0
@@ -0,0 +1,135 @@
1
+ import { html } from 'lit';
2
+ import { ifDefined } from 'lit/directives/if-defined.js';
3
+
4
+ import type { PlayerEmbedProps } from '.';
5
+ import type { Meta, StoryObj } from '@storybook/web-components';
6
+
7
+ import '.';
8
+
9
+ type MultiEmbedArgs = PlayerEmbedProps & {
10
+ apiKey1?: string;
11
+ apiKey2?: string;
12
+ apiKey3?: string;
13
+ vouchId1?: string;
14
+ vouchId2?: string;
15
+ vouchId3?: string;
16
+ showVouch?: boolean;
17
+ };
18
+
19
+ const _MultiEmbed = ({
20
+ vouchId1,
21
+ vouchId2,
22
+ vouchId3,
23
+ templateId,
24
+ questions,
25
+ preload,
26
+ autoplay,
27
+ env,
28
+ apiKey1,
29
+ apiKey2,
30
+ apiKey3,
31
+ controls,
32
+ aspectRatio
33
+ }: MultiEmbedArgs) => {
34
+ return html`
35
+ <div style="height: 33vh">
36
+ <vouch-embed-player
37
+ env=${ifDefined(env)}
38
+ apiKey=${ifDefined(apiKey1)}
39
+ vouchId=${ifDefined(vouchId1)}
40
+ templateId=${ifDefined(templateId)}
41
+ .questions=${questions}
42
+ .controls=${controls}
43
+ ?autoplay=${autoplay}
44
+ preload=${ifDefined(preload)}
45
+ aspectRatio=${ifDefined(aspectRatio)}
46
+ @error=${console.log}
47
+ ></vouch-embed-player>
48
+ </div>
49
+ <div style="height: 33vh">
50
+ <vouch-embed-player
51
+ env=${ifDefined(env)}
52
+ apiKey=${ifDefined(apiKey2)}
53
+ vouchId=${ifDefined(vouchId2)}
54
+ templateId=${ifDefined(templateId)}
55
+ .questions=${questions}
56
+ .controls=${controls}
57
+ ?autoplay=${autoplay}
58
+ preload=${ifDefined(preload)}
59
+ aspectRatio=${ifDefined(aspectRatio)}
60
+ @error=${console.log}
61
+ ></vouch-embed-player>
62
+ </div>
63
+ <div style="height: 33vh">
64
+ <vouch-embed-player
65
+ env=${ifDefined(env)}
66
+ apiKey=${ifDefined(apiKey3)}
67
+ vouchId=${ifDefined(vouchId3)}
68
+ templateId=${ifDefined(templateId)}
69
+ .questions=${questions}
70
+ .controls=${controls}
71
+ ?autoplay=${autoplay}
72
+ preload=${ifDefined(preload)}
73
+ aspectRatio=${ifDefined(aspectRatio)}
74
+ @error=${console.log}
75
+ ></vouch-embed-player>
76
+ </div>
77
+ `;
78
+ };
79
+
80
+ // More on how to set up stories at: https://storybook.js.org/docs/web-components/writing-stories/introduction
81
+ const meta = {
82
+ title: 'Embeds',
83
+ tags: ['autodocs'],
84
+ render: (args) => _MultiEmbed(args),
85
+ component: 'vouch-embed-player'
86
+ } satisfies Meta<PlayerEmbedProps>;
87
+
88
+ type Story = StoryObj<MultiEmbedArgs>;
89
+
90
+ const MultiPlayer: Story = {
91
+ args: {
92
+ env: 'dev',
93
+ apiKey1: 'TVik9uTMgE-PD25UTHIS6gyl0hMBWC7AT4dkpdlLBT4VIfDWZJrQiCk6Ak7m1',
94
+ vouchId1: '6JQEIPeStt',
95
+ apiKey2: 'TVik9uTMgE-PD25UTHIS6gyl0hMBWC7AT4dkpdlLBT4VIfDWZJrQiCk6Ak7m1',
96
+ vouchId2: '6JQEIPeStt',
97
+ apiKey3: 'TVik9uTMgE-PD25UTHIS6gyl0hMBWC7AT4dkpdlLBT4VIfDWZJrQiCk6Ak7m1',
98
+ vouchId3: '6JQEIPeStt',
99
+ templateId: '357fc118-e179-4171-9446-ff2b8e9d1b29',
100
+ questions: [],
101
+ aspectRatio: 0,
102
+ preload: 'none',
103
+ autoplay: false,
104
+ controls: [
105
+ 'progress',
106
+ 'play-large',
107
+ 'navigation',
108
+ 'play',
109
+ 'volume',
110
+ 'current-time',
111
+ 'duration',
112
+ 'speed',
113
+ 'captions',
114
+ 'fullscreen',
115
+ 'preview',
116
+ 'languages'
117
+ ]
118
+ },
119
+ argTypes: {
120
+ env: {
121
+ control: 'radio',
122
+ options: ['local', 'dev', 'staging', 'prod']
123
+ },
124
+ preload: {
125
+ control: 'radio',
126
+ options: ['auto', 'none']
127
+ }
128
+ },
129
+ parameters: {
130
+ layout: 'fullscreen'
131
+ }
132
+ };
133
+
134
+ export default meta;
135
+ export { MultiPlayer };
@@ -1,51 +1,78 @@
1
1
  import { html } from 'lit';
2
2
  import { ifDefined } from 'lit/directives/if-defined.js';
3
3
 
4
- import type { EmbedProps } from './';
4
+ import type { PlayerEmbedProps } from '.';
5
5
  import type { Meta, StoryObj } from '@storybook/web-components';
6
6
 
7
- import './';
7
+ import '.';
8
8
 
9
- type EmbedArgs = EmbedProps & {
9
+ type PlayerEmbedArgs = PlayerEmbedProps & {
10
10
  showVouch?: boolean;
11
11
  };
12
12
 
13
- const _Embed = ({ vouchId, templateId, preload, autoplay, env, apiKey, controls, aspectRatio }: EmbedArgs) => {
13
+ const _PlayerEmbed = ({
14
+ vouchId,
15
+ templateId,
16
+ questions,
17
+ preload,
18
+ autoplay,
19
+ env,
20
+ apiKey,
21
+ controls,
22
+ aspectRatio
23
+ }: PlayerEmbedArgs) => {
14
24
  return html`
15
25
  <div style="height: 100vh">
16
- <vouch-embed
26
+ <vouch-embed-player
17
27
  env=${ifDefined(env)}
18
28
  apiKey=${ifDefined(apiKey)}
19
29
  vouchId=${ifDefined(vouchId)}
20
30
  templateId=${ifDefined(templateId)}
31
+ .questions=${questions}
21
32
  .controls=${controls}
22
33
  ?autoplay=${autoplay}
23
34
  preload=${ifDefined(preload)}
24
35
  aspectRatio=${ifDefined(aspectRatio)}
25
- ></vouch-embed>
36
+ @error=${console.log}
37
+ ></vouch-embed-player>
26
38
  </div>
27
39
  `;
28
40
  };
29
41
 
30
42
  // More on how to set up stories at: https://storybook.js.org/docs/web-components/writing-stories/introduction
31
43
  const meta = {
32
- title: 'Embed',
44
+ title: 'Embeds',
33
45
  tags: ['autodocs'],
34
- render: (args) => _Embed(args),
35
- component: 'vouch-embed'
36
- } satisfies Meta<EmbedProps>;
46
+ render: (args) => _PlayerEmbed(args),
47
+ component: 'vouch-embed-player'
48
+ } satisfies Meta<PlayerEmbedProps>;
37
49
 
38
- type Story = StoryObj<EmbedArgs>;
50
+ type Story = StoryObj<PlayerEmbedArgs>;
39
51
 
40
- const Embed: Story = {
52
+ const Player: Story = {
41
53
  args: {
42
- env: 'local',
54
+ env: 'dev',
43
55
  apiKey: 'TVik9uTMgE-PD25UTHIS6gyl0hMBWC7AT4dkpdlLBT4VIfDWZJrQiCk6Ak7m1',
44
56
  vouchId: '6JQEIPeStt',
45
57
  templateId: '357fc118-e179-4171-9446-ff2b8e9d1b29',
58
+ questions: [],
46
59
  aspectRatio: 0,
47
60
  preload: 'none',
48
- autoplay: false
61
+ autoplay: false,
62
+ controls: [
63
+ 'progress',
64
+ 'play-large',
65
+ 'navigation',
66
+ 'play',
67
+ 'volume',
68
+ 'current-time',
69
+ 'duration',
70
+ 'speed',
71
+ 'captions',
72
+ 'fullscreen',
73
+ 'preview',
74
+ 'languages'
75
+ ]
49
76
  },
50
77
  argTypes: {
51
78
  env: {
@@ -63,4 +90,4 @@ const Embed: Story = {
63
90
  };
64
91
 
65
92
  export default meta;
66
- export { Embed };
93
+ export { Player };
@@ -1,27 +1,28 @@
1
1
  import { createRef, ref } from 'lit/directives/ref.js';
2
2
 
3
- import type { Embed } from '..';
3
+ import type { PlayerEmbed } from '..';
4
4
  import type { ReactiveController, ReactiveControllerHost } from 'lit';
5
+ import type { DirectiveResult } from 'lit/directive.js';
5
6
  import type { Ref } from 'lit/directives/ref.js';
6
7
 
7
8
  import { forwardEvent } from '~/utils/events';
8
9
 
9
- type EmbedHost = ReactiveControllerHost & Embed;
10
+ type PlayerEmbedHost = ReactiveControllerHost & PlayerEmbed;
10
11
 
11
12
  class EventForwardController implements ReactiveController {
12
- host: EmbedHost;
13
+ host: PlayerEmbedHost;
13
14
 
14
15
  private _events: string[] = [];
15
16
  private _cleanup: (() => void)[] = [];
16
17
  private _forwardElementRef: Ref<HTMLElement> = createRef();
17
18
 
18
- constructor(host: EmbedHost, events: string[]) {
19
+ constructor(host: PlayerEmbedHost, events: string[]) {
19
20
  this.host = host;
20
21
  this._events = events;
21
22
  host.addController(this);
22
23
  }
23
24
 
24
- register() {
25
+ register(): DirectiveResult {
25
26
  return ref(this._forwardElementRef);
26
27
  }
27
28
 
@@ -1,26 +1,29 @@
1
1
  import { Task } from '@lit/task';
2
2
  import { v4 as uuidv4 } from 'uuid';
3
3
 
4
- import type { Embed, EmbedProps } from '..';
4
+ import type { PlayerEmbed, PlayerEmbedProps } from '..';
5
5
  import type { ReactiveControllerHost } from 'lit';
6
6
  import type { Environment } from '~/utils/env';
7
7
 
8
8
  import { getEnvUrls } from '~/utils/env';
9
9
 
10
- type EmbedHost = ReactiveControllerHost & Embed;
10
+ type PlayerEmbedHost = ReactiveControllerHost & PlayerEmbed;
11
11
 
12
- type TaskDeps = [
13
- EmbedProps['env'],
14
- EmbedProps['apiKey'],
15
- EmbedProps['data'],
16
- EmbedProps['vouchId'],
17
- EmbedProps['templateId']
12
+ type FetchTaskDeps = [
13
+ PlayerEmbedProps['env'],
14
+ PlayerEmbedProps['apiKey'],
15
+ PlayerEmbedProps['data'],
16
+ PlayerEmbedProps['vouchId'],
17
+ PlayerEmbedProps['templateId']
18
18
  ];
19
19
 
20
+ type FilterTaskDeps = [PlayerEmbedProps['data'], PlayerEmbedProps['questions']];
21
+
20
22
  class FetcherController {
21
- host: EmbedHost;
23
+ host: PlayerEmbedHost;
22
24
 
23
25
  private _fetching = false;
26
+ private _vouch: PlayerEmbedProps['data'];
24
27
 
25
28
  set fetching(value) {
26
29
  if (this._fetching !== value) {
@@ -45,13 +48,13 @@ class FetcherController {
45
48
  });
46
49
 
47
50
  const vouch = await res.json();
48
- this.host.dispatchEvent(new CustomEvent('vouch:loaded', { detail: vouchId }));
51
+ this.host.dispatchEvent(new CustomEvent('vouch:loaded', { detail: vouch?.id }));
49
52
 
50
53
  // HACK: we're currently using API Gateway caching on the embed API without any invalidation logic,
51
54
  // so to ensure that the cache stays up to date, whenever we detect a cache hit we trigger another
52
55
  // API call with the `Cache-Control` header which will re-fill the cache
53
56
  const resCacheCheck = res?.headers?.get('X-Cache-Check');
54
- if (resCacheCheck && resCacheCheck !== cacheCheck) {
57
+ if (resCacheCheck !== cacheCheck) {
55
58
  fetch(`${embedApiUrl}/vouches/${vouchId}`, {
56
59
  method: 'GET',
57
60
  headers: [
@@ -81,7 +84,7 @@ class FetcherController {
81
84
  // so to ensure that the cache stays up to date, whenever we detect a cache hit we trigger another
82
85
  // API call with the `Cache-Control` header which will re-fill the cache
83
86
  const resCacheCheck = res?.headers?.get('X-Cache-Check');
84
- if (resCacheCheck && resCacheCheck !== cacheCheck) {
87
+ if (resCacheCheck !== cacheCheck) {
85
88
  fetch(`${embedApiUrl}/templates/${templateId}`, {
86
89
  method: 'GET',
87
90
  headers: [
@@ -94,11 +97,11 @@ class FetcherController {
94
97
  return template;
95
98
  };
96
99
 
97
- constructor(host: EmbedHost) {
100
+ constructor(host: PlayerEmbedHost) {
98
101
  this.host = host;
99
- new Task<TaskDeps, void>(
102
+ new Task<FetchTaskDeps, void>(
100
103
  this.host,
101
- async ([env, apiKey, data, vouchId, templateId]: TaskDeps) => {
104
+ async ([env, apiKey, data, vouchId, templateId]: FetchTaskDeps) => {
102
105
  try {
103
106
  host.vouch = undefined;
104
107
  host.template = undefined;
@@ -109,7 +112,7 @@ class FetcherController {
109
112
  this.fetching = true;
110
113
  template = await this.getTemplate(env, apiKey, templateId);
111
114
  }
112
- host.vouch = data;
115
+ this._vouch = data;
113
116
  host.template = template ?? data?.settings?.template?.instance;
114
117
  } else if (vouchId) {
115
118
  this.fetching = true;
@@ -118,7 +121,7 @@ class FetcherController {
118
121
  this.getVouch(env, apiKey, vouchId),
119
122
  templateId ? this.getTemplate(env, apiKey, templateId) : null
120
123
  ]);
121
- host.vouch = vouch;
124
+ this._vouch = vouch;
122
125
  host.template = template ?? vouch?.settings?.template?.instance;
123
126
  }
124
127
  } finally {
@@ -127,6 +130,22 @@ class FetcherController {
127
130
  },
128
131
  () => [host.env, host.apiKey, host.data, host.vouchId, host.templateId]
129
132
  );
133
+
134
+ // This second task is to be able to filter the vouch without fetching it again if only the questions changed
135
+ new Task<FilterTaskDeps, void>(
136
+ this.host,
137
+ ([vouch, questions]: FilterTaskDeps) => {
138
+ host.vouch = vouch
139
+ ? {
140
+ ...vouch,
141
+ questions: {
142
+ items: vouch?.questions.items.filter((_, index) => !questions?.length || questions?.includes(index + 1))
143
+ }
144
+ }
145
+ : undefined;
146
+ },
147
+ () => [this._vouch, host.questions]
148
+ );
130
149
  }
131
150
  }
132
151
 
@@ -0,0 +1,224 @@
1
+ import type { PlayerEmbed } from '../..';
2
+ import type { MediaEventDetail } from '@vouchfor/media-player';
3
+ import type { ReactiveController, ReactiveControllerHost } from 'lit';
4
+
5
+ import { findVouchId, getReportingMetadata, getUids } from './utils';
6
+ import { getEnvUrls } from '~/utils/env';
7
+
8
+ const MINIMUM_SEND_THRESHOLD = 1;
9
+
10
+ type PlayerEmbedHost = ReactiveControllerHost & PlayerEmbed;
11
+
12
+ type TrackingEvent = 'VOUCH_LOADED' | 'VOUCH_RESPONSE_VIEWED' | 'VIDEO_PLAYED' | 'VIDEO_STREAMED';
13
+ type TrackingPayload = {
14
+ vouchId?: string;
15
+ answerId?: string;
16
+ streamStart?: number;
17
+ streamEnd?: number;
18
+ };
19
+
20
+ type BatchEvent = {
21
+ event: TrackingEvent;
22
+ payload: TrackingPayload & {
23
+ time: string;
24
+ };
25
+ };
26
+
27
+ type TimeMap = {
28
+ [key: string]: number;
29
+ };
30
+
31
+ type BooleanMap = {
32
+ [key: string]: boolean;
33
+ };
34
+
35
+ class TrackingController implements ReactiveController {
36
+ host: PlayerEmbedHost;
37
+
38
+ private _batchedEvents: BatchEvent[] = [];
39
+ private _hasPlayed = false;
40
+ private _hasLoaded: BooleanMap = {};
41
+ private _answersViewed: BooleanMap = {};
42
+ private _streamStartTime: TimeMap = {};
43
+ private _streamLatestTime: TimeMap = {};
44
+ private _currentlyPlayingVideo: MediaEventDetail | null = null;
45
+
46
+ constructor(host: PlayerEmbedHost) {
47
+ this.host = host;
48
+ host.addController(this);
49
+ }
50
+
51
+ private _createTrackingEvent = (event: TrackingEvent, payload?: TrackingPayload) => {
52
+ const vouchId = findVouchId(payload, this.host.vouch);
53
+
54
+ if (!vouchId || this.host.disableTracking) {
55
+ return;
56
+ }
57
+
58
+ this._batchedEvents.push({
59
+ event,
60
+ payload: {
61
+ ...payload,
62
+ vouchId,
63
+ time: new Date().toISOString()
64
+ }
65
+ });
66
+ };
67
+
68
+ private _sendTrackingEvent = () => {
69
+ if (this._batchedEvents.length <= 0) {
70
+ return;
71
+ }
72
+
73
+ const { publicApiUrl } = getEnvUrls(this.host.env);
74
+ const { client, tab, request, visitor } = getUids(this.host.env);
75
+
76
+ navigator.sendBeacon(
77
+ `${publicApiUrl}/api/batchevents`,
78
+ JSON.stringify({
79
+ payload: {
80
+ events: this._batchedEvents
81
+ },
82
+ context: {
83
+ 'x-uid-client': client,
84
+ 'x-uid-tab': tab,
85
+ 'x-uid-request': request,
86
+ 'x-uid-visitor': visitor,
87
+ 'x-reporting-metadata': getReportingMetadata(this.host.trackingSource)
88
+ }
89
+ })
90
+ );
91
+
92
+ this._batchedEvents = [];
93
+ };
94
+
95
+ private _streamEnded = () => {
96
+ if (this._currentlyPlayingVideo) {
97
+ const { id, key } = this._currentlyPlayingVideo;
98
+ // Don't send a tracking event when seeking backwards
99
+ if (this._streamLatestTime[key] > this._streamStartTime[key] + MINIMUM_SEND_THRESHOLD) {
100
+ // Send a video streamed event any time the stream ends to capture the time between starting
101
+ // the video and the video stopping for any reason (pausing, deleting the embed node or closing the browser)
102
+ this._createTrackingEvent('VIDEO_STREAMED', {
103
+ answerId: id,
104
+ streamStart: this._streamStartTime[key],
105
+ streamEnd: this._streamLatestTime[key]
106
+ });
107
+ }
108
+
109
+ // Make sure these events are only sent once by deleting the start and latest times
110
+ delete this._streamStartTime[key];
111
+ delete this._streamLatestTime[key];
112
+ }
113
+ };
114
+
115
+ private _handleVouchLoaded = ({ detail: vouchId }: CustomEvent<string>) => {
116
+ if (!vouchId) {
117
+ return;
118
+ }
119
+
120
+ // Only send loaded event once per session
121
+ if (!this._hasLoaded[vouchId]) {
122
+ this._createTrackingEvent('VOUCH_LOADED', { vouchId });
123
+ this._hasLoaded[vouchId] = true;
124
+ }
125
+ };
126
+
127
+ private _handlePlay = () => {
128
+ // Only send the video played event once per session
129
+ if (!this._hasPlayed) {
130
+ this._createTrackingEvent('VIDEO_PLAYED', {
131
+ streamStart: this.host.currentTime
132
+ });
133
+ this._hasPlayed = true;
134
+ }
135
+ };
136
+
137
+ private _handleVideoPlay = ({ detail: { id, key } }: CustomEvent<MediaEventDetail>) => {
138
+ // Only increment play count once per session
139
+ if (!this._answersViewed[key]) {
140
+ this._createTrackingEvent('VOUCH_RESPONSE_VIEWED', {
141
+ answerId: id
142
+ });
143
+ this._answersViewed[key] = true;
144
+ }
145
+ };
146
+
147
+ private _handleVideoTimeUpdate = ({ detail: { id, key, node } }: CustomEvent<MediaEventDetail>) => {
148
+ if (
149
+ // We only want to count any time that the video is actually playing
150
+ !this.host.paused
151
+ // Only update the latest time if this event fires for the currently active video
152
+ // id === this.host.scene?.video?.id
153
+ ) {
154
+ this._currentlyPlayingVideo = { id, key, node };
155
+ this._streamLatestTime[key] = node.currentTime;
156
+
157
+ if (!this._streamStartTime[key]) {
158
+ this._streamStartTime[key] = node.currentTime;
159
+ this._streamLatestTime[key] = node.currentTime;
160
+ }
161
+ }
162
+ };
163
+
164
+ private _handleVideoPause = ({ detail: { id, key } }: CustomEvent<MediaEventDetail>) => {
165
+ if (this._streamLatestTime[key] > this._streamStartTime[key] + MINIMUM_SEND_THRESHOLD) {
166
+ this._createTrackingEvent('VIDEO_STREAMED', {
167
+ answerId: id,
168
+ streamStart: this._streamStartTime[key],
169
+ streamEnd: this._streamLatestTime[key]
170
+ });
171
+ }
172
+ delete this._streamStartTime[key];
173
+ delete this._streamLatestTime[key];
174
+ };
175
+
176
+ private _pageUnloading = () => {
177
+ this._streamEnded();
178
+ this._sendTrackingEvent();
179
+ };
180
+
181
+ private _handleVisibilityChange = () => {
182
+ if (document.visibilityState === 'hidden') {
183
+ this._pageUnloading();
184
+ }
185
+ };
186
+
187
+ private _handlePageHide = () => {
188
+ this._pageUnloading();
189
+ };
190
+
191
+ hostConnected() {
192
+ requestAnimationFrame(() => {
193
+ if ('onvisibilitychange' in document) {
194
+ document.addEventListener('visibilitychange', this._handleVisibilityChange);
195
+ } else {
196
+ window.addEventListener('pagehide', this._handlePageHide);
197
+ }
198
+ this.host.addEventListener('vouch:loaded', this._handleVouchLoaded);
199
+ this.host.mediaPlayer?.addEventListener('play', this._handlePlay);
200
+ this.host.mediaPlayer?.addEventListener('video:play', this._handleVideoPlay);
201
+ this.host.mediaPlayer?.addEventListener('video:pause', this._handleVideoPause);
202
+ this.host.mediaPlayer?.addEventListener('video:timeupdate', this._handleVideoTimeUpdate);
203
+ });
204
+ }
205
+
206
+ hostDisconnected() {
207
+ // Send events if DOM node is destroyed
208
+ this._pageUnloading();
209
+
210
+ if ('onvisibilitychange' in document) {
211
+ document.removeEventListener('visibilitychange', this._handleVisibilityChange);
212
+ } else {
213
+ window.removeEventListener('pagehide', this._handlePageHide);
214
+ }
215
+ this.host.removeEventListener('vouch:loaded', this._handleVouchLoaded);
216
+ this.host.mediaPlayer?.removeEventListener('play', this._handlePlay);
217
+ this.host.mediaPlayer?.removeEventListener('video:play', this._handleVideoPlay);
218
+ this.host.mediaPlayer?.removeEventListener('video:pause', this._handleVideoPause);
219
+ this.host.mediaPlayer?.removeEventListener('video:timeupdate', this._handleVideoTimeUpdate);
220
+ }
221
+ }
222
+
223
+ export { TrackingController };
224
+ export type { TrackingEvent, TrackingPayload };