ts-video-player 0.1.0 → 0.1.2

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 (176) hide show
  1. package/README.md +1 -1
  2. package/dist/cdn.d.ts +0 -1
  3. package/dist/cdn.min.js +76 -78
  4. package/dist/cdn.test.d.ts +0 -1
  5. package/dist/{chunk-eajts2jv.js → chunk-d352kdh6.js} +215 -193
  6. package/dist/{chunk-z1ehbz9f.js → chunk-ds1nj487.js} +35 -37
  7. package/dist/{chunk-36x4yhjn.js → chunk-rhykj0qy.js} +58 -60
  8. package/dist/config.d.ts +0 -1
  9. package/dist/core/audio-gain.d.ts +0 -1
  10. package/dist/core/caption-styles.d.ts +0 -1
  11. package/dist/core/chapters.d.ts +0 -1
  12. package/dist/core/error-overlay.d.ts +0 -1
  13. package/dist/core/events.d.ts +0 -1
  14. package/dist/core/features.d.ts +0 -1
  15. package/dist/core/i18n.d.ts +0 -1
  16. package/dist/core/index.d.ts +0 -1
  17. package/dist/core/live.d.ts +0 -1
  18. package/dist/core/media-session.d.ts +0 -1
  19. package/dist/core/orientation.d.ts +0 -1
  20. package/dist/core/popover.d.ts +0 -1
  21. package/dist/core/remote-playback.d.ts +0 -1
  22. package/dist/core/settings-menu.d.ts +0 -1
  23. package/dist/core/spinner.d.ts +0 -1
  24. package/dist/core/state.d.ts +0 -1
  25. package/dist/core/text-tracks.d.ts +0 -1
  26. package/dist/core/themes.d.ts +0 -1
  27. package/dist/core/thumbnails.d.ts +0 -1
  28. package/dist/core/tooltips.d.ts +0 -1
  29. package/dist/elements/base.d.ts +0 -1
  30. package/dist/elements/index.d.ts +0 -1
  31. package/dist/elements/index.js +49 -52
  32. package/dist/elements/media-airplay-button.d.ts +0 -1
  33. package/dist/elements/media-error-display.d.ts +0 -1
  34. package/dist/elements/media-fullscreen-button.d.ts +0 -1
  35. package/dist/elements/media-live-button.d.ts +0 -1
  36. package/dist/elements/media-mute-button.d.ts +0 -1
  37. package/dist/elements/media-pip-button.d.ts +0 -1
  38. package/dist/elements/media-play-button.d.ts +0 -1
  39. package/dist/elements/media-playback-rate-button.d.ts +0 -1
  40. package/dist/elements/media-preview-time.d.ts +0 -1
  41. package/dist/elements/media-progress-bar.d.ts +0 -1
  42. package/dist/elements/media-remote-playback-button.d.ts +0 -1
  43. package/dist/elements/media-seek-button.d.ts +0 -1
  44. package/dist/elements/media-settings-menu.d.ts +0 -1
  45. package/dist/elements/media-time-display.d.ts +0 -1
  46. package/dist/elements/media-time-group.d.ts +0 -1
  47. package/dist/elements/media-time-separator.d.ts +0 -1
  48. package/dist/elements/media-volume-slider.d.ts +0 -1
  49. package/dist/elements/utils.d.ts +0 -1
  50. package/dist/elements/video-player.d.ts +0 -1
  51. package/dist/elements/video-skin.d.ts +0 -1
  52. package/dist/features/captions.d.ts +0 -1
  53. package/dist/features/create-player.d.ts +0 -1
  54. package/dist/features/fullscreen.d.ts +0 -1
  55. package/dist/features/gestures.d.ts +0 -1
  56. package/dist/features/index.d.ts +0 -1
  57. package/dist/features/index.js +25 -28
  58. package/dist/features/keyboard.d.ts +0 -1
  59. package/dist/features/pip.d.ts +0 -1
  60. package/dist/features/playback.d.ts +0 -1
  61. package/dist/features/quality.d.ts +0 -1
  62. package/dist/features/types.d.ts +0 -1
  63. package/dist/features/volume.d.ts +0 -1
  64. package/dist/index.d.ts +0 -1
  65. package/dist/index.js +368 -370
  66. package/dist/layouts/index.d.ts +0 -1
  67. package/dist/player.d.ts +0 -1
  68. package/dist/plugins/ads.d.ts +0 -1
  69. package/dist/plugins/analytics.d.ts +0 -1
  70. package/dist/plugins/end-screen.d.ts +0 -1
  71. package/dist/plugins/index.d.ts +0 -1
  72. package/dist/plugins/index.js +2143 -17
  73. package/dist/plugins/skip-segments.d.ts +0 -1
  74. package/dist/plugins/watermark.d.ts +0 -1
  75. package/dist/providers/base.d.ts +0 -1
  76. package/dist/providers/dash.d.ts +0 -1
  77. package/dist/providers/hls.d.ts +0 -1
  78. package/dist/providers/html5.d.ts +4 -1
  79. package/dist/providers/index.d.ts +0 -1
  80. package/dist/providers/vimeo.d.ts +0 -1
  81. package/dist/providers/youtube.d.ts +0 -1
  82. package/dist/stx/index.d.ts +0 -1
  83. package/dist/stx/index.js +4 -6
  84. package/dist/types.d.ts +0 -1
  85. package/dist/ui/components.d.ts +0 -1
  86. package/dist/ui/index.d.ts +0 -1
  87. package/dist/ui/index.js +730 -13
  88. package/package.json +3 -5
  89. package/src/providers/html5.ts +1 -1
  90. package/src/providers/index.ts +36 -2
  91. package/dist/cdn.d.ts.map +0 -1
  92. package/dist/cdn.min.js.map +0 -54
  93. package/dist/cdn.test.d.ts.map +0 -1
  94. package/dist/chunk-36x4yhjn.js.map +0 -12
  95. package/dist/chunk-eajts2jv.js.map +0 -42
  96. package/dist/chunk-z1ehbz9f.js.map +0 -19
  97. package/dist/config.d.ts.map +0 -1
  98. package/dist/core/audio-gain.d.ts.map +0 -1
  99. package/dist/core/caption-styles.d.ts.map +0 -1
  100. package/dist/core/chapters.d.ts.map +0 -1
  101. package/dist/core/error-overlay.d.ts.map +0 -1
  102. package/dist/core/events.d.ts.map +0 -1
  103. package/dist/core/features.d.ts.map +0 -1
  104. package/dist/core/i18n.d.ts.map +0 -1
  105. package/dist/core/index.d.ts.map +0 -1
  106. package/dist/core/live.d.ts.map +0 -1
  107. package/dist/core/media-session.d.ts.map +0 -1
  108. package/dist/core/orientation.d.ts.map +0 -1
  109. package/dist/core/popover.d.ts.map +0 -1
  110. package/dist/core/remote-playback.d.ts.map +0 -1
  111. package/dist/core/settings-menu.d.ts.map +0 -1
  112. package/dist/core/spinner.d.ts.map +0 -1
  113. package/dist/core/state.d.ts.map +0 -1
  114. package/dist/core/text-tracks.d.ts.map +0 -1
  115. package/dist/core/themes.d.ts.map +0 -1
  116. package/dist/core/thumbnails.d.ts.map +0 -1
  117. package/dist/core/tooltips.d.ts.map +0 -1
  118. package/dist/elements/base.d.ts.map +0 -1
  119. package/dist/elements/index.d.ts.map +0 -1
  120. package/dist/elements/index.js.map +0 -9
  121. package/dist/elements/media-airplay-button.d.ts.map +0 -1
  122. package/dist/elements/media-error-display.d.ts.map +0 -1
  123. package/dist/elements/media-fullscreen-button.d.ts.map +0 -1
  124. package/dist/elements/media-live-button.d.ts.map +0 -1
  125. package/dist/elements/media-mute-button.d.ts.map +0 -1
  126. package/dist/elements/media-pip-button.d.ts.map +0 -1
  127. package/dist/elements/media-play-button.d.ts.map +0 -1
  128. package/dist/elements/media-playback-rate-button.d.ts.map +0 -1
  129. package/dist/elements/media-preview-time.d.ts.map +0 -1
  130. package/dist/elements/media-progress-bar.d.ts.map +0 -1
  131. package/dist/elements/media-remote-playback-button.d.ts.map +0 -1
  132. package/dist/elements/media-seek-button.d.ts.map +0 -1
  133. package/dist/elements/media-settings-menu.d.ts.map +0 -1
  134. package/dist/elements/media-time-display.d.ts.map +0 -1
  135. package/dist/elements/media-time-group.d.ts.map +0 -1
  136. package/dist/elements/media-time-separator.d.ts.map +0 -1
  137. package/dist/elements/media-volume-slider.d.ts.map +0 -1
  138. package/dist/elements/utils.d.ts.map +0 -1
  139. package/dist/elements/video-player.d.ts.map +0 -1
  140. package/dist/elements/video-skin.d.ts.map +0 -1
  141. package/dist/features/captions.d.ts.map +0 -1
  142. package/dist/features/create-player.d.ts.map +0 -1
  143. package/dist/features/fullscreen.d.ts.map +0 -1
  144. package/dist/features/gestures.d.ts.map +0 -1
  145. package/dist/features/index.d.ts.map +0 -1
  146. package/dist/features/index.js.map +0 -9
  147. package/dist/features/keyboard.d.ts.map +0 -1
  148. package/dist/features/pip.d.ts.map +0 -1
  149. package/dist/features/playback.d.ts.map +0 -1
  150. package/dist/features/quality.d.ts.map +0 -1
  151. package/dist/features/types.d.ts.map +0 -1
  152. package/dist/features/volume.d.ts.map +0 -1
  153. package/dist/index.d.ts.map +0 -1
  154. package/dist/index.js.map +0 -31
  155. package/dist/layouts/index.d.ts.map +0 -1
  156. package/dist/player.d.ts.map +0 -1
  157. package/dist/plugins/ads.d.ts.map +0 -1
  158. package/dist/plugins/analytics.d.ts.map +0 -1
  159. package/dist/plugins/end-screen.d.ts.map +0 -1
  160. package/dist/plugins/index.d.ts.map +0 -1
  161. package/dist/plugins/index.js.map +0 -9
  162. package/dist/plugins/skip-segments.d.ts.map +0 -1
  163. package/dist/plugins/watermark.d.ts.map +0 -1
  164. package/dist/providers/base.d.ts.map +0 -1
  165. package/dist/providers/dash.d.ts.map +0 -1
  166. package/dist/providers/hls.d.ts.map +0 -1
  167. package/dist/providers/html5.d.ts.map +0 -1
  168. package/dist/providers/index.d.ts.map +0 -1
  169. package/dist/providers/vimeo.d.ts.map +0 -1
  170. package/dist/providers/youtube.d.ts.map +0 -1
  171. package/dist/stx/index.d.ts.map +0 -1
  172. package/dist/stx/index.js.map +0 -11
  173. package/dist/types.d.ts.map +0 -1
  174. package/dist/ui/components.d.ts.map +0 -1
  175. package/dist/ui/index.d.ts.map +0 -1
  176. package/dist/ui/index.js.map +0 -9
