@vouchfor/embeds 0.0.0-experiment.75edba9 → 0.0.0-experiment.77626fa

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. package/dist/es/embeds.js +1144 -7
  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 +1755 -0
  14. package/dist/iife/dialog-embed/embed.iife.js.map +1 -0
  15. package/dist/iife/embeds.iife.js +578 -381
  16. package/dist/iife/embeds.iife.js.map +1 -1
  17. package/dist/iife/player-embed/embed.iife.js +1617 -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} +30 -16
  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 -118
  28. package/src/components/PlayerEmbed/controllers/tracking/utils.ts +95 -0
  29. package/src/components/{Embed → PlayerEmbed}/index.ts +62 -23
  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
  33. package/dist/es/browser-6d6dce17.js +0 -433
  34. package/dist/es/browser-6d6dce17.js.map +0 -1
  35. package/dist/es/index-6071eed0.js +0 -9948
  36. package/dist/es/index-6071eed0.js.map +0 -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,16 +1,13 @@
1
- import { TEMPLATE_VERSION } from '@vouchfor/canvas-video';
2
- import { v4 as uuidv4 } from 'uuid';
3
-
4
- import type { Embed } from '..';
1
+ import type { PlayerEmbed } from '../..';
5
2
  import type { VideoEventDetail } from '@vouchfor/media-player';
6
3
  import type { ReactiveController, ReactiveControllerHost } from 'lit';
7
4
 
8
- import packageJson from '../../../../package.json';
5
+ import { findVouchId, getReportingMetadata, getUids } from './utils';
9
6
  import { getEnvUrls } from '~/utils/env';
10
7
 
11
8
  const MINIMUM_SEND_THRESHOLD = 1;
12
9
 
13
- type EmbedHost = ReactiveControllerHost & Embed;
10
+ type PlayerEmbedHost = ReactiveControllerHost & PlayerEmbed;
14
11
 
15
12
  type TrackingEvent = 'VOUCH_LOADED' | 'VOUCH_RESPONSE_VIEWED' | 'VIDEO_PLAYED' | 'VIDEO_STREAMED';
