@vouchfor/embeds 0.0.0-experiment.2f44fa4 → 0.0.0-experiment.310b30d

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.
Files changed (32) hide show
  1. package/dist/es/embeds.js +963 -1434
  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/src/components/{Embed → PlayerEmbed}/controllers/fetcher.d.ts +4 -4
  8. package/dist/es/src/components/{Embed/controllers/tracking.d.ts → PlayerEmbed/controllers/tracking/index.d.ts} +14 -11
  9. package/dist/es/src/components/PlayerEmbed/controllers/tracking/utils.d.ts +17 -0
  10. package/dist/es/src/components/{Embed → PlayerEmbed}/index.d.ts +27 -22
  11. package/dist/es/src/components/PlayerEmbed/tests/data.d.ts +3 -0
  12. package/dist/es/src/index.d.ts +2 -1
  13. package/dist/iife/dialog-embed/embed.iife.js +1750 -0
  14. package/dist/iife/dialog-embed/embed.iife.js.map +1 -0
  15. package/dist/iife/embeds.iife.js +576 -383
  16. package/dist/iife/embeds.iife.js.map +1 -1
  17. package/dist/iife/player-embed/embed.iife.js +1612 -0
  18. package/dist/iife/player-embed/embed.iife.js.map +1 -0
  19. package/package.json +43 -31
  20. package/src/components/DialogEmbed/Dialog.stories.ts +91 -0
  21. package/src/components/DialogEmbed/DialogOverlay.ts +131 -0
  22. package/src/components/DialogEmbed/DialogPortal.ts +126 -0
  23. package/src/components/DialogEmbed/index.ts +97 -0
  24. package/src/components/{Embed/Embed.stories.ts → PlayerEmbed/PlayerEmbed.stories.ts} +15 -15
  25. package/src/components/{Embed → PlayerEmbed}/controllers/event-forwarder.ts +6 -5
  26. package/src/components/{Embed → PlayerEmbed}/controllers/fetcher.ts +11 -11
  27. package/src/components/{Embed/controllers/tracking.ts → PlayerEmbed/controllers/tracking/index.ts} +47 -115
  28. package/src/components/PlayerEmbed/controllers/tracking/utils.ts +95 -0
  29. package/src/components/{Embed → PlayerEmbed}/index.ts +67 -26
  30. package/src/components/PlayerEmbed/tests/PlayerEmbed.spec.ts +80 -0
  31. package/src/components/PlayerEmbed/tests/data.ts +183 -0
  32. package/src/index.ts +2 -1
@@ -1,29 +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
12
  type FetchTaskDeps = [
13
- EmbedProps['env'],
14
- EmbedProps['apiKey'],
15
- EmbedProps['data'],
16
- EmbedProps['vouchId'],
17
- EmbedProps['templateId']
13
+ PlayerEmbedProps['env'],
14
+ PlayerEmbedProps['apiKey'],
15
+ PlayerEmbedProps['data'],
16
+ PlayerEmbedProps['vouchId'],
17
+ PlayerEmbedProps['templateId']
18
18
  ];
19
19
 
20
- type FilterTaskDeps = [EmbedProps['data'], EmbedProps['questions']];
20
+ type FilterTaskDeps = [PlayerEmbedProps['data'], PlayerEmbedProps['questions']];
21
21
 