@@ -1,20 +1,2146 @@
1
+ // src/plugins/analytics.ts
2
+ var DEFAULT_MILESTONES = [10, 25, 50, 75, 90, 100];
3
+ var DEFAULT_HEARTBEAT_INTERVAL = 30;
4
+
5
+ class VideoAnalytics {
6
+ player = null;
7
+ config;
8
+ state;
9
+ heartbeatTimer = null;
10
+ eventQueue = [];
11
+ flushTimer = null;
12
+ sessionId;
13
+ visitorId;
14
+ unsubscribers = [];
15
+ constructor(config) {
16
+ this.config = {
17
+ milestones: DEFAULT_MILESTONES,
18
+ heartbeatInterval: DEFAULT_HEARTBEAT_INTERVAL,
19
+ trackQualityChanges: true,
20
+ trackSeeking: true,
21
+ trackBuffering: true,
22
+ trackErrors: true,
23
+ trackEngagement: true,
24
+ ...config
25
+ };
26
+ this.sessionId = config.sessionId || this.generateId();
27
+ this.visitorId = config.visitorId || this.getOrCreateVisitorId();
28
+ this.state = this.createInitialState();
29
+ }
30
+ createInitialState() {
31
+ return {
32
+ hasStarted: false,
33
+ hasCompleted: false,
34
+ watchTime: 0,
35
+ pauseCount: 0,
36
+ seekCount: 0,
37
+ bufferCount: 0,
38
+ bufferTime: 0,
39
+ qualityChangeCount: 0,
40
+ reachedMilestones: [],
41
+ reachedQuartiles: [],
42
+ playbackStartTime: null,
43
+ lastHeartbeat: 0,
44
+ isBuffering: false,
45
+ bufferStartTime: null
46
+ };
47
+ }
48
+ attach(player) {
49
+ if (this.config.disabled)
50
+ return;
51
+ this.player = player;
52
+ this.attachEventListeners();
53
+ this.startHeartbeat();
54
+ }
55
+ detach() {
56
+ this.stopHeartbeat();
57
+ this.unsubscribers.forEach((unsub) => unsub());
58
+ this.unsubscribers = [];
59
+ this.flush();
60
+ this.player = null;
61
+ }
62
+ attachEventListeners() {
63
+ if (!this.player)
64
+ return;
65
+ this.on("play", () => {
66
+ if (!this.state.hasStarted) {
67
+ this.state.hasStarted = true;
68
+ this.state.playbackStartTime = Date.now();
69
+ this.trackEvent("video_start");
70
+ } else {
71
+ this.trackEvent("video_resume");
72
+ }
73
+ this.trackEvent("video_play");
74
+ });
75
+ this.on("pause", () => {
76
+ this.state.pauseCount++;
77
+ this.updateWatchTime();
78
+ this.trackEvent("video_pause");
79
+ });
80
+ if (this.config.trackSeeking) {
81
+ this.on("seeking", () => {
82
+ this.state.seekCount++;
83
+ });
84
+ this.on("seeked", () => {
85
+ const playerState = this.player?.state;
86
+ this.trackEvent("video_seek", {
87
+ seekTo: playerState?.currentTime || 0
88
+ });
89
+ });
90
+ }
91
+ if (this.config.trackBuffering) {
92
+ this.on("waiting", () => {
93
+ if (!this.state.isBuffering) {
94
+ this.state.isBuffering = true;
95
+ this.state.bufferStartTime = Date.now();
96
+ this.state.bufferCount++;
97
+ this.trackEvent("video_buffer_start");
98
+ }
99
+ });
100
+ this.on("playing", () => {
101
+ if (this.state.isBuffering) {
102
+ this.state.isBuffering = false;
103
+ if (this.state.bufferStartTime) {
104
+ this.state.bufferTime += (Date.now() - this.state.bufferStartTime) / 1000;
105
+ }
106
+ this.trackEvent("video_buffer_end", {
107
+ bufferDuration: this.state.bufferStartTime ? (Date.now() - this.state.bufferStartTime) / 1000 : 0
108
+ });
109
+ this.state.bufferStartTime = null;
110
+ }
111
+ });
112
+ }
113
+ this.on("timeupdate", () => {
114
+ this.checkMilestones();
115
+ });
116
+ if (this.config.trackQualityChanges) {
117
+ this.on("qualitychange", (quality) => {
118
+ this.state.qualityChangeCount++;
119
+ this.trackEvent("video_quality_change", {
120
+ quality: quality ? `${quality.height}p` : "auto",
121
+ bitrate: quality?.bitrate
122
+ });
123
+ });
124
+ }
125
+ this.on("volumechange", () => {
126
+ const playerState = this.player?.state;
127
+ if (playerState?.muted) {
128
+ this.trackEvent("video_mute");
129
+ } else {
130
+ this.trackEvent("video_volume_change", {
131
+ volume: playerState?.volume
132
+ });
133
+ }
134
+ });
135
+ this.on("fullscreenchange", (isFullscreen) => {
136
+ this.trackEvent(isFullscreen ? "video_fullscreen_enter" : "video_fullscreen_exit");
137
+ });
138
+ this.on("pipchange", (isPiP) => {
139
+ this.trackEvent(isPiP ? "video_pip_enter" : "video_pip_exit");
140
+ });
141
+ this.on("texttrackchange", (track) => {
142
+ if (track) {
143
+ this.trackEvent("video_caption_change", {
144
+ language: track.language,
145
+ label: track.label
146
+ });
147
+ } else {
148
+ this.trackEvent("video_caption_disable");
149
+ }
150
+ });
151
+ this.on("audiotrackchange", (track) => {
152
+ if (track) {
153
+ this.trackEvent("video_audio_track_change", {
154
+ language: track.language,
155
+ label: track.label
156
+ });
157
+ }
158
+ });
159
+ this.on("ratechange", (rate) => {
160
+ this.trackEvent("video_speed_change", { speed: rate });
161
+ });
162
+ this.on("ended", () => {
163
+ if (!this.state.hasCompleted) {
164
+ this.state.hasCompleted = true;
165
+ this.updateWatchTime();
166
+ this.trackEvent("video_complete", {
167
+ watchTime: this.state.watchTime,
168
+ pauseCount: this.state.pauseCount,
169
+ seekCount: this.state.seekCount,
170
+ bufferCount: this.state.bufferCount,
171
+ bufferTime: this.state.bufferTime
172
+ });
173
+ }
174
+ });
175
+ if (this.config.trackErrors) {
176
+ this.on("error", (error) => {
177
+ this.trackEvent("video_error", {
178
+ code: error?.code,
179
+ message: error?.message
180
+ });
181
+ });
182
+ }
183
+ }
184
+ on(event, handler) {
185
+ if (!this.player)
186
+ return;
187
+ this.player.on(event, handler);
188
+ this.unsubscribers.push(() => this.player?.off(event, handler));
189
+ }
190
+ checkMilestones() {
191
+ const playerState = this.player?.state;
192
+ if (!playerState || !playerState.duration)
193
+ return;
194
+ const progress = playerState.currentTime / playerState.duration * 100;
195
+ for (const milestone of this.config.milestones || []) {
196
+ if (progress >= milestone && !this.state.reachedMilestones.includes(milestone)) {
197
+ this.state.reachedMilestones.push(milestone);
198
+ this.trackEvent("video_milestone", { milestone });
199
+ }
200
+ }
201
+ const quartiles = [25, 50, 75, 100];
202
+ for (const quartile of quartiles) {
203
+ if (progress >= quartile && !this.state.reachedQuartiles.includes(quartile)) {
204
+ this.state.reachedQuartiles.push(quartile);
205
+ this.trackEvent("video_quartile", { quartile });
206
+ }
207
+ }
208
+ }
209
+ updateWatchTime() {
210
+ if (this.state.playbackStartTime) {
211
+ this.state.watchTime += (Date.now() - this.state.playbackStartTime) / 1000;
212
+ this.state.playbackStartTime = Date.now();
213
+ }
214
+ }
215
+ startHeartbeat() {
216
+ if (this.heartbeatTimer)
217
+ return;
218
+ const interval = (this.config.heartbeatInterval || DEFAULT_HEARTBEAT_INTERVAL) * 1000;
219
+ this.heartbeatTimer = setInterval(() => {
220
+ const playerState = this.player?.state;
221
+ if (playerState?.playbackState === "playing") {
222
+ this.updateWatchTime();
223
+ this.trackEvent("video_heartbeat", {
224
+ watchTime: this.state.watchTime,
225
+ currentTime: playerState.currentTime
226
+ });
227
+ }
228
+ }, interval);
229
+ }
230
+ stopHeartbeat() {
231
+ if (this.heartbeatTimer) {
232
+ clearInterval(this.heartbeatTimer);
233
+ this.heartbeatTimer = null;
234
+ }
235
+ }
236
+ trackEvent(name, properties) {
237
+ if (this.config.disabled)
238
+ return;
239
+ const playerState = this.player?.state;
240
+ const event = {
241
+ name,
242
+ timestamp: new Date,
243
+ currentTime: playerState?.currentTime || 0,
244
+ duration: playerState?.duration || this.config.duration || 0,
245
+ properties
246
+ };
247
+ this.eventQueue.push(event);
248
+ this.scheduleFlush();
249
+ }
250
+ scheduleFlush() {
251
+ if (this.flushTimer)
252
+ return;
253
+ this.flushTimer = setTimeout(() => {
254
+ this.flush();
255
+ this.flushTimer = null;
256
+ }, 1000);
257
+ }
258
+ async flush() {
259
+ if (this.eventQueue.length === 0)
260
+ return;
261
+ const events = [...this.eventQueue];
262
+ this.eventQueue = [];
263
+ try {
264
+ await this.sendEvents(events);
265
+ } catch (error) {
266
+ console.error("Failed to send analytics events:", error);
267
+ this.eventQueue.unshift(...events);
268
+ }
269
+ }
270
+ async sendEvents(events) {
271
+ const payload = events.map((event) => this.buildEventPayload(event));
272
+ for (const p of payload) {
273
+ await fetch(this.config.apiEndpoint, {
274
+ method: "POST",
275
+ headers: {
276
+ "Content-Type": "application/json"
277
+ },
278
+ body: JSON.stringify(p),
279
+ keepalive: true
280
+ });
281
+ }
282
+ }
283
+ buildEventPayload(event) {
284
+ return {
285
+ s: this.config.siteId,
286
+ sid: this.sessionId,
287
+ vid: this.visitorId,
288
+ e: "event",
289
+ u: typeof window !== "undefined" ? window.location.href : "",
290
+ p: {
291
+ eventName: event.name,
292
+ videoId: this.config.videoId,
293
+ videoTitle: this.config.videoTitle,
294
+ contentType: this.config.contentType,
295
+ category: this.config.category,
296
+ creatorId: this.config.creatorId,
297
+ seriesId: this.config.seriesId,
298
+ episode: this.config.episode,
299
+ season: this.config.season,
300
+ currentTime: event.currentTime,
301
+ duration: event.duration,
302
+ progress: event.duration > 0 ? event.currentTime / event.duration * 100 : 0,
303
+ watchTime: this.state.watchTime,
304
+ ...this.config.customProperties,
305
+ ...event.properties
306
+ }
307
+ };
308
+ }
309
+ generateId() {
310
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
311
+ }
312
+ getOrCreateVisitorId() {
313
+ if (typeof localStorage === "undefined") {
314
+ return this.generateId();
315
+ }
316
+ const key = "tsvp_visitor_id";
317
+ let visitorId = localStorage.getItem(key);
318
+ if (!visitorId) {
319
+ visitorId = this.generateId();
320
+ localStorage.setItem(key, visitorId);
321
+ }
322
+ return visitorId;
323
+ }
324
+ getState() {
325
+ return { ...this.state };
326
+ }
327
+ getSummary() {
328
+ return {
329
+ videoId: this.config.videoId,
330
+ sessionId: this.sessionId,
331
+ hasStarted: this.state.hasStarted,
332
+ hasCompleted: this.state.hasCompleted,
333
+ watchTime: this.state.watchTime,
334
+ pauseCount: this.state.pauseCount,
335
+ seekCount: this.state.seekCount,
336
+ bufferCount: this.state.bufferCount,
337
+ bufferTime: this.state.bufferTime,
338
+ qualityChangeCount: this.state.qualityChangeCount,
339
+ reachedMilestones: this.state.reachedMilestones,
340
+ completionRate: this.state.reachedQuartiles.length > 0 ? Math.max(...this.state.reachedQuartiles) : 0
341
+ };
342
+ }
343
+ reset() {
344
+ this.flush();
345
+ this.state = this.createInitialState();
346
+ this.sessionId = this.generateId();
347
+ }
348
+ destroy() {
349
+ this.detach();
350
+ if (this.flushTimer) {
351
+ clearTimeout(this.flushTimer);
352
+ }
353
+ }
354
+ }
355
+ function createVideoAnalytics(config) {
356
+ return new VideoAnalytics(config);
357
+ }
358
+ function analyticsPlugin(config) {
359
+ return {
360
+ name: "analytics",
361
+ install(player, videoConfig) {
362
+ const analytics = createVideoAnalytics({
363
+ ...config,
364
+ ...videoConfig
365
+ });
366
+ analytics.attach(player);
367
+ player.analytics = analytics;
368
+ return () => {
369
+ analytics.destroy();
370
+ delete player.analytics;
371
+ };
372
+ }
373
+ };
374
+ }
375
+ // src/plugins/ads.ts
376
+ class VASTParser {
377
+ maxRedirects;
378
+ timeout;
379
+ redirectCount = 0;
380
+ constructor(options = {}) {
381
+ this.maxRedirects = options.maxRedirects ?? 5;
382
+ this.timeout = options.timeout ?? 1e4;
383
+ }
384
+ async parseFromUrl(url) {
385
+ this.redirectCount = 0;
386
+ return this.fetchAndParse(url);
387
+ }
388
+ parseFromXml(xml) {
389
+ const parser = new DOMParser;
390
+ const doc = parser.parseFromString(xml, "text/xml");
391
+ return this.parseDocument(doc);
392
+ }
393
+ async fetchAndParse(url) {
394
+ if (this.redirectCount >= this.maxRedirects) {
395
+ throw new Error("Too many VAST redirects");
396
+ }
397
+ const controller = new AbortController;
398
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
399
+ try {
400
+ const response = await fetch(url, { signal: controller.signal });
401
+ const text = await response.text();
402
+ const parser = new DOMParser;
403
+ const doc = parser.parseFromString(text, "text/xml");
404
+ const wrapper = doc.querySelector("Wrapper");
405
+ if (wrapper) {
406
+ const vastAdTagUri = wrapper.querySelector("VASTAdTagURI")?.textContent?.trim();
407
+ if (vastAdTagUri) {
408
+ this.redirectCount++;
409
+ return this.fetchAndParse(vastAdTagUri);
410
+ }
411
+ }
412
+ return this.parseDocument(doc);
413
+ } finally {
414
+ clearTimeout(timeoutId);
415
+ }
416
+ }
417
+ parseDocument(doc) {
418
+ const ads = [];
419
+ const adElements = doc.querySelectorAll("Ad");
420
+ adElements.forEach((adEl) => {
421
+ const inLine = adEl.querySelector("InLine");
422
+ if (!inLine)
423
+ return;
424
+ const ad = {
425
+ id: adEl.getAttribute("id") || this.generateId(),
426
+ type: "linear",
427
+ adSystem: inLine.querySelector("AdSystem")?.textContent?.trim(),
428
+ title: inLine.querySelector("AdTitle")?.textContent?.trim(),
429
+ description: inLine.querySelector("Description")?.textContent?.trim(),
430
+ duration: 0,
431
+ mediaFiles: [],
432
+ trackingEvents: []
433
+ };
434
+ const linear = inLine.querySelector("Linear");
435
+ if (linear) {
436
+ const durationStr = linear.querySelector("Duration")?.textContent?.trim();
437
+ if (durationStr) {
438
+ ad.duration = this.parseDuration(durationStr);
439
+ }
440
+ const skipOffset = linear.getAttribute("skipoffset");
441
+ if (skipOffset) {
442
+ ad.skipOffset = this.parseOffset(skipOffset, ad.duration);
443
+ }
444
+ linear.querySelectorAll("MediaFile").forEach((mf) => {
445
+ const url = mf.textContent?.trim();
446
+ if (url) {
447
+ ad.mediaFiles.push({
448
+ url,
449
+ type: mf.getAttribute("type") || "video/mp4",
450
+ width: Number.parseInt(mf.getAttribute("width") || "0", 10),
451
+ height: Number.parseInt(mf.getAttribute("height") || "0", 10),
452
+ bitrate: mf.getAttribute("bitrate") ? Number.parseInt(mf.getAttribute("bitrate"), 10) : undefined,
453
+ delivery: mf.getAttribute("delivery") || "progressive"
454
+ });
455
+ }
456
+ });
457
+ const clickThrough = linear.querySelector("VideoClicks > ClickThrough");
458
+ if (clickThrough) {
459
+ ad.clickThroughUrl = clickThrough.textContent?.trim();
460
+ }
461
+ linear.querySelectorAll("TrackingEvents > Tracking").forEach((tracking) => {
462
+ const event = tracking.getAttribute("event");
463
+ const url = tracking.textContent?.trim();
464
+ if (event && url) {
465
+ const trackingEvent = { event, url };
466
+ if (event === "progress") {
467
+ const offset = tracking.getAttribute("offset");
468
+ if (offset) {
469
+ trackingEvent.offset = this.parseOffset(offset, ad.duration);
470
+ }
471
+ }
472
+ ad.trackingEvents.push(trackingEvent);
473
+ }
474
+ });
475
+ linear.querySelectorAll("Icons > Icon").forEach((icon) => {
476
+ const staticResource = icon.querySelector("StaticResource");
477
+ if (staticResource?.textContent) {
478
+ ad.icons = ad.icons || [];
479
+ ad.icons.push({
480
+ url: staticResource.textContent.trim(),
481
+ width: Number.parseInt(icon.getAttribute("width") || "0", 10),
482
+ height: Number.parseInt(icon.getAttribute("height") || "0", 10),
483
+ xPosition: icon.getAttribute("xPosition") || "right",
484
+ yPosition: icon.getAttribute("yPosition") || "top",
485
+ clickUrl: icon.querySelector("IconClicks > IconClickThrough")?.textContent?.trim()
486
+ });
487
+ }
488
+ });
489
+ }
490
+ inLine.querySelectorAll("CompanionAds > Companion").forEach((comp) => {
491
+ ad.companions = ad.companions || [];
492
+ const staticResource = comp.querySelector("StaticResource");
493
+ const iframeResource = comp.querySelector("IFrameResource");
494
+ const htmlResource = comp.querySelector("HTMLResource");
495
+ let resourceUrl = "";
496
+ let resourceType = "static";
497
+ if (staticResource?.textContent) {
498
+ resourceUrl = staticResource.textContent.trim();
499
+ resourceType = "static";
500
+ } else if (iframeResource?.textContent) {
501
+ resourceUrl = iframeResource.textContent.trim();
502
+ resourceType = "iframe";
503
+ } else if (htmlResource?.textContent) {
504
+ resourceUrl = htmlResource.textContent.trim();
505
+ resourceType = "html";
506
+ }
507
+ if (resourceUrl) {
508
+ ad.companions.push({
509
+ width: Number.parseInt(comp.getAttribute("width") || "0", 10),
510
+ height: Number.parseInt(comp.getAttribute("height") || "0", 10),
511
+ resourceUrl,
512
+ resourceType,
513
+ clickThroughUrl: comp.querySelector("CompanionClickThrough")?.textContent?.trim(),
514
+ altText: comp.getAttribute("altText") || undefined,
515
+ trackingUrl: comp.querySelector("TrackingEvents > Tracking")?.textContent?.trim()
516
+ });
517
+ }
518
+ });
519
+ if (ad.mediaFiles.length > 0) {
520
+ ads.push(ad);
521
+ }
522
+ });
523
+ return ads;
524
+ }
525
+ parseDuration(duration) {
526
+ const parts = duration.split(":");
527
+ if (parts.length !== 3)
528
+ return 0;
529
+ const hours = Number.parseFloat(parts[0]);
530
+ const minutes = Number.parseFloat(parts[1]);
531
+ const seconds = Number.parseFloat(parts[2]);
532
+ return hours * 3600 + minutes * 60 + seconds;
533
+ }
534
+ parseOffset(offset, duration) {
535
+ if (offset.endsWith("%")) {
536
+ const percent = Number.parseFloat(offset) / 100;
537
+ return duration * percent;
538
+ }
539
+ return this.parseDuration(offset);
540
+ }
541
+ generateId() {
542
+ return `ad-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
543
+ }
544
+ }
545
+
546
+ class AdsManager {
547
+ player = null;
548
+ config;
549
+ parser;
550
+ state;
551
+ container = null;
552
+ adVideo = null;
553
+ adContainer = null;
554
+ skipButton = null;
555
+ adBreaks = [];
556
+ currentAds = [];
557
+ currentAdIndex = 0;
558
+ skipTimer = null;
559
+ contentPaused = false;
560
+ onAdStart = null;
561
+ onAdComplete = null;
562
+ onAdSkip = null;
563
+ onAdClick = null;
564
+ onAdErrorCallback = null;
565
+ onAllAdsComplete = null;
566
+ constructor(config = {}) {
567
+ this.config = {
568
+ skipDelay: 5,
569
+ maxRedirects: 5,
570
+ timeout: 1e4,
571
+ vpaidEnabled: false,
572
+ preloadAds: true,
573
+ debug: false,
574
+ ...config
575
+ };
576
+ this.parser = new VASTParser({
577
+ maxRedirects: this.config.maxRedirects,
578
+ timeout: this.config.timeout
579
+ });
580
+ this.state = {
581
+ isPlaying: false,
582
+ currentAd: null,
583
+ currentBreak: null,
584
+ timeRemaining: 0,
585
+ canSkip: false,
586
+ skipCountdown: 0,
587
+ isLoading: false,
588
+ error: null
589
+ };
590
+ if (this.config.schedule) {
591
+ this.adBreaks = this.config.schedule;
592
+ } else if (this.config.vastUrl || this.config.vastXml) {
593
+ this.adBreaks = [{
594
+ type: "preroll",
595
+ vastUrl: this.config.vastUrl,
596
+ vastXml: this.config.vastXml
597
+ }];
598
+ }
599
+ }
600
+ attach(player, container) {
601
+ this.player = player;
602
+ this.container = container;
603
+ this.createAdContainer();
604
+ this.attachPlayerEvents();
605
+ if (this.config.preloadAds) {
606
+ this.preloadAds();
607
+ }
608
+ }
609
+ createAdContainer() {
610
+ if (!this.container)
611
+ return;
612
+ this.adContainer = document.createElement("div");
613
+ this.adContainer.className = "tsvp-ad-container";
614
+ this.adContainer.style.cssText = `
615
+ position: absolute;
616
+ inset: 0;
617
+ z-index: 50;
618
+ display: none;
619
+ background: #000;
620
+ `;
621
+ this.adVideo = document.createElement("video");
622
+ this.adVideo.style.cssText = `
623
+ width: 100%;
624
+ height: 100%;
625
+ object-fit: contain;
626
+ `;
627
+ this.adVideo.playsInline = true;
628
+ const overlay = document.createElement("div");
629
+ overlay.className = "tsvp-ad-overlay";
630
+ overlay.style.cssText = `
631
+ position: absolute;
632
+ inset: 0;
633
+ display: flex;
634
+ flex-direction: column;
635
+ pointer-events: none;
636
+ `;
637
+ const adInfo = document.createElement("div");
638
+ adInfo.className = "tsvp-ad-info";
639
+ adInfo.style.cssText = `
640
+ position: absolute;
641
+ top: 12px;
642
+ left: 12px;
643
+ padding: 6px 12px;
644
+ background: rgba(0, 0, 0, 0.7);
645
+ color: #fff;
646
+ font-size: 12px;
647
+ font-family: var(--tsvp-font-family, sans-serif);
648
+ border-radius: 4px;
649
+ `;
650
+ adInfo.textContent = "Ad";
651
+ this.skipButton = document.createElement("button");
652
+ this.skipButton.className = "tsvp-ad-skip";
653
+ this.skipButton.style.cssText = `
654
+ position: absolute;
655
+ bottom: 80px;
656
+ right: 12px;
657
+ padding: 10px 16px;
658
+ background: rgba(0, 0, 0, 0.8);
659
+ color: #fff;
660
+ border: 1px solid rgba(255, 255, 255, 0.3);
661
+ border-radius: 4px;
662
+ font-size: 14px;
663
+ font-family: var(--tsvp-font-family, sans-serif);
664
+ cursor: pointer;
665
+ pointer-events: auto;
666
+ display: none;
667
+ `;
668
+ this.skipButton.addEventListener("click", () => this.skipAd());
669
+ const learnMore = document.createElement("button");
670
+ learnMore.className = "tsvp-ad-learn-more";
671
+ learnMore.style.cssText = `
672
+ position: absolute;
673
+ bottom: 80px;
674
+ left: 12px;
675
+ padding: 10px 16px;
676
+ background: var(--tsvp-color-primary, #00a8ff);
677
+ color: #fff;
678
+ border: none;
679
+ border-radius: 4px;
680
+ font-size: 14px;
681
+ font-family: var(--tsvp-font-family, sans-serif);
682
+ cursor: pointer;
683
+ pointer-events: auto;
684
+ display: none;
685
+ `;
686
+ learnMore.textContent = "Learn More";
687
+ learnMore.addEventListener("click", () => this.handleAdClick());
688
+ overlay.appendChild(adInfo);
689
+ overlay.appendChild(this.skipButton);
690
+ overlay.appendChild(learnMore);
691
+ this.adContainer.appendChild(this.adVideo);
692
+ this.adContainer.appendChild(overlay);
693
+ this.container.appendChild(this.adContainer);
694
+ this.adVideo.addEventListener("timeupdate", () => this.onAdTimeUpdate());
695
+ this.adVideo.addEventListener("ended", () => this.onAdEnded());
696
+ this.adVideo.addEventListener("error", () => this.handleAdError());
697
+ }
698
+ attachPlayerEvents() {
699
+ if (!this.player)
700
+ return;
701
+ let firstPlay = true;
702
+ this.player.on("play", async () => {
703
+ if (firstPlay) {
704
+ firstPlay = false;
705
+ const preroll = this.adBreaks.find((b) => b.type === "preroll" && !b.played);
706
+ if (preroll) {
707
+ await this.playAdBreak(preroll);
708
+ }
709
+ }
710
+ });
711
+ this.player.on("timeupdate", () => {
712
+ const state = this.player?.state;
713
+ if (!state)
714
+ return;
715
+ for (const adBreak of this.adBreaks) {
716
+ if (adBreak.type === "midroll" && !adBreak.played && adBreak.timeOffset && state.currentTime >= adBreak.timeOffset) {
717
+ this.playAdBreak(adBreak);
718
+ break;
719
+ }
720
+ }
721
+ });
722
+ this.player.on("ended", async () => {
723
+ const postroll = this.adBreaks.find((b) => b.type === "postroll" && !b.played);
724
+ if (postroll) {
725
+ await this.playAdBreak(postroll);
726
+ }
727
+ });
728
+ }
729
+ async preloadAds() {
730
+ for (const adBreak of this.adBreaks) {
731
+ if (adBreak.vastUrl) {
732
+ try {
733
+ await this.parser.parseFromUrl(adBreak.vastUrl);
734
+ } catch (error) {
735
+ this.log("Failed to preload ad:", error);
736
+ }
737
+ }
738
+ }
739
+ }
740
+ async playAdBreak(adBreak) {
741
+ if (this.state.isPlaying)
742
+ return;
743
+ this.state.currentBreak = adBreak;
744
+ this.state.isLoading = true;
745
+ this.pauseContent();
746
+ try {
747
+ if (adBreak.vastUrl) {
748
+ this.currentAds = await this.parser.parseFromUrl(adBreak.vastUrl);
749
+ } else if (adBreak.vastXml) {
750
+ this.currentAds = this.parser.parseFromXml(adBreak.vastXml);
751
+ }
752
+ if (this.currentAds.length === 0) {
753
+ throw new Error("No ads found in VAST response");
754
+ }
755
+ this.currentAdIndex = 0;
756
+ adBreak.played = true;
757
+ await this.playAd(this.currentAds[0]);
758
+ } catch (error) {
759
+ this.state.error = error.message;
760
+ this.state.isLoading = false;
761
+ this.onAdErrorCallback?.(error.message);
762
+ this.resumeContent();
763
+ }
764
+ }
765
+ async playAd(ad) {
766
+ if (!this.adVideo || !this.adContainer)
767
+ return;
768
+ this.state.currentAd = ad;
769
+ this.state.isPlaying = true;
770
+ this.state.isLoading = false;
771
+ this.state.timeRemaining = ad.duration;
772
+ this.state.canSkip = false;
773
+ this.state.skipCountdown = this.config.skipDelay || 0;
774
+ const mediaFile = this.selectMediaFile(ad.mediaFiles);
775
+ if (!mediaFile) {
776
+ throw new Error("No suitable media file found");
777
+ }
778
+ this.adContainer.style.display = "block";
779
+ this.adVideo.src = mediaFile.url;
780
+ await this.adVideo.play();
781
+ this.trackEvent(ad, "impression");
782
+ this.trackEvent(ad, "start");
783
+ this.startSkipTimer(ad);
784
+ this.onAdStart?.(ad);
785
+ }
786
+ selectMediaFile(mediaFiles) {
787
+ const sorted = [...mediaFiles].sort((a, b) => {
788
+ if (a.type.includes("mp4") && !b.type.includes("mp4"))
789
+ return -1;
790
+ if (!a.type.includes("mp4") && b.type.includes("mp4"))
791
+ return 1;
792
+ return (b.bitrate || 0) - (a.bitrate || 0);
793
+ });
794
+ return sorted[0] || null;
795
+ }
796
+ startSkipTimer(ad) {
797
+ const skipDelay = this.config.skipDelay === -1 ? ad.skipOffset || 5 : this.config.skipDelay || 0;
798
+ if (skipDelay <= 0) {
799
+ this.state.canSkip = true;
800
+ this.updateSkipButton();
801
+ return;
802
+ }
803
+ this.state.skipCountdown = skipDelay;
804
+ this.skipTimer = setInterval(() => {
805
+ this.state.skipCountdown--;
806
+ if (this.state.skipCountdown <= 0) {
807
+ this.state.canSkip = true;
808
+ if (this.skipTimer) {
809
+ clearInterval(this.skipTimer);
810
+ this.skipTimer = null;
811
+ }
812
+ }
813
+ this.updateSkipButton();
814
+ }, 1000);
815
+ }
816
+ updateSkipButton() {
817
+ if (!this.skipButton)
818
+ return;
819
+ if (this.state.canSkip) {
820
+ this.skipButton.textContent = "Skip Ad";
821
+ this.skipButton.style.display = "block";
822
+ } else if (this.state.skipCountdown > 0) {
823
+ this.skipButton.textContent = `Skip in ${this.state.skipCountdown}s`;
824
+ this.skipButton.style.display = "block";
825
+ this.skipButton.style.cursor = "default";
826
+ } else {
827
+ this.skipButton.style.display = "none";
828
+ }
829
+ }
830
+ skipAd() {
831
+ if (!this.state.canSkip || !this.state.currentAd)
832
+ return;
833
+ this.trackEvent(this.state.currentAd, "skip");
834
+ this.onAdSkip?.(this.state.currentAd);
835
+ this.onAdEnded();
836
+ }
837
+ handleAdClick() {
838
+ if (!this.state.currentAd?.clickThroughUrl)
839
+ return;
840
+ this.adVideo?.pause();
841
+ this.trackEvent(this.state.currentAd, "clickThrough");
842
+ this.onAdClick?.(this.state.currentAd);
843
+ window.open(this.state.currentAd.clickThroughUrl, "_blank");
844
+ }
845
+ onAdTimeUpdate() {
846
+ if (!this.adVideo || !this.state.currentAd)
847
+ return;
848
+ const currentTime = this.adVideo.currentTime;
849
+ const duration = this.state.currentAd.duration;
850
+ this.state.timeRemaining = Math.max(0, duration - currentTime);
851
+ const progress = currentTime / duration;
852
+ if (progress >= 0.25)
853
+ this.trackEvent(this.state.currentAd, "firstQuartile");
854
+ if (progress >= 0.5)
855
+ this.trackEvent(this.state.currentAd, "midpoint");
856
+ if (progress >= 0.75)
857
+ this.trackEvent(this.state.currentAd, "thirdQuartile");
858
+ for (const event of this.state.currentAd.trackingEvents) {
859
+ if (event.event === "progress" && event.offset && currentTime >= event.offset) {
860
+ this.trackUrl(event.url);
861
+ }
862
+ }
863
+ }
864
+ onAdEnded() {
865
+ if (this.state.currentAd) {
866
+ this.trackEvent(this.state.currentAd, "complete");
867
+ this.onAdComplete?.(this.state.currentAd);
868
+ }
869
+ if (this.skipTimer) {
870
+ clearInterval(this.skipTimer);
871
+ this.skipTimer = null;
872
+ }
873
+ this.currentAdIndex++;
874
+ if (this.currentAdIndex < this.currentAds.length) {
875
+ this.playAd(this.currentAds[this.currentAdIndex]);
876
+ } else {
877
+ this.finishAdBreak();
878
+ }
879
+ }
880
+ handleAdError() {
881
+ const error = "Ad playback failed";
882
+ this.state.error = error;
883
+ this.onAdErrorCallback?.(error);
884
+ this.trackEvent(this.state.currentAd, "error");
885
+ this.finishAdBreak();
886
+ }
887
+ finishAdBreak() {
888
+ this.state.isPlaying = false;
889
+ this.state.currentAd = null;
890
+ this.state.currentBreak = null;
891
+ this.currentAds = [];
892
+ this.currentAdIndex = 0;
893
+ if (this.adContainer) {
894
+ this.adContainer.style.display = "none";
895
+ }
896
+ this.onAllAdsComplete?.();
897
+ this.resumeContent();
898
+ }
899
+ pauseContent() {
900
+ if (!this.player)
901
+ return;
902
+ const state = this.player.state;
903
+ if (state.playbackState === "playing") {
904
+ this.contentPaused = true;
905
+ this.player.pause();
906
+ }
907
+ }
908
+ resumeContent() {
909
+ if (!this.player)
910
+ return;
911
+ if (this.contentPaused) {
912
+ this.contentPaused = false;
913
+ this.player.play();
914
+ }
915
+ }
916
+ trackEvent(ad, eventName) {
917
+ const events = ad.trackingEvents.filter((e) => e.event === eventName);
918
+ for (const event of events) {
919
+ this.trackUrl(event.url);
920
+ }
921
+ }
922
+ trackUrl(url) {
923
+ const img = new Image;
924
+ img.src = url;
925
+ }
926
+ log(...args) {
927
+ if (this.config.debug) {
928
+ console.log("[VideoAds]", ...args);
929
+ }
930
+ }
931
+ getState() {
932
+ return { ...this.state };
933
+ }
934
+ on(event, handler) {
935
+ switch (event) {
936
+ case "adStart":
937
+ this.onAdStart = handler;
938
+ break;
939
+ case "adComplete":
940
+ this.onAdComplete = handler;
941
+ break;
942
+ case "adSkip":
943
+ this.onAdSkip = handler;
944
+ break;
945
+ case "adClick":
946
+ this.onAdClick = handler;
947
+ break;
948
+ case "adError":
949
+ this.onAdErrorCallback = handler;
950
+ break;
951
+ case "allAdsComplete":
952
+ this.onAllAdsComplete = handler;
953
+ break;
954
+ }
955
+ }
956
+ destroy() {
957
+ if (this.skipTimer) {
958
+ clearInterval(this.skipTimer);
959
+ }
960
+ if (this.adContainer) {
961
+ this.adContainer.remove();
962
+ }
963
+ this.player = null;
964
+ this.container = null;
965
+ this.adVideo = null;
966
+ this.adContainer = null;
967
+ }
968
+ }
969
+ function createAdsManager(config) {
970
+ return new AdsManager(config);
971
+ }
972
+ function adsPlugin(config) {
973
+ return {
974
+ name: "ads",
975
+ install(player, container) {
976
+ const ads = createAdsManager(config);
977
+ ads.attach(player, container);
978
+ player.ads = ads;
979
+ return () => {
980
+ ads.destroy();
981
+ delete player.ads;
982
+ };
983
+ }
984
+ };
985
+ }
986
+ // src/plugins/skip-segments.ts
987
+ var defaultButtonText = {
988
+ intro: "Skip Intro",
989
+ outro: "Skip Outro",
990
+ recap: "Skip Recap",
991
+ preview: "Skip Preview",
992
+ credits: "Skip Credits",
993
+ sponsor: "Skip",
994
+ custom: "Skip"
995
+ };
996
+
997
+ class SkipSegmentsManager {
998
+ player = null;
999
+ config;
1000
+ state;
1001
+ container = null;
1002
+ button = null;
1003
+ markersContainer = null;
1004
+ countdownTimer = null;
1005
+ hideTimer = null;
1006
+ unsubscribers = [];
1007
+ onSkip = null;
1008
+ onSegmentEnter = null;
1009
+ onSegmentExit = null;
1010
+ constructor(config) {
1011
+ this.config = {
1012
+ segments: config.segments || [],
1013
+ buttonVisibleDuration: config.buttonVisibleDuration ?? 1e4,
1014
+ defaultAutoSkipDelay: config.defaultAutoSkipDelay ?? 0,
1015
+ buttonPosition: config.buttonPosition || "bottom-right",
1016
+ showMarkers: config.showMarkers ?? true,
1017
+ markerColor: config.markerColor || "rgba(255, 255, 0, 0.8)",
1018
+ keyboardShortcut: config.keyboardShortcut ?? true
1019
+ };
1020
+ this.state = {
1021
+ activeSegment: null,
1022
+ buttonVisible: false,
1023
+ countdown: 0,
1024
+ isAutoSkipping: false
1025
+ };
1026
+ }
1027
+ attach(player, container) {
1028
+ this.player = player;
1029
+ this.container = container;
1030
+ this.createButton();
1031
+ this.attachEventListeners();
1032
+ if (this.config.showMarkers) {
1033
+ this.createMarkers();
1034
+ }
1035
+ }
1036
+ createButton() {
1037
+ if (!this.container)
1038
+ return;
1039
+ this.button = document.createElement("button");
1040
+ this.button.className = "tsvp-skip-button";
1041
+ this.button.type = "button";
1042
+ const positionStyles = this.getPositionStyles();
1043
+ this.button.style.cssText = `
1044
+ position: absolute;
1045
+ ${positionStyles}
1046
+ padding: 12px 24px;
1047
+ background: rgba(0, 0, 0, 0.85);
1048
+ color: #fff;
1049
+ border: 2px solid rgba(255, 255, 255, 0.8);
1050
+ border-radius: 4px;
1051
+ font-size: 16px;
1052
+ font-weight: 600;
1053
+ font-family: var(--tsvp-font-family, sans-serif);
1054
+ cursor: pointer;
1055
+ z-index: 30;
1056
+ display: none;
1057
+ transition: transform 150ms ease, background 150ms ease;
1058
+ `;
1059
+ this.button.addEventListener("mouseenter", () => {
1060
+ this.button.style.background = "rgba(255, 255, 255, 0.2)";
1061
+ this.button.style.transform = "scale(1.05)";
1062
+ });
1063
+ this.button.addEventListener("mouseleave", () => {
1064
+ this.button.style.background = "rgba(0, 0, 0, 0.85)";
1065
+ this.button.style.transform = "scale(1)";
1066
+ });
1067
+ this.button.addEventListener("click", () => this.skip());
1068
+ this.container.appendChild(this.button);
1069
+ }
1070
+ getPositionStyles() {
1071
+ switch (this.config.buttonPosition) {
1072
+ case "top-left":
1073
+ return "top: 20px; left: 20px;";
1074
+ case "top-right":
1075
+ return "top: 20px; right: 20px;";
1076
+ case "bottom-left":
1077
+ return "bottom: 80px; left: 20px;";
1078
+ case "bottom-right":
1079
+ default:
1080
+ return "bottom: 80px; right: 20px;";
1081
+ }
1082
+ }
1083
+ createMarkers() {}
1084
+ getMarkersElement(duration) {
1085
+ this.markersContainer = document.createElement("div");
1086
+ this.markersContainer.className = "tsvp-skip-markers";
1087
+ this.markersContainer.style.cssText = `
1088
+ position: absolute;
1089
+ left: 0;
1090
+ right: 0;
1091
+ top: 0;
1092
+ bottom: 0;
1093
+ pointer-events: none;
1094
+ `;
1095
+ for (const segment of this.config.segments) {
1096
+ const marker = document.createElement("div");
1097
+ marker.className = "tsvp-skip-marker";
1098
+ const startPercent = segment.startTime / duration * 100;
1099
+ const widthPercent = (segment.endTime - segment.startTime) / duration * 100;
1100
+ marker.style.cssText = `
1101
+ position: absolute;
1102
+ left: ${startPercent}%;
1103
+ width: ${widthPercent}%;
1104
+ top: 0;
1105
+ bottom: 0;
1106
+ background: ${this.config.markerColor};
1107
+ opacity: 0.6;
1108
+ `;
1109
+ this.markersContainer.appendChild(marker);
1110
+ }
1111
+ return this.markersContainer;
1112
+ }
1113
+ attachEventListeners() {
1114
+ if (!this.player)
1115
+ return;
1116
+ const handleTimeUpdate = () => {
1117
+ const state = this.player?.state;
1118
+ if (!state)
1119
+ return;
1120
+ this.checkSegments(state.currentTime);
1121
+ };
1122
+ this.player.on("timeupdate", handleTimeUpdate);
1123
+ this.unsubscribers.push(() => this.player?.off("timeupdate", handleTimeUpdate));
1124
+ if (this.config.keyboardShortcut) {
1125
+ const handleKeydown = (e) => {
1126
+ if (e.key === "Enter" && this.state.activeSegment && this.state.buttonVisible) {
1127
+ e.preventDefault();
1128
+ this.skip();
1129
+ }
1130
+ };
1131
+ document.addEventListener("keydown", handleKeydown);
1132
+ this.unsubscribers.push(() => document.removeEventListener("keydown", handleKeydown));
1133
+ }
1134
+ }
1135
+ checkSegments(currentTime) {
1136
+ const previousSegment = this.state.activeSegment;
1137
+ let activeSegment = null;
1138
+ for (const segment of this.config.segments) {
1139
+ if (currentTime >= segment.startTime && currentTime < segment.endTime) {
1140
+ activeSegment = segment;
1141
+ break;
1142
+ }
1143
+ }
1144
+ if (activeSegment !== previousSegment) {
1145
+ if (previousSegment) {
1146
+ this.onSegmentExit?.(previousSegment);
1147
+ this.hideButton();
1148
+ }
1149
+ if (activeSegment) {
1150
+ this.state.activeSegment = activeSegment;
1151
+ this.onSegmentEnter?.(activeSegment);
1152
+ this.showButton(activeSegment);
1153
+ } else {
1154
+ this.state.activeSegment = null;
1155
+ }
1156
+ }
1157
+ }
1158
+ showButton(segment) {
1159
+ if (!this.button)
1160
+ return;
1161
+ const text = segment.buttonText || defaultButtonText[segment.type];
1162
+ this.button.textContent = text;
1163
+ this.state.buttonVisible = true;
1164
+ this.button.style.display = "block";
1165
+ this.button.style.opacity = "0";
1166
+ this.button.style.transform = "translateX(20px)";
1167
+ requestAnimationFrame(() => {
1168
+ this.button.style.transition = "opacity 200ms ease, transform 200ms ease";
1169
+ this.button.style.opacity = "1";
1170
+ this.button.style.transform = "translateX(0)";
1171
+ });
1172
+ const autoSkipDelay = segment.autoSkipDelay ?? this.config.defaultAutoSkipDelay;
1173
+ if (autoSkipDelay > 0) {
1174
+ this.startAutoSkip(autoSkipDelay, segment);
1175
+ }
1176
+ if (this.config.buttonVisibleDuration > 0 && !autoSkipDelay) {
1177
+ this.hideTimer = setTimeout(() => {
1178
+ this.hideButton();
1179
+ }, this.config.buttonVisibleDuration);
1180
+ }
1181
+ }
1182
+ hideButton() {
1183
+ if (!this.button)
1184
+ return;
1185
+ this.state.buttonVisible = false;
1186
+ this.state.isAutoSkipping = false;
1187
+ this.state.countdown = 0;
1188
+ if (this.countdownTimer) {
1189
+ clearInterval(this.countdownTimer);
1190
+ this.countdownTimer = null;
1191
+ }
1192
+ if (this.hideTimer) {
1193
+ clearTimeout(this.hideTimer);
1194
+ this.hideTimer = null;
1195
+ }
1196
+ this.button.style.opacity = "0";
1197
+ this.button.style.transform = "translateX(20px)";
1198
+ setTimeout(() => {
1199
+ if (this.button && !this.state.buttonVisible) {
1200
+ this.button.style.display = "none";
1201
+ }
1202
+ }, 200);
1203
+ }
1204
+ startAutoSkip(delay, segment) {
1205
+ this.state.isAutoSkipping = true;
1206
+ this.state.countdown = delay;
1207
+ this.updateButtonText(segment);
1208
+ this.countdownTimer = setInterval(() => {
1209
+ this.state.countdown--;
1210
+ this.updateButtonText(segment);
1211
+ if (this.state.countdown <= 0) {
1212
+ if (this.countdownTimer) {
1213
+ clearInterval(this.countdownTimer);
1214
+ this.countdownTimer = null;
1215
+ }
1216
+ this.skip();
1217
+ }
1218
+ }, 1000);
1219
+ }
1220
+ updateButtonText(segment) {
1221
+ if (!this.button)
1222
+ return;
1223
+ const baseText = segment.buttonText || defaultButtonText[segment.type];
1224
+ if (this.state.isAutoSkipping && this.state.countdown > 0 && segment.showCountdown !== false) {
1225
+ this.button.textContent = `${baseText} (${this.state.countdown})`;
1226
+ } else {
1227
+ this.button.textContent = baseText;
1228
+ }
1229
+ }
1230
+ skip() {
1231
+ if (!this.state.activeSegment || !this.player)
1232
+ return;
1233
+ const segment = this.state.activeSegment;
1234
+ const skipTo = segment.skipTo ?? segment.endTime;
1235
+ this.player.seekTo(skipTo);
1236
+ this.onSkip?.(segment);
1237
+ this.hideButton();
1238
+ }
1239
+ addSegment(segment) {
1240
+ this.config.segments.push(segment);
1241
+ }
1242
+ removeSegment(type) {
1243
+ this.config.segments = this.config.segments.filter((s) => s.type !== type);
1244
+ }
1245
+ clearSegments() {
1246
+ this.config.segments = [];
1247
+ this.hideButton();
1248
+ }
1249
+ setSegments(segments) {
1250
+ this.config.segments = segments;
1251
+ }
1252
+ getSegments() {
1253
+ return [...this.config.segments];
1254
+ }
1255
+ getState() {
1256
+ return { ...this.state };
1257
+ }
1258
+ on(event, handler) {
1259
+ switch (event) {
1260
+ case "skip":
1261
+ this.onSkip = handler;
1262
+ break;
1263
+ case "segmentEnter":
1264
+ this.onSegmentEnter = handler;
1265
+ break;
1266
+ case "segmentExit":
1267
+ this.onSegmentExit = handler;
1268
+ break;
1269
+ }
1270
+ }
1271
+ destroy() {
1272
+ if (this.countdownTimer) {
1273
+ clearInterval(this.countdownTimer);
1274
+ }
1275
+ if (this.hideTimer) {
1276
+ clearTimeout(this.hideTimer);
1277
+ }
1278
+ this.unsubscribers.forEach((unsub) => unsub());
1279
+ this.button?.remove();
1280
+ this.markersContainer?.remove();
1281
+ this.player = null;
1282
+ this.container = null;
1283
+ }
1284
+ }
1285
+ function createSkipSegments(config) {
1286
+ return new SkipSegmentsManager(config);
1287
+ }
1288
+ function skipSegmentsPlugin(config) {
1289
+ return {
1290
+ name: "skipSegments",
1291
+ install(player, container) {
1292
+ const skipSegments = createSkipSegments(config);
1293
+ skipSegments.attach(player, container);
1294
+ player.skipSegments = skipSegments;
1295
+ return () => {
1296
+ skipSegments.destroy();
1297
+ delete player.skipSegments;
1298
+ };
1299
+ }
1300
+ };
1301
+ }
1302
+ // src/plugins/end-screen.ts
1303
+ class EndScreenManager {
1304
+ player = null;
1305
+ config;
1306
+ state;
1307
+ container = null;
1308
+ element = null;
1309
+ countdownTimer = null;
1310
+ showTimer = null;
1311
+ unsubscribers = [];
1312
+ onRecommendationClick = null;
1313
+ onReplay = null;
1314
+ onAutoplay = null;
1315
+ onShow = null;
1316
+ onHide = null;
1317
+ constructor(config = {}) {
1318
+ this.config = {
1319
+ enabled: config.enabled ?? true,
1320
+ showDelay: config.showDelay ?? 0,
1321
+ recommendations: config.recommendations || [],
1322
+ actions: config.actions || [],
1323
+ showReplay: config.showReplay ?? true,
1324
+ showShare: config.showShare ?? false,
1325
+ autoplayCountdown: config.autoplayCountdown ?? 0,
1326
+ title: config.title || "",
1327
+ template: config.template || "",
1328
+ backgroundStyle: config.backgroundStyle || "blur"
1329
+ };
1330
+ this.state = {
1331
+ visible: false,
1332
+ countdown: 0,
1333
+ isCountingDown: false,
1334
+ selectedIndex: 0
1335
+ };
1336
+ }
1337
+ attach(player, container) {
1338
+ this.player = player;
1339
+ this.container = container;
1340
+ this.createElement();
1341
+ this.attachEventListeners();
1342
+ }
1343
+ createElement() {
1344
+ if (!this.container)
1345
+ return;
1346
+ this.element = document.createElement("div");
1347
+ this.element.className = "tsvp-end-screen";
1348
+ this.element.style.cssText = `
1349
+ position: absolute;
1350
+ inset: 0;
1351
+ display: none;
1352
+ flex-direction: column;
1353
+ align-items: center;
1354
+ justify-content: center;
1355
+ z-index: 40;
1356
+ font-family: var(--tsvp-font-family, sans-serif);
1357
+ color: var(--tsvp-color-text, #ffffff);
1358
+ padding: 24px;
1359
+ overflow: auto;
1360
+ `;
1361
+ this.applyBackgroundStyle();
1362
+ this.container.appendChild(this.element);
1363
+ }
1364
+ applyBackgroundStyle() {
1365
+ if (!this.element)
1366
+ return;
1367
+ switch (this.config.backgroundStyle) {
1368
+ case "blur":
1369
+ this.element.style.background = "rgba(0, 0, 0, 0.85)";
1370
+ this.element.style.backdropFilter = "blur(10px)";
1371
+ break;
1372
+ case "dark":
1373
+ this.element.style.background = "rgba(0, 0, 0, 0.95)";
1374
+ break;
1375
+ case "gradient":
1376
+ this.element.style.background = "linear-gradient(135deg, rgba(0,0,0,0.9) 0%, rgba(20,20,40,0.95) 100%)";
1377
+ break;
1378
+ case "none":
1379
+ this.element.style.background = "transparent";
1380
+ break;
1381
+ }
1382
+ }
1383
+ render() {
1384
+ if (!this.element)
1385
+ return;
1386
+ if (this.config.template) {
1387
+ this.element.innerHTML = this.config.template;
1388
+ return;
1389
+ }
1390
+ this.element.innerHTML = "";
1391
+ if (this.config.title) {
1392
+ const title = document.createElement("h2");
1393
+ title.className = "tsvp-end-screen-title";
1394
+ title.textContent = this.config.title;
1395
+ title.style.cssText = `
1396
+ margin: 0 0 24px;
1397
+ font-size: 24px;
1398
+ font-weight: 600;
1399
+ text-align: center;
1400
+ `;
1401
+ this.element.appendChild(title);
1402
+ }
1403
+ if (this.config.recommendations.length > 0) {
1404
+ const grid = document.createElement("div");
1405
+ grid.className = "tsvp-end-screen-grid";
1406
+ grid.style.cssText = `
1407
+ display: grid;
1408
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
1409
+ gap: 16px;
1410
+ max-width: 900px;
1411
+ width: 100%;
1412
+ margin-bottom: 24px;
1413
+ `;
1414
+ this.config.recommendations.forEach((rec, index) => {
1415
+ const card = this.createRecommendationCard(rec, index);
1416
+ grid.appendChild(card);
1417
+ });
1418
+ this.element.appendChild(grid);
1419
+ }
1420
+ const actionsRow = document.createElement("div");
1421
+ actionsRow.className = "tsvp-end-screen-actions";
1422
+ actionsRow.style.cssText = `
1423
+ display: flex;
1424
+ gap: 12px;
1425
+ flex-wrap: wrap;
1426
+ justify-content: center;
1427
+ `;
1428
+ if (this.config.showReplay) {
1429
+ const replayBtn = this.createActionButton({
1430
+ label: "Replay",
1431
+ icon: `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
1432
+ <path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/>
1433
+ </svg>`,
1434
+ style: "secondary",
1435
+ onClick: () => this.replay()
1436
+ });
1437
+ actionsRow.appendChild(replayBtn);
1438
+ }
1439
+ if (this.config.showShare) {
1440
+ const shareBtn = this.createActionButton({
1441
+ label: "Share",
1442
+ icon: `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
1443
+ <path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92s2.92-1.31 2.92-2.92-1.31-2.92-2.92-2.92z"/>
1444
+ </svg>`,
1445
+ style: "outline",
1446
+ onClick: () => this.share()
1447
+ });
1448
+ actionsRow.appendChild(shareBtn);
1449
+ }
1450
+ for (const action of this.config.actions) {
1451
+ const btn = this.createActionButton(action);
1452
+ actionsRow.appendChild(btn);
1453
+ }
1454
+ this.element.appendChild(actionsRow);
1455
+ if (this.state.isCountingDown && this.config.recommendations.length > 0) {
1456
+ const countdown = document.createElement("div");
1457
+ countdown.className = "tsvp-end-screen-countdown";
1458
+ countdown.style.cssText = `
1459
+ margin-top: 24px;
1460
+ text-align: center;
1461
+ font-size: 14px;
1462
+ opacity: 0.8;
1463
+ `;
1464
+ countdown.innerHTML = `
1465
+ Next video in <strong>${this.state.countdown}</strong> seconds
1466
+ <button class="tsvp-end-screen-cancel" style="
1467
+ margin-left: 12px;
1468
+ padding: 4px 12px;
1469
+ border: 1px solid rgba(255,255,255,0.3);
1470
+ border-radius: 4px;
1471
+ background: transparent;
1472
+ color: inherit;
1473
+ cursor: pointer;
1474
+ ">Cancel</button>
1475
+ `;
1476
+ const cancelBtn = countdown.querySelector(".tsvp-end-screen-cancel");
1477
+ cancelBtn?.addEventListener("click", () => this.cancelAutoplay());
1478
+ this.element.appendChild(countdown);
1479
+ }
1480
+ }
1481
+ createRecommendationCard(rec, index) {
1482
+ const card = document.createElement("div");
1483
+ card.className = "tsvp-end-screen-card";
1484
+ card.tabIndex = 0;
1485
+ const isSelected = index === this.state.selectedIndex && this.state.isCountingDown;
1486
+ card.style.cssText = `
1487
+ background: rgba(255, 255, 255, 0.1);
1488
+ border-radius: 8px;
1489
+ overflow: hidden;
1490
+ cursor: pointer;
1491
+ transition: transform 150ms ease, background 150ms ease;
1492
+ ${isSelected ? "outline: 2px solid var(--tsvp-color-primary, #00a8ff); outline-offset: 2px;" : ""}
1493
+ `;
1494
+ const thumbnail = document.createElement("div");
1495
+ thumbnail.className = "tsvp-end-screen-thumbnail";
1496
+ thumbnail.style.cssText = `
1497
+ position: relative;
1498
+ aspect-ratio: 16 / 9;
1499
+ background: url(${rec.thumbnail}) center/cover;
1500
+ `;
1501
+ if (rec.duration) {
1502
+ const duration = document.createElement("span");
1503
+ duration.className = "tsvp-end-screen-duration";
1504
+ duration.textContent = rec.duration;
1505
+ duration.style.cssText = `
1506
+ position: absolute;
1507
+ bottom: 4px;
1508
+ right: 4px;
1509
+ padding: 2px 6px;
1510
+ background: rgba(0, 0, 0, 0.8);
1511
+ border-radius: 4px;
1512
+ font-size: 12px;
1513
+ font-weight: 500;
1514
+ `;
1515
+ thumbnail.appendChild(duration);
1516
+ }
1517
+ card.appendChild(thumbnail);
1518
+ const info = document.createElement("div");
1519
+ info.className = "tsvp-end-screen-info";
1520
+ info.style.cssText = `
1521
+ padding: 12px;
1522
+ `;
1523
+ const title = document.createElement("h3");
1524
+ title.className = "tsvp-end-screen-card-title";
1525
+ title.textContent = rec.title;
1526
+ title.style.cssText = `
1527
+ margin: 0 0 4px;
1528
+ font-size: 14px;
1529
+ font-weight: 500;
1530
+ overflow: hidden;
1531
+ text-overflow: ellipsis;
1532
+ display: -webkit-box;
1533
+ -webkit-line-clamp: 2;
1534
+ -webkit-box-orient: vertical;
1535
+ `;
1536
+ info.appendChild(title);
1537
+ if (rec.channel) {
1538
+ const channel = document.createElement("p");
1539
+ channel.className = "tsvp-end-screen-channel";
1540
+ channel.textContent = rec.channel;
1541
+ channel.style.cssText = `
1542
+ margin: 0;
1543
+ font-size: 12px;
1544
+ opacity: 0.7;
1545
+ `;
1546
+ info.appendChild(channel);
1547
+ }
1548
+ if (rec.views || rec.date) {
1549
+ const meta = document.createElement("p");
1550
+ meta.className = "tsvp-end-screen-meta";
1551
+ meta.textContent = [rec.views, rec.date].filter(Boolean).join(" • ");
1552
+ meta.style.cssText = `
1553
+ margin: 4px 0 0;
1554
+ font-size: 12px;
1555
+ opacity: 0.6;
1556
+ `;
1557
+ info.appendChild(meta);
1558
+ }
1559
+ card.appendChild(info);
1560
+ card.addEventListener("mouseenter", () => {
1561
+ card.style.transform = "scale(1.03)";
1562
+ card.style.background = "rgba(255, 255, 255, 0.15)";
1563
+ });
1564
+ card.addEventListener("mouseleave", () => {
1565
+ card.style.transform = "scale(1)";
1566
+ card.style.background = "rgba(255, 255, 255, 0.1)";
1567
+ });
1568
+ card.addEventListener("click", () => {
1569
+ this.onRecommendationClick?.(rec);
1570
+ if (rec.onClick) {
1571
+ rec.onClick();
1572
+ } else if (rec.url) {
1573
+ window.location.href = rec.url;
1574
+ }
1575
+ });
1576
+ return card;
1577
+ }
1578
+ createActionButton(action) {
1579
+ const btn = document.createElement("button");
1580
+ btn.className = `tsvp-end-screen-btn tsvp-end-screen-btn--${action.style || "secondary"}`;
1581
+ btn.type = "button";
1582
+ let bgColor = "rgba(255, 255, 255, 0.2)";
1583
+ let borderColor = "transparent";
1584
+ switch (action.style) {
1585
+ case "primary":
1586
+ bgColor = "var(--tsvp-color-primary, #00a8ff)";
1587
+ break;
1588
+ case "outline":
1589
+ bgColor = "transparent";
1590
+ borderColor = "rgba(255, 255, 255, 0.5)";
1591
+ break;
1592
+ }
1593
+ btn.style.cssText = `
1594
+ display: flex;
1595
+ align-items: center;
1596
+ gap: 8px;
1597
+ padding: 10px 20px;
1598
+ background: ${bgColor};
1599
+ border: 1px solid ${borderColor};
1600
+ border-radius: 6px;
1601
+ color: #fff;
1602
+ font-size: 14px;
1603
+ font-weight: 500;
1604
+ font-family: inherit;
1605
+ cursor: pointer;
1606
+ transition: transform 150ms ease, opacity 150ms ease;
1607
+ `;
1608
+ if (action.icon) {
1609
+ const icon = document.createElement("span");
1610
+ icon.innerHTML = action.icon;
1611
+ icon.style.display = "flex";
1612
+ btn.appendChild(icon);
1613
+ }
1614
+ const label = document.createElement("span");
1615
+ label.textContent = action.label;
1616
+ btn.appendChild(label);
1617
+ btn.addEventListener("mouseenter", () => {
1618
+ btn.style.transform = "scale(1.05)";
1619
+ });
1620
+ btn.addEventListener("mouseleave", () => {
1621
+ btn.style.transform = "scale(1)";
1622
+ });
1623
+ btn.addEventListener("click", action.onClick);
1624
+ return btn;
1625
+ }
1626
+ attachEventListeners() {
1627
+ if (!this.player)
1628
+ return;
1629
+ const handleEnded = () => {
1630
+ if (this.config.enabled) {
1631
+ this.showTimer = setTimeout(() => {
1632
+ this.show();
1633
+ }, this.config.showDelay);
1634
+ }
1635
+ };
1636
+ this.player.on("ended", handleEnded);
1637
+ this.unsubscribers.push(() => this.player?.off("ended", handleEnded));
1638
+ const handlePlay = () => {
1639
+ if (this.state.visible) {
1640
+ this.hide();
1641
+ }
1642
+ };
1643
+ this.player.on("play", handlePlay);
1644
+ this.unsubscribers.push(() => this.player?.off("play", handlePlay));
1645
+ const handleKeydown = (e) => {
1646
+ if (!this.state.visible)
1647
+ return;
1648
+ switch (e.key) {
1649
+ case "Escape":
1650
+ this.hide();
1651
+ break;
1652
+ case "ArrowLeft":
1653
+ this.selectPrevious();
1654
+ break;
1655
+ case "ArrowRight":
1656
+ this.selectNext();
1657
+ break;
1658
+ case "Enter":
1659
+ this.selectCurrent();
1660
+ break;
1661
+ }
1662
+ };
1663
+ document.addEventListener("keydown", handleKeydown);
1664
+ this.unsubscribers.push(() => document.removeEventListener("keydown", handleKeydown));
1665
+ }
1666
+ show() {
1667
+ if (!this.element || this.state.visible)
1668
+ return;
1669
+ this.state.visible = true;
1670
+ this.state.selectedIndex = 0;
1671
+ if (this.config.autoplayCountdown > 0 && this.config.recommendations.length > 0) {
1672
+ this.startAutoplayCountdown();
1673
+ }
1674
+ this.render();
1675
+ this.element.style.display = "flex";
1676
+ this.element.style.opacity = "0";
1677
+ requestAnimationFrame(() => {
1678
+ this.element.style.transition = "opacity 300ms ease";
1679
+ this.element.style.opacity = "1";
1680
+ });
1681
+ this.onShow?.();
1682
+ }
1683
+ hide() {
1684
+ if (!this.element || !this.state.visible)
1685
+ return;
1686
+ this.state.visible = false;
1687
+ this.cancelAutoplay();
1688
+ this.element.style.opacity = "0";
1689
+ setTimeout(() => {
1690
+ if (this.element && !this.state.visible) {
1691
+ this.element.style.display = "none";
1692
+ }
1693
+ }, 300);
1694
+ this.onHide?.();
1695
+ }
1696
+ startAutoplayCountdown() {
1697
+ this.state.isCountingDown = true;
1698
+ this.state.countdown = this.config.autoplayCountdown;
1699
+ this.countdownTimer = setInterval(() => {
1700
+ this.state.countdown--;
1701
+ this.render();
1702
+ if (this.state.countdown <= 0) {
1703
+ this.playNextVideo();
1704
+ }
1705
+ }, 1000);
1706
+ }
1707
+ cancelAutoplay() {
1708
+ this.state.isCountingDown = false;
1709
+ this.state.countdown = 0;
1710
+ if (this.countdownTimer) {
1711
+ clearInterval(this.countdownTimer);
1712
+ this.countdownTimer = null;
1713
+ }
1714
+ if (this.state.visible) {
1715
+ this.render();
1716
+ }
1717
+ }
1718
+ playNextVideo() {
1719
+ const rec = this.config.recommendations[this.state.selectedIndex];
1720
+ if (rec) {
1721
+ this.onAutoplay?.(rec);
1722
+ if (rec.onClick) {
1723
+ rec.onClick();
1724
+ } else if (rec.url) {
1725
+ window.location.href = rec.url;
1726
+ }
1727
+ }
1728
+ }
1729
+ selectPrevious() {
1730
+ if (this.config.recommendations.length === 0)
1731
+ return;
1732
+ this.state.selectedIndex = (this.state.selectedIndex - 1 + this.config.recommendations.length) % this.config.recommendations.length;
1733
+ this.render();
1734
+ }
1735
+ selectNext() {
1736
+ if (this.config.recommendations.length === 0)
1737
+ return;
1738
+ this.state.selectedIndex = (this.state.selectedIndex + 1) % this.config.recommendations.length;
1739
+ this.render();
1740
+ }
1741
+ selectCurrent() {
1742
+ const rec = this.config.recommendations[this.state.selectedIndex];
1743
+ if (rec) {
1744
+ this.onRecommendationClick?.(rec);
1745
+ if (rec.onClick) {
1746
+ rec.onClick();
1747
+ } else if (rec.url) {
1748
+ window.location.href = rec.url;
1749
+ }
1750
+ }
1751
+ }
1752
+ replay() {
1753
+ this.hide();
1754
+ this.player?.seekTo(0);
1755
+ this.player?.play();
1756
+ this.onReplay?.();
1757
+ }
1758
+ share() {
1759
+ if (navigator.share) {
1760
+ navigator.share({
1761
+ title: document.title,
1762
+ url: window.location.href
1763
+ });
1764
+ } else {
1765
+ navigator.clipboard?.writeText(window.location.href);
1766
+ }
1767
+ }
1768
+ setRecommendations(recommendations) {
1769
+ this.config.recommendations = recommendations;
1770
+ if (this.state.visible) {
1771
+ this.render();
1772
+ }
1773
+ }
1774
+ getState() {
1775
+ return { ...this.state };
1776
+ }
1777
+ on(event, handler) {
1778
+ switch (event) {
1779
+ case "recommendationClick":
1780
+ this.onRecommendationClick = handler;
1781
+ break;
1782
+ case "replay":
1783
+ this.onReplay = handler;
1784
+ break;
1785
+ case "autoplay":
1786
+ this.onAutoplay = handler;
1787
+ break;
1788
+ case "show":
1789
+ this.onShow = handler;
1790
+ break;
1791
+ case "hide":
1792
+ this.onHide = handler;
1793
+ break;
1794
+ }
1795
+ }
1796
+ destroy() {
1797
+ if (this.countdownTimer) {
1798
+ clearInterval(this.countdownTimer);
1799
+ }
1800
+ if (this.showTimer) {
1801
+ clearTimeout(this.showTimer);
1802
+ }
1803
+ this.unsubscribers.forEach((unsub) => unsub());
1804
+ this.element?.remove();
1805
+ this.player = null;
1806
+ this.container = null;
1807
+ }
1808
+ }
1809
+ function createEndScreen(config) {
1810
+ return new EndScreenManager(config);
1811
+ }
1812
+ function endScreenPlugin(config) {
1813
+ return {
1814
+ name: "endScreen",
1815
+ install(player, container) {
1816
+ const endScreen = createEndScreen(config);
1817
+ endScreen.attach(player, container);
1818
+ player.endScreen = endScreen;
1819
+ return () => {
1820
+ endScreen.destroy();
1821
+ delete player.endScreen;
1822
+ };
1823
+ }
1824
+ };
1825
+ }
1826
+ // src/plugins/watermark.ts
1827
+ class WatermarkManager {
1828
+ player = null;
1829
+ container = null;
1830
+ elements = new Map;
1831
+ dynamicTimers = new Map;
1832
+ unsubscribers = [];
1833
+ constructor() {}
1834
+ attach(player, container) {
1835
+ this.player = player;
1836
+ this.container = container;
1837
+ }
1838
+ add(id, config) {
1839
+ if (!this.container) {
1840
+ throw new Error("Watermark manager not attached to container");
1841
+ }
1842
+ this.remove(id);
1843
+ const element = this.createElement(config);
1844
+ element.dataset.watermarkId = id;
1845
+ this.elements.set(id, element);
1846
+ this.container.appendChild(element);
1847
+ if (config.showOnlyDuringPlayback && this.player) {
1848
+ this.setupPlaybackVisibility(id, element);
1849
+ }
1850
+ if (config.animation && config.animation.type !== "none") {
1851
+ this.applyAnimation(element, config.animation);
1852
+ }
1853
+ return element;
1854
+ }
1855
+ createElement(config) {
1856
+ const wrapper = document.createElement("div");
1857
+ wrapper.className = "tsvp-watermark";
1858
+ const positionStyles = this.getPositionStyles(config.position || "bottom-right", config.offset);
1859
+ wrapper.style.cssText = `
1860
+ position: absolute;
1861
+ ${positionStyles}
1862
+ z-index: 25;
1863
+ pointer-events: ${config.url ? "auto" : "none"};
1864
+ opacity: ${config.opacity ?? 0.7};
1865
+ transition: opacity ${config.fadeIn || 300}ms ease;
1866
+ ${config.rotate ? `transform: rotate(${config.rotate}deg);` : ""}
1867
+ `;
1868
+ if (config.styles) {
1869
+ Object.assign(wrapper.style, config.styles);
1870
+ }
1871
+ let content;
1872
+ if (config.html) {
1873
+ content = document.createElement("div");
1874
+ content.innerHTML = config.html;
1875
+ } else if (config.image) {
1876
+ const img = document.createElement("img");
1877
+ img.src = config.image;
1878
+ img.alt = "Watermark";
1879
+ img.style.cssText = `
1880
+ display: block;
1881
+ max-width: 100%;
1882
+ height: auto;
1883
+ ${config.size ? `width: ${config.size};` : ""}
1884
+ `;
1885
+ content = img;
1886
+ } else if (config.text) {
1887
+ content = document.createElement("span");
1888
+ content.textContent = config.text;
1889
+ content.style.cssText = `
1890
+ font-family: var(--tsvp-font-family, sans-serif);
1891
+ font-size: ${config.size || "14px"};
1892
+ color: rgba(255, 255, 255, 0.8);
1893
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
1894
+ `;
1895
+ } else {
1896
+ content = document.createElement("span");
1897
+ }
1898
+ if (config.url) {
1899
+ const link = document.createElement("a");
1900
+ link.href = config.url;
1901
+ link.target = config.newTab ? "_blank" : "_self";
1902
+ link.rel = config.newTab ? "noopener noreferrer" : "";
1903
+ link.style.textDecoration = "none";
1904
+ link.appendChild(content);
1905
+ wrapper.appendChild(link);
1906
+ } else {
1907
+ wrapper.appendChild(content);
1908
+ }
1909
+ return wrapper;
1910
+ }
1911
+ getPositionStyles(position, offset) {
1912
+ const offsetX = typeof offset?.x === "number" ? `${offset.x}px` : offset?.x || "16px";
1913
+ const offsetY = typeof offset?.y === "number" ? `${offset.y}px` : offset?.y || "16px";
1914
+ const positions = {
1915
+ "top-left": `top: ${offsetY}; left: ${offsetX};`,
1916
+ "top-center": `top: ${offsetY}; left: 50%; transform: translateX(-50%);`,
1917
+ "top-right": `top: ${offsetY}; right: ${offsetX};`,
1918
+ "center-left": `top: 50%; left: ${offsetX}; transform: translateY(-50%);`,
1919
+ center: `top: 50%; left: 50%; transform: translate(-50%, -50%);`,
1920
+ "center-right": `top: 50%; right: ${offsetX}; transform: translateY(-50%);`,
1921
+ "bottom-left": `bottom: ${offsetY}; left: ${offsetX};`,
1922
+ "bottom-center": `bottom: ${offsetY}; left: 50%; transform: translateX(-50%);`,
1923
+ "bottom-right": `bottom: ${offsetY}; right: ${offsetX};`
1924
+ };
1925
+ return positions[position] || positions["bottom-right"];
1926
+ }
1927
+ setupPlaybackVisibility(_id, element) {
1928
+ if (!this.player)
1929
+ return;
1930
+ element.style.opacity = "0";
1931
+ const handlePlay = () => {
1932
+ element.style.opacity = "";
1933
+ };
1934
+ const handlePause = () => {
1935
+ element.style.opacity = "0";
1936
+ };
1937
+ this.player.on("play", handlePlay);
1938
+ this.player.on("pause", handlePause);
1939
+ this.player.on("ended", handlePause);
1940
+ this.unsubscribers.push(() => {
1941
+ this.player?.off("play", handlePlay);
1942
+ this.player?.off("pause", handlePause);
1943
+ this.player?.off("ended", handlePause);
1944
+ });
1945
+ }
1946
+ applyAnimation(element, animation) {
1947
+ const duration = animation.duration || 1000;
1948
+ const delay = animation.delay || 0;
1949
+ const repeat = animation.repeat ? "infinite" : "1";
1950
+ if (!document.querySelector("#tsvp-watermark-animations")) {
1951
+ const style = document.createElement("style");
1952
+ style.id = "tsvp-watermark-animations";
1953
+ style.textContent = `
1954
+ @keyframes tsvp-watermark-fade {
1955
+ 0%, 100% { opacity: 0.3; }
1956
+ 50% { opacity: 1; }
1957
+ }
1958
+ @keyframes tsvp-watermark-slide {
1959
+ 0% { transform: translateX(-10px); }
1960
+ 50% { transform: translateX(10px); }
1961
+ 100% { transform: translateX(-10px); }
1962
+ }
1963
+ @keyframes tsvp-watermark-bounce {
1964
+ 0%, 100% { transform: translateY(0); }
1965
+ 50% { transform: translateY(-10px); }
1966
+ }
1967
+ @keyframes tsvp-watermark-pulse {
1968
+ 0%, 100% { transform: scale(1); }
1969
+ 50% { transform: scale(1.1); }
1970
+ }
1971
+ `;
1972
+ document.head.appendChild(style);
1973
+ }
1974
+ const animationName = `tsvp-watermark-${animation.type}`;
1975
+ element.style.animation = `${animationName} ${duration}ms ease ${delay}ms ${repeat}`;
1976
+ }
1977
+ addDynamic(id, config, baseConfig = {}) {
1978
+ const getText = () => {
1979
+ const parts = [];
1980
+ if (config.userId) {
1981
+ parts.push(config.userId);
1982
+ }
1983
+ if (config.showTimestamp) {
1984
+ parts.push(new Date().toISOString());
1985
+ }
1986
+ if (config.customText) {
1987
+ parts.push(config.customText);
1988
+ }
1989
+ return parts.join(" | ");
1990
+ };
1991
+ const positions = [
1992
+ "top-left",
1993
+ "top-right",
1994
+ "bottom-left",
1995
+ "bottom-right",
1996
+ "center-left",
1997
+ "center-right"
1998
+ ];
1999
+ let currentPosition = baseConfig.position || "bottom-right";
2000
+ const element = this.add(id, {
2001
+ ...baseConfig,
2002
+ text: getText(),
2003
+ position: currentPosition,
2004
+ opacity: baseConfig.opacity ?? 0.3
2005
+ });
2006
+ if (config.updateInterval && config.updateInterval > 0) {
2007
+ const textTimer = setInterval(() => {
2008
+ const textEl = element.querySelector("span");
2009
+ if (textEl) {
2010
+ textEl.textContent = getText();
2011
+ }
2012
+ }, config.updateInterval);
2013
+ this.dynamicTimers.set(`${id}-text`, textTimer);
2014
+ }
2015
+ if (config.randomizePosition && config.moveInterval && config.moveInterval > 0) {
2016
+ const moveTimer = setInterval(() => {
2017
+ const randomIndex = Math.floor(Math.random() * positions.length);
2018
+ currentPosition = positions[randomIndex];
2019
+ const positionStyles = this.getPositionStyles(currentPosition, baseConfig.offset);
2020
+ element.style.cssText = element.style.cssText.replace(/top:.*?;|bottom:.*?;|left:.*?;|right:.*?;|transform:.*?;/g, "");
2021
+ element.style.cssText += positionStyles;
2022
+ }, config.moveInterval);
2023
+ this.dynamicTimers.set(`${id}-move`, moveTimer);
2024
+ }
2025
+ return element;
2026
+ }
2027
+ remove(id) {
2028
+ const element = this.elements.get(id);
2029
+ if (element) {
2030
+ element.remove();
2031
+ this.elements.delete(id);
2032
+ }
2033
+ const textTimer = this.dynamicTimers.get(`${id}-text`);
2034
+ if (textTimer) {
2035
+ clearInterval(textTimer);
2036
+ this.dynamicTimers.delete(`${id}-text`);
2037
+ }
2038
+ const moveTimer = this.dynamicTimers.get(`${id}-move`);
2039
+ if (moveTimer) {
2040
+ clearInterval(moveTimer);
2041
+ this.dynamicTimers.delete(`${id}-move`);
2042
+ }
2043
+ }
2044
+ removeAll() {
2045
+ for (const id of this.elements.keys()) {
2046
+ this.remove(id);
2047
+ }
2048
+ }
2049
+ update(id, config) {
2050
+ const element = this.elements.get(id);
2051
+ if (!element)
2052
+ return;
2053
+ if (config.opacity !== undefined) {
2054
+ element.style.opacity = String(config.opacity);
2055
+ }
2056
+ if (config.position) {
2057
+ const positionStyles = this.getPositionStyles(config.position, config.offset);
2058
+ element.style.cssText = element.style.cssText.replace(/top:.*?;|bottom:.*?;|left:.*?;|right:.*?;|transform:.*?;/g, "");
2059
+ element.style.cssText += positionStyles;
2060
+ }
2061
+ if (config.text) {
2062
+ const textEl = element.querySelector("span");
2063
+ if (textEl) {
2064
+ textEl.textContent = config.text;
2065
+ }
2066
+ }
2067
+ if (config.image) {
2068
+ const imgEl = element.querySelector("img");
2069
+ if (imgEl) {
2070
+ imgEl.src = config.image;
2071
+ }
2072
+ }
2073
+ }
2074
+ show(id) {
2075
+ const element = this.elements.get(id);
2076
+ if (element) {
2077
+ element.style.display = "";
2078
+ }
2079
+ }
2080
+ hide(id) {
2081
+ const element = this.elements.get(id);
2082
+ if (element) {
2083
+ element.style.display = "none";
2084
+ }
2085
+ }
2086
+ showAll() {
2087
+ for (const id of this.elements.keys()) {
2088
+ this.show(id);
2089
+ }
2090
+ }
2091
+ hideAll() {
2092
+ for (const id of this.elements.keys()) {
2093
+ this.hide(id);
2094
+ }
2095
+ }
2096
+ get(id) {
2097
+ return this.elements.get(id);
2098
+ }
2099
+ getAll() {
2100
+ return Array.from(this.elements.keys());
2101
+ }
2102
+ destroy() {
2103
+ this.removeAll();
2104
+ this.unsubscribers.forEach((unsub) => unsub());
2105
+ this.player = null;
2106
+ this.container = null;
2107
+ }
2108
+ }
2109
+ function createWatermarkManager() {
2110
+ return new WatermarkManager;
2111
+ }
2112
+ function watermarkPlugin(watermarks) {
2113
+ return {
2114
+ name: "watermark",
2115
+ install(player, container) {
2116
+ const manager = createWatermarkManager();
2117
+ manager.attach(player, container);
2118
+ for (const { id, ...config } of watermarks) {
2119
+ manager.add(id, config);
2120
+ }
2121
+ player.watermark = manager;
2122
+ return () => {
2123
+ manager.destroy();
2124
+ delete player.watermark;
2125
+ };
2126
+ }
2127
+ };
2128
+ }
1
2129
  export {
2
- watermarkPlugin,
3
- skipSegmentsPlugin,
4
- endScreenPlugin,
5
- createWatermarkManager,
6
- createVideoAnalytics,
7
- createSkipSegments,
8
- createEndScreen,
9
- createAdsManager,
10
- analyticsPlugin,
11
- adsPlugin,
12
- WatermarkManager,
13
- VideoAnalytics,
14
- VASTParser,
15
- SkipSegmentsManager,
2130
+ AdsManager,
16
2131
  EndScreenManager,
17
- AdsManager
2132
+ SkipSegmentsManager,
2133
+ VASTParser,
2134
+ VideoAnalytics,
2135
+ WatermarkManager,
2136
+ adsPlugin,
2137
+ analyticsPlugin,
2138
+ createAdsManager,
2139
+ createEndScreen,
2140
+ createSkipSegments,
2141
+ createVideoAnalytics,
2142
+ createWatermarkManager,
2143
+ endScreenPlugin,
2144
+ skipSegmentsPlugin,
2145
+ watermarkPlugin
18
2146
  };
19
-
20
- //# debugId=A3B32FA203220F8B64756E2164756E21