16
13
  type TrackingPayload = {
@@ -20,6 +17,13 @@ type TrackingPayload = {
20
17
  streamEnd?: number;
21
18
  };
22
19
 
20
+ type BatchEvent = {
21
+ event: TrackingEvent;
22
+ payload: TrackingPayload & {
23
+ time: string;
24
+ };
25
+ };
26
+
23
27
  type TimeMap = {
24
28
  [key: string]: number;
25
29
  };
@@ -29,12 +33,9 @@ type BooleanMap = {
29
33
  };
30
34
 
31
35
  class TrackingController implements ReactiveController {
32
- host: EmbedHost;
33
-
34
- private _tabId: string | undefined = undefined;
35
- private _clientId: string | undefined = undefined;
36
- private _visitorId: string | undefined = undefined;
36
+ host: PlayerEmbedHost;
37
37
 
38
+ private _batchedEvents: BatchEvent[] = [];
38
39
  private _hasPlayed = false;
39
40
  private _hasLoaded: BooleanMap = {};
40
41
  private _answersViewed: BooleanMap = {};
@@ -42,126 +43,53 @@ class TrackingController implements ReactiveController {
42
43
  private _streamLatestTime: TimeMap = {};
43
44
  private _currentlyPlayingVideo: VideoEventDetail | null = null;
44
45
 
45
- constructor(host: EmbedHost) {
46
+ constructor(host: PlayerEmbedHost) {
46
47
  this.host = host;
47
48
  host.addController(this);
48
49
  }
49
50
 
50
- private _findVouchId(payload?: TrackingPayload) {
51
- if (payload && 'vouchId' in payload) {
52
- return payload.vouchId;
53
- }
54
- if (this.host.vouch) {
55
- return this.host.vouch.id;
56
- }
57
- return null;
58
- }
59
-
60
- private _createVisitor = (visitorId: string) => {
61
- const { publicApiUrl } = getEnvUrls(this.host.env);
62
- window.localStorage?.setItem?.('vouch-uid-visitor', visitorId);
63
- navigator.sendBeacon(`${publicApiUrl}/api/visitor`, JSON.stringify({ visitorId }));
64
- return visitorId;
65
- };
66
-
67
- private _getUids() {
68
- if (typeof window === 'undefined') {
69
- return {
70
- client: null,
71
- tab: null,
72
- request: uuidv4()
73
- };
74
- }
75
-
76
- // Persisted for a user for the same device + browser, so we can e.g. search for all logs related to that browser
77
- const visitorId =
78
- this._visitorId || window.localStorage?.getItem?.('vouch-uid-visitor') || this._createVisitor(uuidv4());
79
- // Persisted for a user for the same device + browser, so we can e.g. search for all logs related to that browser
80
- const clientId = this._clientId || window.localStorage?.getItem?.('vouch-uid-client') || uuidv4();
81
- // Persisted in session storage, so we can search for everything the user has done in a specific tab
82
- const tabId = this._tabId || window.sessionStorage?.getItem?.('vouch-uid-tab') || uuidv4();
83
- // Not persisted, allows us to search for any logs related to a single FE request
84
- // E.g. BE should pass this request ID through all other services to be able to group logs
85
- const requestId = uuidv4();
86
-
87
- // Cache and persist uids
88
- if (visitorId !== this._visitorId) {
89
- this._visitorId = visitorId;
90
- window.localStorage?.setItem?.('vouch-uid-visitor', visitorId);
91
- }
92
-
93
- if (clientId !== this._clientId) {
94
- this._clientId = clientId;
95
- window.localStorage?.setItem?.('vouch-uid-client', clientId);
96
- }
51
+ private _createTrackingEvent = (event: TrackingEvent, payload?: TrackingPayload) => {
52
+ const vouchId = findVouchId(payload, this.host.vouch);
97
53
 
98
- if (tabId !== this._tabId) {
99
- this._tabId = tabId;
100
- window.sessionStorage?.setItem?.('vouch-uid-tab', tabId);
54
+ if (!vouchId || this.host.disableTracking) {
55
+ return;
101
56
  }
102
57
 
103
- return {
104
- client: clientId,
105
- tab: tabId,
106
- request: requestId,
107
- visitor: visitorId
108
- };
109
- }
110
-
111
- private _getReportingMetadata = () => {
112
- const [country, region] = Intl.DateTimeFormat().resolvedOptions().timeZone?.split?.('/') ?? [];
113
-
114
- const utmParams: any = {};
115
- [...new URLSearchParams(location.search).entries()].forEach(([key, value]) => {
116
- if (/utm/.test(key)) {
117
- const param = key.toLowerCase().replace(/[-_][a-z0-9]/g, (group) => group.slice(-1).toUpperCase());
118
- utmParams[param] = value;
58
+ this._batchedEvents.push({
59
+ event,
60
+ payload: {
61
+ ...payload,
62
+ vouchId,
63
+ time: new Date().toISOString()
119
64
  }
120
65
  });
121
-
122
- return {
123
- source: this.host.trackingSource,
124
- time: new Date(),
125
- region,
126
- country,
127
- screenHeight: window.screen.height,
128
- screenWidth: window.screen.width,
129
- referrer: document.referrer,
130
- currentUrl: location.href,
131
- embedType: 'media-player-embed',
132
- embedVersion: packageJson.version,
133
- templateVersion: TEMPLATE_VERSION,
134
- ...utmParams
135
- };
136
66
  };
137
67
 
138
- private _sendTrackingEvent = (event: TrackingEvent, payload?: TrackingPayload) => {
139
- const vouchId = this._findVouchId(payload);
140
-
141
- if (!vouchId || this.host.disableTracking) {
68
+ private _sendTrackingEvent = () => {
69
+ if (this._batchedEvents.length <= 0) {
142
70
  return;
143
71
  }
144
72
 
145
73
  const { publicApiUrl } = getEnvUrls(this.host.env);
146
- const { client, tab, request, visitor } = this._getUids();
74
+ const { client, tab, request, visitor } = getUids(this.host.env);
147
75
 
148
76
  navigator.sendBeacon(
149
- `${publicApiUrl}/api/v2/events`,
77
+ `${publicApiUrl}/api/batchevents`,
150
78
  JSON.stringify({
151
- event,
152
79
  payload: {
153
- ...payload,
154
- vouchId
80
+ events: this._batchedEvents
155
81
  },
156
82
  context: {
157
83
  'x-uid-client': client,
158
84
  'x-uid-tab': tab,
159
85
  'x-uid-request': request,
160
86
  'x-uid-visitor': visitor,
161
- 'x-reporting-metadata': this._getReportingMetadata()
87
+ 'x-reporting-metadata': getReportingMetadata(this.host.trackingSource)
162
88
  }
163
89
  })
164
90
  );
91
+
92
+ this._batchedEvents = [];
165
93
  };
166
94
 
167
95
  private _streamEnded = () => {
@@ -171,7 +99,7 @@ class TrackingController implements ReactiveController {
171
99
  if (this._streamLatestTime[key] > this._streamStartTime[key] + MINIMUM_SEND_THRESHOLD) {
172
100
  // Send a video streamed event any time the stream ends to capture the time between starting
173
101
  // the video and the video stopping for any reason (pausing, deleting the embed node or closing the browser)
174
- this._sendTrackingEvent('VIDEO_STREAMED', {
102
+ this._createTrackingEvent('VIDEO_STREAMED', {
175
103
  answerId: id,
176
104
  streamStart: this._streamStartTime[key],
177
105
  streamEnd: this._streamLatestTime[key]
@@ -191,7 +119,7 @@ class TrackingController implements ReactiveController {
191
119
 
192
120
  // Only send loaded event once per session
193
121
  if (!this._hasLoaded[vouchId]) {
194
- this._sendTrackingEvent('VOUCH_LOADED', { vouchId });
122
+ this._createTrackingEvent('VOUCH_LOADED', { vouchId });
195
123
  this._hasLoaded[vouchId] = true;
196
124
  }
197
125
  };
@@ -199,26 +127,21 @@ class TrackingController implements ReactiveController {
199
127
  private _handlePlay = () => {
200
128
  // Only send the video played event once per session
201
129
  if (!this._hasPlayed) {
202
- this._sendTrackingEvent('VIDEO_PLAYED', {
130
+ this._createTrackingEvent('VIDEO_PLAYED', {
203
131
  streamStart: this.host.currentTime
204
132
  });
205
133
  this._hasPlayed = true;
206
134
  }
207
135
  };
208
136
 
209
- private _handleVideoPlay = ({ detail: { id, key, node } }: CustomEvent<VideoEventDetail>) => {
137
+ private _handleVideoPlay = ({ detail: { id, key } }: CustomEvent<VideoEventDetail>) => {
210
138
  // Only increment play count once per session
211
139
  if (!this._answersViewed[key]) {
212
- this._sendTrackingEvent('VOUCH_RESPONSE_VIEWED', {
140
+ this._createTrackingEvent('VOUCH_RESPONSE_VIEWED', {
213
141
  answerId: id
214
142
  });
215
143
  this._answersViewed[key] = true;
216
144
  }
217
-
218
- if (!this._streamStartTime[key]) {
219
- this._streamStartTime[key] = node.currentTime;
220
- this._streamLatestTime[key] = node.currentTime;
221
- }
222
145
  };
223
146
 
224
147
  private _handleVideoTimeUpdate = ({ detail: { id, key, node } }: CustomEvent<VideoEventDetail>) => {
@@ -230,12 +153,17 @@ class TrackingController implements ReactiveController {
230
153
  ) {
231
154
  this._currentlyPlayingVideo = { id, key, node };
232
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
+ }
233
161
  }
234
162
  };
235
163
 
236
164
  private _handleVideoPause = ({ detail: { id, key } }: CustomEvent<VideoEventDetail>) => {
237
165
  if (this._streamLatestTime[key] > this._streamStartTime[key] + MINIMUM_SEND_THRESHOLD) {
238
- this._sendTrackingEvent('VIDEO_STREAMED', {
166
+ this._createTrackingEvent('VIDEO_STREAMED', {
239
167
  answerId: id,
240
168
  streamStart: this._streamStartTime[key],
241
169
  streamEnd: this._streamLatestTime[key]
@@ -247,9 +175,7 @@ class TrackingController implements ReactiveController {
247
175
 
248
176
  private _pageUnloading = () => {
249
177
  this._streamEnded();
250
- // This will try to send the same stream event again so we delete the start and latest
251
- // time in stream ended so that there is no times to send and the pause event does nothing
252
- this.host.pause();
178
+ this._sendTrackingEvent();
253
179
  };
254
180
 
255
181
  private _handleVisibilityChange = () => {
@@ -278,7 +204,9 @@ class TrackingController implements ReactiveController {
278
204
  }
279
205
 
280
206
  hostDisconnected() {
281
- this._streamEnded();
207
+ // Send events if DOM node is destroyed
208
+ this._pageUnloading();
209
+
282
210
  if ('onvisibilitychange' in document) {
283
211
  document.removeEventListener('visibilitychange', this._handleVisibilityChange);
284
212
  } else {
@@ -293,3 +221,4 @@ class TrackingController implements ReactiveController {
293
221
  }
294
222
 
295
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 };
@@ -5,7 +5,6 @@ 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,10 @@ 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<
17
+ MediaPlayerProps,
18
+ 'data' | 'aspectRatio' | 'language' | 'preload' | 'autoplay' | 'controls'
19
+ > & {
18
20
  env: Environment;
19
21
  apiKey: string;
20
22
  disableTracking?: boolean;
@@ -25,8 +27,8 @@ type EmbedProps = Pick<MediaPlayerProps, 'data' | 'aspectRatio' | 'preload' | 'a
25
27
  questions?: number[];
26
28
  };
27
29
 
28
- @customElement('vouch-embed')
29
- class Embed extends LitElement {
30
+ @customElement('vouch-embed-player')
31
+ class PlayerEmbed extends LitElement {
30
32
  static styles = [
31
33
  css`
32
34
  :host {
@@ -35,22 +37,21 @@ class Embed extends LitElement {
35
37
  `
36
38
  ];
37
39
 
38
- private _mediaPlayerRef: Ref<MediaPlayer> = createRef();
40
+ @property({ type: Object }) data: PlayerEmbedProps['data'];
41
+ @property({ type: String }) vouchId: PlayerEmbedProps['vouchId'];
42
+ @property({ type: String }) templateId: PlayerEmbedProps['templateId'];
43
+ @property({ type: Array }) questions: PlayerEmbedProps['questions'];
39
44
 
40
- @property({ type: Object }) data: EmbedProps['data'];
41
- @property({ type: String }) vouchId: EmbedProps['vouchId'];
42
- @property({ type: String }) templateId: EmbedProps['templateId'];
43
- @property({ type: Array }) questions: EmbedProps['questions'];
45
+ @property({ type: String }) env: PlayerEmbedProps['env'] = 'prod';
46
+ @property({ type: String }) apiKey: PlayerEmbedProps['apiKey'] = '';
47
+ @property({ type: Boolean }) disableTracking: PlayerEmbedProps['disableTracking'] = false;
48
+ @property({ type: String }) trackingSource: PlayerEmbedProps['trackingSource'] = 'embedded_player';
44
49
 
45
- @property({ type: String }) env: EmbedProps['env'] = 'prod';
46
- @property({ type: String }) apiKey: EmbedProps['apiKey'] = '';
47
- @property({ type: Boolean }) disableTracking: EmbedProps['disableTracking'] = false;
48
- @property({ type: String }) trackingSource: EmbedProps['trackingSource'] = 'embedded_player';
49
-
50
- @property({ type: Array }) controls: EmbedProps['controls'];
51
- @property({ type: String }) preload: EmbedProps['preload'] = 'auto';
52
- @property({ type: Boolean }) autoplay: EmbedProps['autoplay'] = false;
53
- @property({ type: Number }) aspectRatio: EmbedProps['aspectRatio'] = 0;
50
+ @property({ type: Array }) controls: PlayerEmbedProps['controls'];
51
+ @property({ type: String }) preload: PlayerEmbedProps['preload'] = 'auto';
52
+ @property({ type: Boolean }) autoplay: PlayerEmbedProps['autoplay'] = false;
53
+ @property({ type: Number }) aspectRatio: PlayerEmbedProps['aspectRatio'] = 0;
54
+ @property({ type: String }) language?: MediaPlayerProps['language'];
54
55
 
55
56
  private eventController = new EventForwardController(this, [
56
57
  'durationchange',
@@ -63,6 +64,7 @@ class Embed extends LitElement {
63
64
  'playing',
64
65
  'ratechange',
65
66
  'scenechange',
67
+ 'scenesupdate',
66
68
  'seeking',
67
69
  'seeked',
68
70
  'timeupdate',
@@ -85,17 +87,23 @@ class Embed extends LitElement {
85
87
  // @ts-ignore
86
88
  private _trackingController = new TrackingController(this);
87
89
 
88
- @state() vouch: EmbedProps['data'];
90
+ @state() vouch: PlayerEmbedProps['data'];
89
91
  @state() template: TemplateInstance | undefined;
90
92
 
91
93
  get fetching() {
92
94
  return this._fetcherController.fetching;
93
95
  }
94
96
 
97
+ private _mediaPlayerRef = createRef<MediaPlayer>();
98
+
95
99
  get waiting() {
96
100
  return this._mediaPlayerRef.value?.waiting;
97
101
  }
98
102
 
103
+ get initialised() {
104
+ return this._mediaPlayerRef.value?.initialised;
105
+ }
106
+
99
107
  get seeking() {
100
108
  return this._mediaPlayerRef.value?.seeking;
101
109
  }
@@ -180,12 +188,42 @@ class Embed extends LitElement {
180
188
  this._mediaPlayerRef.value?.pause();
181
189
  }
182
190
 
191
+ reset(time = 0, play = false) {
192
+ this._mediaPlayerRef.value?.reset(time, play);
193
+ }
194
+
183
195
  setScene(index: number) {
184
196
  this._mediaPlayerRef.value?.setScene(index);
185
197
  }
186
198
 
199
+ private _renderStyles() {
200
+ if (!this.aspectRatio) {
201
+ return html`
202
+ <style>
203
+ :host {
204
+ width: 100%;
205
+ height: 100%;
206
+ }
207
+ </style>
208
+ `;
209
+ }
210
+
211
+ if (typeof this.aspectRatio === 'number') {
212
+ return html`
213
+ <style>
214
+ :host {
215
+ aspect-ratio: ${this.aspectRatio};
216
+ }
217
+ </style>
218
+ `;
219
+ }
220
+
221
+ return null;
222
+ }
223
+
187
224
  render() {
188
225
  return html`
226
+ ${this._renderStyles()}
189
227
  <vmp-media-player
190
228
  ${ref(this._mediaPlayerRef)}
191
229
  ${this.eventController.register()}
@@ -195,6 +233,7 @@ class Embed extends LitElement {
195
233
  .template=${this.template}
196
234
  aspectRatio=${ifDefined(this.aspectRatio)}
197
235
  preload=${ifDefined(this.preload)}
236
+ language=${ifDefined(this.language)}
198
237
  .controls=${this.controls}
199
238
  ></vmp-media-player>
200
239
  `;
@@ -203,15 +242,15 @@ class Embed extends LitElement {
203
242
 
204
243
  declare global {
205
244
  interface HTMLElementTagNameMap {
206
- 'vouch-embed': Embed;
245
+ 'vouch-embed-player': PlayerEmbed;
207
246
  }
208
247
 
209
248
  namespace JSX {
210
249
  interface IntrinsicElements {
211
- 'vouch-embed': Embed;
250
+ 'vouch-embed-player': PlayerEmbed;
212
251
  }
213
252
  }
214
253
  }
215
254
 
216
- export { Embed };
217
- export type { EmbedProps };
255
+ export { PlayerEmbed };
256
+ 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
+ });