22
22
  class FetcherController {
23
- host: EmbedHost;
23
+ host: PlayerEmbedHost;
24
24
 
25
25
  private _fetching = false;
26
- private _vouch: EmbedProps['data'];
26
+ private _vouch: PlayerEmbedProps['data'];
27
27
 
28
28
  set fetching(value) {
29
29
  if (this._fetching !== value) {
@@ -97,7 +97,7 @@ class FetcherController {
97
97
  return template;
98
98
  };
99
99
 
100
- constructor(host: EmbedHost) {
100
+ constructor(host: PlayerEmbedHost) {
101
101
  this.host = host;
102
102
  new Task<FetchTaskDeps, void>(
103
103
  this.host,
@@ -1,15 +1,13 @@
1
- import { v4 as uuidv4 } from 'uuid';
2
-
3
- import type { Embed } from '..';
1
+ import type { PlayerEmbed } from '../..';
4
2
  import type { VideoEventDetail } from '@vouchfor/media-player';
5
3
  import type { ReactiveController, ReactiveControllerHost } from 'lit';
6
4
 
7
- import packageJson from '../../../../package.json';
5
+ import { findVouchId, getReportingMetadata, getUids } from './utils';
8
6
  import { getEnvUrls } from '~/utils/env';
9
7
 
10
8
  const MINIMUM_SEND_THRESHOLD = 1;
11
9
 
12
- type EmbedHost = ReactiveControllerHost & Embed;
10
+ type PlayerEmbedHost = ReactiveControllerHost & PlayerEmbed;
13
11
 
14
12
  type TrackingEvent = 'VOUCH_LOADED' | 'VOUCH_RESPONSE_VIEWED' | 'VIDEO_PLAYED' | 'VIDEO_STREAMED';
15
13
  type TrackingPayload = {
@@ -19,6 +17,13 @@ type TrackingPayload = {
19
17
  streamEnd?: number;
20
18
  };
21
19
 
20
+ type BatchEvent = {
21
+ event: TrackingEvent;
22
+ payload: TrackingPayload & {
23
+ time: string;
24
+ };
25
+ };
26
+
22
27
  type TimeMap = {
23
28
  [key: string]: number;
24
29
  };
@@ -28,12 +33,9 @@ type BooleanMap = {
28
33
  };
29
34
 
30
35
  class TrackingController implements ReactiveController {
31
- host: EmbedHost;
32
-
33
- private _tabId: string | undefined = undefined;
34
- private _clientId: string | undefined = undefined;
35
- private _visitorId: string | undefined = undefined;
36
+ host: PlayerEmbedHost;
36
37
 
38
+ private _batchedEvents: BatchEvent[] = [];
37
39
  private _hasPlayed = false;
38
40
  private _hasLoaded: BooleanMap = {};
39
41
  private _answersViewed: BooleanMap = {};
@@ -41,124 +43,53 @@ class TrackingController implements ReactiveController {
41
43
  private _streamLatestTime: TimeMap = {};
42
44
  private _currentlyPlayingVideo: VideoEventDetail | null = null;
43
45
 
44
- constructor(host: EmbedHost) {
46
+ constructor(host: PlayerEmbedHost) {
45
47
  this.host = host;
46
48
  host.addController(this);
47
49
  }
48
50
 
49
- private _findVouchId(payload?: TrackingPayload) {
50
- if (payload && 'vouchId' in payload) {
51
- return payload.vouchId;
52
- }
53
- if (this.host.vouch) {
54
- return this.host.vouch.id;
55
- }
56
- return null;
57
- }
58
-
59
- private _createVisitor = (visitorId: string) => {
60
- const { publicApiUrl } = getEnvUrls(this.host.env);
61
- window.localStorage?.setItem?.('vouch-uid-visitor', visitorId);
62
- navigator.sendBeacon(`${publicApiUrl}/api/visitor`, JSON.stringify({ visitorId }));
63
- return visitorId;
64
- };
65
-
66
- private _getUids() {
67
- if (typeof window === 'undefined') {
68
- return {
69
- client: null,
70
- tab: null,
71
- request: uuidv4()
72
- };
73
- }
74
-
75
- // Persisted for a user for the same device + browser, so we can e.g. search for all logs related to that browser
76
- const visitorId =
77
- this._visitorId || window.localStorage?.getItem?.('vouch-uid-visitor') || this._createVisitor(uuidv4());
78
- // Persisted for a user for the same device + browser, so we can e.g. search for all logs related to that browser
79
- const clientId = this._clientId || window.localStorage?.getItem?.('vouch-uid-client') || uuidv4();
80
- // Persisted in session storage, so we can search for everything the user has done in a specific tab
81
- const tabId = this._tabId || window.sessionStorage?.getItem?.('vouch-uid-tab') || uuidv4();
82
- // Not persisted, allows us to search for any logs related to a single FE request
83
- // E.g. BE should pass this request ID through all other services to be able to group logs
84
- const requestId = uuidv4();
85
-
86
- // Cache and persist uids
87
- if (visitorId !== this._visitorId) {
88
- this._visitorId = visitorId;
89
- window.localStorage?.setItem?.('vouch-uid-visitor', visitorId);
90
- }
91
-
92
- if (clientId !== this._clientId) {
93
- this._clientId = clientId;
94
- window.localStorage?.setItem?.('vouch-uid-client', clientId);
95
- }
51
+ private _createTrackingEvent = (event: TrackingEvent, payload?: TrackingPayload) => {
52
+ const vouchId = findVouchId(payload, this.host.vouch);
96
53
 
97
- if (tabId !== this._tabId) {
98
- this._tabId = tabId;
99
- window.sessionStorage?.setItem?.('vouch-uid-tab', tabId);
54
+ if (!vouchId || this.host.disableTracking) {
55
+ return;
100
56
  }
101
57
 
102
- return {
103
- client: clientId,
104
- tab: tabId,
105
- request: requestId,
106
- visitor: visitorId
107
- };
108
- }
109
-
110
- private _getReportingMetadata = () => {
111
- const [country, region] = Intl.DateTimeFormat().resolvedOptions().timeZone?.split?.('/') ?? [];
112
-
113
- const utmParams: any = {};
114
- [...new URLSearchParams(location.search).entries()].forEach(([key, value]) => {
115
- if (/utm/.test(key)) {
116
- const param = key.toLowerCase().replace(/[-_][a-z0-9]/g, (group) => group.slice(-1).toUpperCase());
117
- utmParams[param] = value;
58
+ this._batchedEvents.push({
59
+ event,
60
+ payload: {
61
+ ...payload,
62
+ vouchId,
63
+ time: new Date().toISOString()
118
64
  }
119
65
  });
120
-
121
- return {
122
- source: this.host.trackingSource,
123
- time: new Date(),
124
- region,
125
- country,
126
- screenHeight: window.screen.height,
127
- screenWidth: window.screen.width,
128
- referrer: document.referrer,
129
- currentUrl: location.href,
130
- ...utmParams
131
- };
132
66
  };
133
67
 
134
- private _sendTrackingEvent = (event: TrackingEvent, payload?: TrackingPayload) => {
135
- const vouchId = this._findVouchId(payload);
136
-
137
- if (!vouchId || this.host.disableTracking) {
68
+ private _sendTrackingEvent = () => {
69
+ if (this._batchedEvents.length <= 0) {
138
70
  return;
139
71
  }
140
72
 
141
73
  const { publicApiUrl } = getEnvUrls(this.host.env);
142
- const { client, tab, request, visitor } = this._getUids();
74
+ const { client, tab, request, visitor } = getUids(this.host.env);
143
75
 
144
76
  navigator.sendBeacon(
145
- `${publicApiUrl}/api/v2/events`,
77
+ `${publicApiUrl}/api/batchevents`,
146
78
  JSON.stringify({
147
- event,
148
79
  payload: {
149
- ...payload,
150
- vouchId
80
+ events: this._batchedEvents
151
81
  },
152
82
  context: {
153
83
  'x-uid-client': client,
154
84
  'x-uid-tab': tab,
155
85
  'x-uid-request': request,
156
86
  'x-uid-visitor': visitor,
157
- 'x-reporting-metadata': this._getReportingMetadata(),
158
- 'x-embeds-version': packageJson.version
87
+ 'x-reporting-metadata': getReportingMetadata(this.host.trackingSource)
159
88
  }
160
89
  })
161
90
  );
91
+
92
+ this._batchedEvents = [];
162
93
  };
163
94
 
164
95
  private _streamEnded = () => {
@@ -168,7 +99,7 @@ class TrackingController implements ReactiveController {
168
99
  if (this._streamLatestTime[key] > this._streamStartTime[key] + MINIMUM_SEND_THRESHOLD) {
169
100
  // Send a video streamed event any time the stream ends to capture the time between starting
170
101
  // the video and the video stopping for any reason (pausing, deleting the embed node or closing the browser)
171
- this._sendTrackingEvent('VIDEO_STREAMED', {
102
+ this._createTrackingEvent('VIDEO_STREAMED', {
172
103
  answerId: id,
173
104
  streamStart: this._streamStartTime[key],
174
105
  streamEnd: this._streamLatestTime[key]
@@ -188,7 +119,7 @@ class TrackingController implements ReactiveController {
188
119
 
189
120
  // Only send loaded event once per session
190
121
  if (!this._hasLoaded[vouchId]) {
191
- this._sendTrackingEvent('VOUCH_LOADED', { vouchId });
122
+ this._createTrackingEvent('VOUCH_LOADED', { vouchId });
192
123
  this._hasLoaded[vouchId] = true;
193
124
  }
194
125
  };
@@ -196,26 +127,21 @@ class TrackingController implements ReactiveController {
196
127
  private _handlePlay = () => {
197
128
  // Only send the video played event once per session
198
129
  if (!this._hasPlayed) {
199
- this._sendTrackingEvent('VIDEO_PLAYED', {
130
+ this._createTrackingEvent('VIDEO_PLAYED', {
200
131
  streamStart: this.host.currentTime
201
132
  });
202
133
  this._hasPlayed = true;
203
134
  }
204
135
  };
205
136
 
206
- private _handleVideoPlay = ({ detail: { id, key, node } }: CustomEvent<VideoEventDetail>) => {
137
+ private _handleVideoPlay = ({ detail: { id, key } }: CustomEvent<VideoEventDetail>) => {
207
138
  // Only increment play count once per session
208
139
  if (!this._answersViewed[key]) {
209
- this._sendTrackingEvent('VOUCH_RESPONSE_VIEWED', {
140
+ this._createTrackingEvent('VOUCH_RESPONSE_VIEWED', {
210
141
  answerId: id
211
142
  });
212
143
  this._answersViewed[key] = true;
213
144
  }
214
-
215
- if (!this._streamStartTime[key]) {
216
- this._streamStartTime[key] = node.currentTime;
217
- this._streamLatestTime[key] = node.currentTime;
218
- }
219
145
  };
220
146
 
221
147
  private _handleVideoTimeUpdate = ({ detail: { id, key, node } }: CustomEvent<VideoEventDetail>) => {
@@ -227,12 +153,17 @@ class TrackingController implements ReactiveController {
227
153
  ) {
228
154
  this._currentlyPlayingVideo = { id, key, node };
229
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
+ }
230
161
  }
231
162
  };
232
163
 
233
164
  private _handleVideoPause = ({ detail: { id, key } }: CustomEvent<VideoEventDetail>) => {
234
165
  if (this._streamLatestTime[key] > this._streamStartTime[key] + MINIMUM_SEND_THRESHOLD) {
235
- this._sendTrackingEvent('VIDEO_STREAMED', {
166
+ this._createTrackingEvent('VIDEO_STREAMED', {
236
167
  answerId: id,
237
168
  streamStart: this._streamStartTime[key],
238
169
  streamEnd: this._streamLatestTime[key]
@@ -244,9 +175,7 @@ class TrackingController implements ReactiveController {
244
175
 
245
176
  private _pageUnloading = () => {
246
177
  this._streamEnded();
247
- // This will try to send the same stream event again so we delete the start and latest
248
- // time in stream ended so that there is no times to send and the pause event does nothing
249
- this.host.pause();
178
+ this._sendTrackingEvent();
250
179
  };
251
180
 
252
181
  private _handleVisibilityChange = () => {
@@ -275,7 +204,9 @@ class TrackingController implements ReactiveController {
275
204
  }
276
205
 
277
206
  hostDisconnected() {
278
- this._streamEnded();
207
+ // Send events if DOM node is destroyed
208
+ this._pageUnloading();
209
+
279
210
  if ('onvisibilitychange' in document) {
280
211
  document.removeEventListener('visibilitychange', this._handleVisibilityChange);
281
212
  } else {
@@ -290,3 +221,4 @@ class TrackingController implements ReactiveController {
290
221
  }
291
222
 
292
223
  export { TrackingController };
224
+ export type { TrackingEvent, TrackingPayload };
@@ -0,0 +1,95 @@
1
+ import { TEMPLATE_VERSION } from '@vouchfor/canvas-video';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+
4
+ import type { TrackingPayload } from '.';
5
+ import type { Vouch } from '@vouchfor/video-utils';
6
+ import type { Environment } from '~/utils/env';
7
+
8
+ import packageJson from '~/../package.json';
9
+ import { getEnvUrls } from '~/utils/env';
10
+
11
+ function createVisitor(env: Environment) {
12
+ const { publicApiUrl } = getEnvUrls(env);
13
+ const visitorId = uuidv4();
14
+ navigator.sendBeacon(`${publicApiUrl}/api/visitor`, JSON.stringify({ visitorId }));
15
+ return visitorId;
16
+ }
17
+
18
+ function getUids(env: Environment) {
19
+ if (typeof window === 'undefined') {
20
+ return {
21
+ client: null,
22
+ tab: null,
23
+ request: uuidv4()
24
+ };
25
+ }
26
+
27
+ // Persisted for a user for the same device + browser, so we can e.g. search for all logs related to that browser
28
+ let visitorId = window.localStorage?.getItem?.('vouch-uid-visitor');
29
+ // Persisted for a user for the same device + browser, so we can e.g. search for all logs related to that browser
30
+ let clientId = window.localStorage?.getItem?.('vouch-uid-client');
31
+ // Persisted in session storage, so we can search for everything the user has done in a specific tab
32
+ let tabId = window.sessionStorage?.getItem?.('vouch-uid-tab');
33
+ // Not persisted, allows us to search for any logs related to a single FE request
34
+ // E.g. BE should pass this request ID through all other services to be able to group logs
35
+ const requestId = uuidv4();
36
+
37
+ // Cache uids
38
+ if (!visitorId) {
39
+ visitorId = createVisitor(env);
40
+ window.localStorage?.setItem?.('vouch-uid-visitor', visitorId);
41
+ }
42
+
43
+ if (!clientId) {
44
+ clientId = uuidv4();
45
+ window.localStorage?.setItem?.('vouch-uid-client', clientId);
46
+ }
47
+
48
+ if (!tabId) {
49
+ tabId = uuidv4();
50
+ window.sessionStorage?.setItem?.('vouch-uid-tab', tabId);
51
+ }
52
+
53
+ return {
54
+ client: clientId,
55
+ tab: tabId,
56
+ request: requestId,
57
+ visitor: visitorId
58
+ };
59
+ }
60
+
61
+ function findVouchId(payload?: TrackingPayload, vouch?: Vouch) {
62
+ if (payload && 'vouchId' in payload) {
63
+ return payload.vouchId;
64
+ }
65
+ return vouch?.id ?? null;
66
+ }
67
+
68
+ function getReportingMetadata(source = 'embedded_player') {
69
+ const [country, region] = Intl.DateTimeFormat().resolvedOptions().timeZone?.split?.('/') ?? [];
70
+
71
+ const utmParams: any = {};
72
+ [...new URLSearchParams(location.search).entries()].forEach(([key, value]) => {
73
+ if (/utm/.test(key)) {
74
+ const param = key.toLowerCase().replace(/[-_][a-z0-9]/g, (group) => group.slice(-1).toUpperCase());
75
+ utmParams[param] = value;
76
+ }
77
+ });
78
+
79
+ return {
80
+ source,
81
+ time: new Date(),
82
+ region,
83
+ country,
84
+ screenHeight: window.screen.height,
85
+ screenWidth: window.screen.width,
86
+ referrer: document.referrer,
87
+ currentUrl: location.href,
88
+ embedType: 'media-player-embed',
89
+ embedVersion: packageJson.version,
90
+ templateVersion: TEMPLATE_VERSION,
91
+ ...utmParams
92
+ };
93
+ }
94
+
95
+ export { getUids, findVouchId, getReportingMetadata };
@@ -1,11 +1,10 @@
1
- import { html, LitElement } from 'lit';
1
+ import { css, html, LitElement } from 'lit';
2
2
  import { customElement, property, state } from 'lit/decorators.js';
3
3
  import { ifDefined } from 'lit/directives/if-defined.js';
4
4
  import { createRef, ref } from 'lit/directives/ref.js';
5
5
 
6
6
  import type { Scene, Scenes, TemplateInstance } from '@vouchfor/canvas-video';
7
7
  import type { MediaPlayer, MediaPlayerProps } from '@vouchfor/media-player';
8
- import type { Ref } from 'lit/directives/ref.js';
9
8
  import type { Environment } from '~/utils/env';
10
9
 
11
10
  import { EventForwardController } from './controllers/event-forwarder';
@@ -14,7 +13,7 @@ import { TrackingController } from './controllers/tracking';
14
13
 
15
14
  import '@vouchfor/media-player';
16
15
 
17
- type EmbedProps = Pick<MediaPlayerProps, 'data' | 'aspectRatio' | 'preload' | 'autoplay' | 'controls'> & {
16
+ type PlayerEmbedProps = Pick<MediaPlayerProps, 'data' | 'aspectRatio' | 'preload' | 'autoplay' | 'controls'> & {
18
17
  env: Environment;
19
18
  apiKey: string;
20
19
  disableTracking?: boolean;
@@ -25,24 +24,30 @@ type EmbedProps = Pick<MediaPlayerProps, 'data' | 'aspectRatio' | 'preload' | 'a
25
24
  questions?: number[];
26
25
  };
27
26
 
28
- @customElement('vouch-embed')
29
- class Embed extends LitElement {
30
- private _mediaPlayerRef: Ref<MediaPlayer> = createRef();
31
-
32
- @property({ type: Object }) data: EmbedProps['data'];
33
- @property({ type: String }) vouchId: EmbedProps['vouchId'];
34
- @property({ type: String }) templateId: EmbedProps['templateId'];
35
- @property({ type: Array }) questions: EmbedProps['questions'];
36
-
37
- @property({ type: String }) env: EmbedProps['env'] = 'prod';
38
- @property({ type: String }) apiKey: EmbedProps['apiKey'] = '';
39
- @property({ type: Boolean }) disableTracking: EmbedProps['disableTracking'] = false;
40
- @property({ type: String }) trackingSource: EmbedProps['trackingSource'] = 'embed';
41
-
42
- @property({ type: Array }) controls: EmbedProps['controls'];
43
- @property({ type: String }) preload: EmbedProps['preload'] = 'auto';
44
- @property({ type: Boolean }) autoplay: EmbedProps['autoplay'] = false;
45
- @property({ type: Number }) aspectRatio: EmbedProps['aspectRatio'] = 0;
27
+ @customElement('vouch-embed-player')
28
+ class PlayerEmbed extends LitElement {
29
+ static styles = [
30
+ css`
31
+ :host {
32
+ display: flex;
33
+ }
34
+ `
35
+ ];
36
+
37
+ @property({ type: Object }) data: PlayerEmbedProps['data'];
38
+ @property({ type: String }) vouchId: PlayerEmbedProps['vouchId'];
39
+ @property({ type: String }) templateId: PlayerEmbedProps['templateId'];
40
+ @property({ type: Array }) questions: PlayerEmbedProps['questions'];
41
+
42
+ @property({ type: String }) env: PlayerEmbedProps['env'] = 'prod';
43
+ @property({ type: String }) apiKey: PlayerEmbedProps['apiKey'] = '';
44
+ @property({ type: Boolean }) disableTracking: PlayerEmbedProps['disableTracking'] = false;
45
+ @property({ type: String }) trackingSource: PlayerEmbedProps['trackingSource'] = 'embedded_player';
46
+
47
+ @property({ type: Array }) controls: PlayerEmbedProps['controls'];
48
+ @property({ type: String }) preload: PlayerEmbedProps['preload'] = 'auto';
49
+ @property({ type: Boolean }) autoplay: PlayerEmbedProps['autoplay'] = false;
50
+ @property({ type: Number }) aspectRatio: PlayerEmbedProps['aspectRatio'] = 0;
46
51
 
47
52
  private eventController = new EventForwardController(this, [
48
53
  'durationchange',
@@ -77,17 +82,23 @@ class Embed extends LitElement {
77
82
  // @ts-ignore
78
83
  private _trackingController = new TrackingController(this);
79
84
 
80
- @state() vouch: EmbedProps['data'];
85
+ @state() vouch: PlayerEmbedProps['data'];
81
86
  @state() template: TemplateInstance | undefined;
82
87
 
83
88
  get fetching() {
84
89
  return this._fetcherController.fetching;
85
90
  }
86
91
 
92
+ private _mediaPlayerRef = createRef<MediaPlayer>();
93
+
87
94
  get waiting() {
88
95
  return this._mediaPlayerRef.value?.waiting;
89
96
  }
90
97
 
98
+ get initialised() {
99
+ return this._mediaPlayerRef.value?.initialised;
100
+ }
101
+
91
102
  get seeking() {
92
103
  return this._mediaPlayerRef.value?.seeking;
93
104
  }
@@ -172,12 +183,42 @@ class Embed extends LitElement {
172
183
  this._mediaPlayerRef.value?.pause();
173
184
  }
174
185
 
186
+ reset(time = 0, play = false) {
187
+ this._mediaPlayerRef.value?.reset(time, play);
188
+ }
189
+
175
190
  setScene(index: number) {
176
191
  this._mediaPlayerRef.value?.setScene(index);
177
192
  }
178
193
 
194
+ private _renderStyles() {
195
+ if (!this.aspectRatio) {
196
+ return html`
197
+ <style>
198
+ :host {
199
+ width: 100%;
200
+ height: 100%;
201
+ }
202
+ </style>
203
+ `;
204
+ }
205
+
206
+ if (typeof this.aspectRatio === 'number') {
207
+ return html`
208
+ <style>
209
+ :host {
210
+ aspect-ratio: ${this.aspectRatio};
211
+ }
212
+ </style>
213
+ `;
214
+ }
215
+
216
+ return null;
217
+ }
218
+
179
219
  render() {
180
220
  return html`
221
+ ${this._renderStyles()}
181
222
  <vmp-media-player
182
223
  ${ref(this._mediaPlayerRef)}
183
224
  ${this.eventController.register()}
@@ -195,15 +236,15 @@ class Embed extends LitElement {
195
236
 
196
237
  declare global {
197
238
  interface HTMLElementTagNameMap {
198
- 'vouch-embed': Embed;
239
+ 'vouch-embed-player': PlayerEmbed;
199
240
  }
200
241
 
201
242
  namespace JSX {
202
243
  interface IntrinsicElements {
203
- 'vouch-embed': Embed;
244
+ 'vouch-embed-player': PlayerEmbed;
204
245
  }
205
246
  }
206
247
  }
207
248
 
208
- export { Embed };
209
- export type { EmbedProps };
249
+ export { PlayerEmbed };
250
+ export type { PlayerEmbedProps };
@@ -0,0 +1,80 @@
1
+ /* eslint-disable @typescript-eslint/ban-ts-comment */
2
+ import { expect, fixture, waitUntil } from '@open-wc/testing';
3
+ import { html } from 'lit';
4
+ import sinon from 'sinon';
5
+
6
+ import type { PlayerEmbed } from '../index.js';
7
+ import type { VideoMap } from '@vouchfor/media-player';
8
+
9
+ import { data } from './data.js';
10
+
11
+ // Can't use typescript aliases with esbuild file transforms apparently
12
+ // No idea what a good way to actually build before testing is, the examples give nothing
13
+ // https://modern-web.dev/guides/test-runner/typescript/
14
+ import '../../../test/lib/embeds.js';
15
+
16
+ function getVideo(videos: VideoMap) {
17
+ return Object.values(videos)[0];
18
+ }
19
+
20
+ function playerLoaded(player: PlayerEmbed) {
21
+ return waitUntil(
22
+ () => {
23
+ return player.mediaPlayer?.initialised;
24
+ },
25
+ 'Player has not loaded video',
26
+ { timeout: 20000 }
27
+ );
28
+ }
29
+
30
+ describe('Embeds', () => {
31
+ it('passes', async () => {
32
+ const player = await fixture<PlayerEmbed>(
33
+ html`<vouch-embed-player env="dev" .data=${data} aspectratio=${1}></vouch-embed-player>`
34
+ );
35
+ // @ts-ignore - accessing private property
36
+ const sendTrackingSpy = sinon.spy(player._trackingController, '_sendTrackingEvent');
37
+ // @ts-ignore - accessing private property
38
+ const createTrackingSpy = sinon.spy(player._trackingController, '_createTrackingEvent');
39
+
40
+ await playerLoaded(player);
41
+ // Have to mute the player because we can't programmatically play videos with sound
42
+ player.muted = true;
43
+ player.play();
44
+ expect(player.paused).eq(false);
45
+ await waitUntil(
46
+ () => {
47
+ // Video plays for 3 seconds
48
+ return (getVideo(player.mediaPlayer!.videos)?.node?.currentTime ?? 0) > 3;
49
+ },
50
+ 'Video did not play for 3 seconds',
51
+ { timeout: 20000 }
52
+ );
53
+ expect(getVideo(player.mediaPlayer!.videos)?.node?.paused).eq(false);
54
+ player.pause();
55
+ expect(getVideo(player.mediaPlayer!.videos)?.node?.paused).eq(true);
56
+ expect(sendTrackingSpy.callCount).to.be.eq(0);
57
+ // Destroy node because events are sent when node is removed from the document
58
+ player.remove();
59
+ expect(sendTrackingSpy.callCount).to.be.eq(1);
60
+ expect(createTrackingSpy.args[0]).to.eql([
61
+ 'VIDEO_PLAYED',
62
+ {
63
+ streamStart: 0
64
+ }
65
+ ]);
66
+ expect(createTrackingSpy.args[1]).to.eql([
67
+ 'VOUCH_RESPONSE_VIEWED',
68
+ {
69
+ answerId: '5c66bb3a-ed68-41a0-a601-a49865104418'
70
+ }
71
+ ]);
72
+ expect(createTrackingSpy.args[2][0]).to.eq('VIDEO_STREAMED');
73
+ // Remove streamStart and streamEnd as these are non-deterministic
74
+ expect({ ...createTrackingSpy.args[2][1], streamStart: undefined, streamEnd: undefined }).to.eql({
75
+ answerId: '5c66bb3a-ed68-41a0-a601-a49865104418',
76
+ streamStart: undefined,
77
+ streamEnd: undefined
78
+ });
79
+ });
80
+ });