ts-video-player 0.1.0

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 (251) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +438 -0
  3. package/dist/cdn.d.ts +10 -0
  4. package/dist/cdn.d.ts.map +1 -0
  5. package/dist/cdn.min.js +1046 -0
  6. package/dist/cdn.min.js.map +54 -0
  7. package/dist/cdn.test.d.ts +2 -0
  8. package/dist/cdn.test.d.ts.map +1 -0
  9. package/dist/chunk-36x4yhjn.js +765 -0
  10. package/dist/chunk-36x4yhjn.js.map +12 -0
  11. package/dist/chunk-eajts2jv.js +4243 -0
  12. package/dist/chunk-eajts2jv.js.map +42 -0
  13. package/dist/chunk-z1ehbz9f.js +380 -0
  14. package/dist/chunk-z1ehbz9f.js.map +19 -0
  15. package/dist/config.d.ts +9 -0
  16. package/dist/config.d.ts.map +1 -0
  17. package/dist/core/audio-gain.d.ts +67 -0
  18. package/dist/core/audio-gain.d.ts.map +1 -0
  19. package/dist/core/caption-styles.d.ts +121 -0
  20. package/dist/core/caption-styles.d.ts.map +1 -0
  21. package/dist/core/chapters.d.ts +150 -0
  22. package/dist/core/chapters.d.ts.map +1 -0
  23. package/dist/core/error-overlay.d.ts +71 -0
  24. package/dist/core/error-overlay.d.ts.map +1 -0
  25. package/dist/core/events.d.ts +119 -0
  26. package/dist/core/events.d.ts.map +1 -0
  27. package/dist/core/features.d.ts +64 -0
  28. package/dist/core/features.d.ts.map +1 -0
  29. package/dist/core/i18n.d.ts +132 -0
  30. package/dist/core/i18n.d.ts.map +1 -0
  31. package/dist/core/index.d.ts +37 -0
  32. package/dist/core/index.d.ts.map +1 -0
  33. package/dist/core/live.d.ts +130 -0
  34. package/dist/core/live.d.ts.map +1 -0
  35. package/dist/core/media-session.d.ts +38 -0
  36. package/dist/core/media-session.d.ts.map +1 -0
  37. package/dist/core/orientation.d.ts +22 -0
  38. package/dist/core/orientation.d.ts.map +1 -0
  39. package/dist/core/popover.d.ts +42 -0
  40. package/dist/core/popover.d.ts.map +1 -0
  41. package/dist/core/remote-playback.d.ts +195 -0
  42. package/dist/core/remote-playback.d.ts.map +1 -0
  43. package/dist/core/settings-menu.d.ts +132 -0
  44. package/dist/core/settings-menu.d.ts.map +1 -0
  45. package/dist/core/spinner.d.ts +67 -0
  46. package/dist/core/spinner.d.ts.map +1 -0
  47. package/dist/core/state.d.ts +142 -0
  48. package/dist/core/state.d.ts.map +1 -0
  49. package/dist/core/text-tracks.d.ts +162 -0
  50. package/dist/core/text-tracks.d.ts.map +1 -0
  51. package/dist/core/themes.d.ts +213 -0
  52. package/dist/core/themes.d.ts.map +1 -0
  53. package/dist/core/thumbnails.d.ts +124 -0
  54. package/dist/core/thumbnails.d.ts.map +1 -0
  55. package/dist/core/tooltips.d.ts +100 -0
  56. package/dist/core/tooltips.d.ts.map +1 -0
  57. package/dist/elements/base.d.ts +3 -0
  58. package/dist/elements/base.d.ts.map +1 -0
  59. package/dist/elements/index.d.ts +33 -0
  60. package/dist/elements/index.d.ts.map +1 -0
  61. package/dist/elements/index.js +55 -0
  62. package/dist/elements/index.js.map +9 -0
  63. package/dist/elements/media-airplay-button.d.ts +13 -0
  64. package/dist/elements/media-airplay-button.d.ts.map +1 -0
  65. package/dist/elements/media-error-display.d.ts +13 -0
  66. package/dist/elements/media-error-display.d.ts.map +1 -0
  67. package/dist/elements/media-fullscreen-button.d.ts +13 -0
  68. package/dist/elements/media-fullscreen-button.d.ts.map +1 -0
  69. package/dist/elements/media-live-button.d.ts +13 -0
  70. package/dist/elements/media-live-button.d.ts.map +1 -0
  71. package/dist/elements/media-mute-button.d.ts +13 -0
  72. package/dist/elements/media-mute-button.d.ts.map +1 -0
  73. package/dist/elements/media-pip-button.d.ts +13 -0
  74. package/dist/elements/media-pip-button.d.ts.map +1 -0
  75. package/dist/elements/media-play-button.d.ts +14 -0
  76. package/dist/elements/media-play-button.d.ts.map +1 -0
  77. package/dist/elements/media-playback-rate-button.d.ts +10 -0
  78. package/dist/elements/media-playback-rate-button.d.ts.map +1 -0
  79. package/dist/elements/media-preview-time.d.ts +18 -0
  80. package/dist/elements/media-preview-time.d.ts.map +1 -0
  81. package/dist/elements/media-progress-bar.d.ts +16 -0
  82. package/dist/elements/media-progress-bar.d.ts.map +1 -0
  83. package/dist/elements/media-remote-playback-button.d.ts +16 -0
  84. package/dist/elements/media-remote-playback-button.d.ts.map +1 -0
  85. package/dist/elements/media-seek-button.d.ts +9 -0
  86. package/dist/elements/media-seek-button.d.ts.map +1 -0
  87. package/dist/elements/media-settings-menu.d.ts +32 -0
  88. package/dist/elements/media-settings-menu.d.ts.map +1 -0
  89. package/dist/elements/media-time-display.d.ts +21 -0
  90. package/dist/elements/media-time-display.d.ts.map +1 -0
  91. package/dist/elements/media-time-group.d.ts +22 -0
  92. package/dist/elements/media-time-group.d.ts.map +1 -0
  93. package/dist/elements/media-time-separator.d.ts +13 -0
  94. package/dist/elements/media-time-separator.d.ts.map +1 -0
  95. package/dist/elements/media-volume-slider.d.ts +16 -0
  96. package/dist/elements/media-volume-slider.d.ts.map +1 -0
  97. package/dist/elements/utils.d.ts +27 -0
  98. package/dist/elements/utils.d.ts.map +1 -0
  99. package/dist/elements/video-player.d.ts +23 -0
  100. package/dist/elements/video-player.d.ts.map +1 -0
  101. package/dist/elements/video-skin.d.ts +23 -0
  102. package/dist/elements/video-skin.d.ts.map +1 -0
  103. package/dist/features/captions.d.ts +8 -0
  104. package/dist/features/captions.d.ts.map +1 -0
  105. package/dist/features/create-player.d.ts +45 -0
  106. package/dist/features/create-player.d.ts.map +1 -0
  107. package/dist/features/fullscreen.d.ts +8 -0
  108. package/dist/features/fullscreen.d.ts.map +1 -0
  109. package/dist/features/gestures.d.ts +8 -0
  110. package/dist/features/gestures.d.ts.map +1 -0
  111. package/dist/features/index.d.ts +26 -0
  112. package/dist/features/index.d.ts.map +1 -0
  113. package/dist/features/index.js +31 -0
  114. package/dist/features/index.js.map +9 -0
  115. package/dist/features/keyboard.d.ts +8 -0
  116. package/dist/features/keyboard.d.ts.map +1 -0
  117. package/dist/features/pip.d.ts +8 -0
  118. package/dist/features/pip.d.ts.map +1 -0
  119. package/dist/features/playback.d.ts +8 -0
  120. package/dist/features/playback.d.ts.map +1 -0
  121. package/dist/features/quality.d.ts +8 -0
  122. package/dist/features/quality.d.ts.map +1 -0
  123. package/dist/features/types.d.ts +26 -0
  124. package/dist/features/types.d.ts.map +1 -0
  125. package/dist/features/volume.d.ts +8 -0
  126. package/dist/features/volume.d.ts.map +1 -0
  127. package/dist/index.d.ts +84 -0
  128. package/dist/index.d.ts.map +1 -0
  129. package/dist/index.js +8149 -0
  130. package/dist/index.js.map +31 -0
  131. package/dist/layouts/index.d.ts +58 -0
  132. package/dist/layouts/index.d.ts.map +1 -0
  133. package/dist/player.d.ts +90 -0
  134. package/dist/player.d.ts.map +1 -0
  135. package/dist/plugins/ads.d.ts +311 -0
  136. package/dist/plugins/ads.d.ts.map +1 -0
  137. package/dist/plugins/analytics.d.ts +203 -0
  138. package/dist/plugins/analytics.d.ts.map +1 -0
  139. package/dist/plugins/end-screen.d.ts +184 -0
  140. package/dist/plugins/end-screen.d.ts.map +1 -0
  141. package/dist/plugins/index.d.ts +13 -0
  142. package/dist/plugins/index.d.ts.map +1 -0
  143. package/dist/plugins/index.js +20 -0
  144. package/dist/plugins/index.js.map +9 -0
  145. package/dist/plugins/skip-segments.d.ts +159 -0
  146. package/dist/plugins/skip-segments.d.ts.map +1 -0
  147. package/dist/plugins/watermark.d.ts +157 -0
  148. package/dist/plugins/watermark.d.ts.map +1 -0
  149. package/dist/providers/base.d.ts +56 -0
  150. package/dist/providers/base.d.ts.map +1 -0
  151. package/dist/providers/dash.d.ts +94 -0
  152. package/dist/providers/dash.d.ts.map +1 -0
  153. package/dist/providers/hls.d.ts +182 -0
  154. package/dist/providers/hls.d.ts.map +1 -0
  155. package/dist/providers/html5.d.ts +64 -0
  156. package/dist/providers/html5.d.ts.map +1 -0
  157. package/dist/providers/index.d.ts +41 -0
  158. package/dist/providers/index.d.ts.map +1 -0
  159. package/dist/providers/vimeo.d.ts +157 -0
  160. package/dist/providers/vimeo.d.ts.map +1 -0
  161. package/dist/providers/youtube.d.ts +134 -0
  162. package/dist/providers/youtube.d.ts.map +1 -0
  163. package/dist/stx/index.d.ts +59 -0
  164. package/dist/stx/index.d.ts.map +1 -0
  165. package/dist/stx/index.js +480 -0
  166. package/dist/stx/index.js.map +11 -0
  167. package/dist/types.d.ts +724 -0
  168. package/dist/types.d.ts.map +1 -0
  169. package/dist/ui/components.d.ts +121 -0
  170. package/dist/ui/components.d.ts.map +1 -0
  171. package/dist/ui/index.d.ts +9 -0
  172. package/dist/ui/index.d.ts.map +1 -0
  173. package/dist/ui/index.js +16 -0
  174. package/dist/ui/index.js.map +9 -0
  175. package/package.json +109 -0
  176. package/src/cdn.test.ts +10 -0
  177. package/src/cdn.ts +60 -0
  178. package/src/config.ts +25 -0
  179. package/src/core/audio-gain.ts +171 -0
  180. package/src/core/caption-styles.ts +568 -0
  181. package/src/core/chapters.ts +551 -0
  182. package/src/core/error-overlay.ts +354 -0
  183. package/src/core/events.ts +552 -0
  184. package/src/core/features.ts +287 -0
  185. package/src/core/i18n.ts +590 -0
  186. package/src/core/index.ts +249 -0
  187. package/src/core/live.ts +472 -0
  188. package/src/core/media-session.ts +137 -0
  189. package/src/core/orientation.ts +57 -0
  190. package/src/core/popover.ts +143 -0
  191. package/src/core/remote-playback.ts +661 -0
  192. package/src/core/settings-menu.ts +630 -0
  193. package/src/core/spinner.ts +310 -0
  194. package/src/core/state.ts +476 -0
  195. package/src/core/text-tracks.ts +599 -0
  196. package/src/core/themes.ts +607 -0
  197. package/src/core/thumbnails.ts +457 -0
  198. package/src/core/tooltips.ts +370 -0
  199. package/src/elements/base.ts +2 -0
  200. package/src/elements/index.ts +82 -0
  201. package/src/elements/media-airplay-button.ts +78 -0
  202. package/src/elements/media-error-display.ts +47 -0
  203. package/src/elements/media-fullscreen-button.ts +72 -0
  204. package/src/elements/media-live-button.ts +65 -0
  205. package/src/elements/media-mute-button.ts +69 -0
  206. package/src/elements/media-pip-button.ts +66 -0
  207. package/src/elements/media-play-button.ts +69 -0
  208. package/src/elements/media-playback-rate-button.ts +59 -0
  209. package/src/elements/media-preview-time.ts +75 -0
  210. package/src/elements/media-progress-bar.ts +200 -0
  211. package/src/elements/media-remote-playback-button.ts +108 -0
  212. package/src/elements/media-seek-button.ts +50 -0
  213. package/src/elements/media-settings-menu.ts +302 -0
  214. package/src/elements/media-time-display.ts +99 -0
  215. package/src/elements/media-time-group.ts +41 -0
  216. package/src/elements/media-time-separator.ts +35 -0
  217. package/src/elements/media-volume-slider.ts +140 -0
  218. package/src/elements/utils.ts +82 -0
  219. package/src/elements/video-player.ts +175 -0
  220. package/src/elements/video-skin.ts +127 -0
  221. package/src/features/captions.ts +56 -0
  222. package/src/features/create-player.ts +91 -0
  223. package/src/features/fullscreen.ts +44 -0
  224. package/src/features/gestures.ts +44 -0
  225. package/src/features/index.ts +44 -0
  226. package/src/features/keyboard.ts +89 -0
  227. package/src/features/pip.ts +47 -0
  228. package/src/features/playback.ts +68 -0
  229. package/src/features/quality.ts +19 -0
  230. package/src/features/types.ts +28 -0
  231. package/src/features/volume.ts +46 -0
  232. package/src/index.ts +635 -0
  233. package/src/layouts/index.ts +672 -0
  234. package/src/player.ts +821 -0
  235. package/src/plugins/ads.ts +1045 -0
  236. package/src/plugins/analytics.ts +673 -0
  237. package/src/plugins/end-screen.ts +793 -0
  238. package/src/plugins/index.ts +61 -0
  239. package/src/plugins/skip-segments.ts +537 -0
  240. package/src/plugins/watermark.ts +537 -0
  241. package/src/providers/base.ts +197 -0
  242. package/src/providers/dash.ts +588 -0
  243. package/src/providers/hls.ts +624 -0
  244. package/src/providers/html5.ts +473 -0
  245. package/src/providers/index.ts +97 -0
  246. package/src/providers/vimeo.ts +533 -0
  247. package/src/providers/youtube.ts +542 -0
  248. package/src/stx/index.ts +606 -0
  249. package/src/types.ts +874 -0
  250. package/src/ui/components.ts +910 -0
  251. package/src/ui/index.ts +29 -0
@@ -0,0 +1,1045 @@
1
+ /**
2
+ * Video Ads Plugin
3
+ *
4
+ * VAST/VPAID ad support for video playback.
5
+ *
6
+ * @module plugins/ads
7
+ */
8
+
9
+ import type { Player } from '../player'
10
+
11
+ // =============================================================================
12
+ // Types
13
+ // =============================================================================
14
+
15
+ export type AdType = 'linear' | 'nonlinear' | 'companion'
16
+ export type AdBreakType = 'preroll' | 'midroll' | 'postroll'
17
+
18
+ export interface AdConfig {
19
+ /** VAST tag URL or inline VAST XML */
20
+ vastUrl?: string
21
+ /** VAST XML content */
22
+ vastXml?: string
23
+ /** VMAP URL for ad schedule */
24
+ vmapUrl?: string
25
+ /** Ad schedule */
26
+ schedule?: AdBreak[]
27
+ /** Skip delay in seconds (0 = no skip, -1 = use VAST value) */
28
+ skipDelay?: number
29
+ /** Maximum number of redirects to follow */
30
+ maxRedirects?: number
31
+ /** Timeout for ad requests in ms */
32
+ timeout?: number
33
+ /** Enable VPAID */
34
+ vpaidEnabled?: boolean
35
+ /** Companion ad container selectors */
36
+ companionContainers?: Record<string, string>
37
+ /** Preload ads */
38
+ preloadAds?: boolean
39
+ /** Debug mode */
40
+ debug?: boolean
41
+ }
42
+
43
+ export interface AdBreak {
44
+ /** Break type */
45
+ type: AdBreakType
46
+ /** Time offset in seconds (for midrolls) */
47
+ timeOffset?: number
48
+ /** VAST URL */
49
+ vastUrl?: string
50
+ /** VAST XML */
51
+ vastXml?: string
52
+ /** Has played */
53
+ played?: boolean
54
+ }
55
+
56
+ export interface Ad {
57
+ /** Ad ID */
58
+ id: string
59
+ /** Ad type */
60
+ type: AdType
61
+ /** Ad system */
62
+ adSystem?: string
63
+ /** Ad title */
64
+ title?: string
65
+ /** Ad description */
66
+ description?: string
67
+ /** Ad duration in seconds */
68
+ duration: number
69
+ /** Skip offset in seconds */
70
+ skipOffset?: number
71
+ /** Click through URL */
72
+ clickThroughUrl?: string
73
+ /** Media files */
74
+ mediaFiles: AdMediaFile[]
75
+ /** Companion ads */
76
+ companions?: CompanionAd[]
77
+ /** Tracking events */
78
+ trackingEvents: AdTrackingEvent[]
79
+ /** VPAID script */
80
+ vpaidScript?: string
81
+ /** Icons */
82
+ icons?: AdIcon[]
83
+ }
84
+
85
+ export interface AdMediaFile {
86
+ /** Media URL */
87
+ url: string
88
+ /** MIME type */
89
+ type: string
90
+ /** Width */
91
+ width: number
92
+ /** Height */
93
+ height: number
94
+ /** Bitrate */
95
+ bitrate?: number
96
+ /** Codec */
97
+ codec?: string
98
+ /** Delivery type */
99
+ delivery: 'progressive' | 'streaming'
100
+ }
101
+
102
+ export interface CompanionAd {
103
+ /** Width */
104
+ width: number
105
+ /** Height */
106
+ height: number
107
+ /** Resource URL (image, iframe, HTML) */
108
+ resourceUrl: string
109
+ /** Resource type */
110
+ resourceType: 'static' | 'iframe' | 'html'
111
+ /** Click through URL */
112
+ clickThroughUrl?: string
113
+ /** Alt text */
114
+ altText?: string
115
+ /** Tracking pixel */
116
+ trackingUrl?: string
117
+ }
118
+
119
+ export interface AdTrackingEvent {
120
+ /** Event name */
121
+ event: string
122
+ /** Tracking URL */
123
+ url: string
124
+ /** Offset (for progress events) */
125
+ offset?: number
126
+ }
127
+
128
+ export interface AdIcon {
129
+ /** Icon URL */
130
+ url: string
131
+ /** Width */
132
+ width: number
133
+ /** Height */
134
+ height: number
135
+ /** Position */
136
+ xPosition: string
137
+ /** Position */
138
+ yPosition: string
139
+ /** Click URL */
140
+ clickUrl?: string
141
+ }
142
+
143
+ export interface AdState {
144
+ /** Is ad playing */
145
+ isPlaying: boolean
146
+ /** Current ad */
147
+ currentAd: Ad | null
148
+ /** Current ad break */
149
+ currentBreak: AdBreak | null
150
+ /** Time remaining */
151
+ timeRemaining: number
152
+ /** Can skip */
153
+ canSkip: boolean
154
+ /** Skip countdown */
155
+ skipCountdown: number
156
+ /** Is loading */
157
+ isLoading: boolean
158
+ /** Error */
159
+ error: string | null
160
+ }
161
+
162
+ // =============================================================================
163
+ // VAST Parser
164
+ // =============================================================================
165
+
166
+ export class VASTParser {
167
+ private maxRedirects: number
168
+ private timeout: number
169
+ private redirectCount = 0
170
+
171
+ constructor(options: { maxRedirects?: number; timeout?: number } = {}) {
172
+ this.maxRedirects = options.maxRedirects ?? 5
173
+ this.timeout = options.timeout ?? 10000
174
+ }
175
+
176
+ /**
177
+ * Parse VAST from URL
178
+ */
179
+ async parseFromUrl(url: string): Promise<Ad[]> {
180
+ this.redirectCount = 0
181
+ return this.fetchAndParse(url)
182
+ }
183
+
184
+ /**
185
+ * Parse VAST from XML string
186
+ */
187
+ parseFromXml(xml: string): Ad[] {
188
+ const parser = new DOMParser()
189
+ const doc = parser.parseFromString(xml, 'text/xml')
190
+ return this.parseDocument(doc)
191
+ }
192
+
193
+ /**
194
+ * Fetch and parse VAST
195
+ */
196
+ private async fetchAndParse(url: string): Promise<Ad[]> {
197
+ if (this.redirectCount >= this.maxRedirects) {
198
+ throw new Error('Too many VAST redirects')
199
+ }
200
+
201
+ const controller = new AbortController()
202
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout)
203
+
204
+ try {
205
+ const response = await fetch(url, { signal: controller.signal })
206
+ const text = await response.text()
207
+ const parser = new DOMParser()
208
+ const doc = parser.parseFromString(text, 'text/xml')
209
+
210
+ // Check for wrapper (redirect)
211
+ const wrapper = doc.querySelector('Wrapper')
212
+ if (wrapper) {
213
+ const vastAdTagUri = wrapper.querySelector('VASTAdTagURI')?.textContent?.trim()
214
+ if (vastAdTagUri) {
215
+ this.redirectCount++
216
+ return this.fetchAndParse(vastAdTagUri)
217
+ }
218
+ }
219
+
220
+ return this.parseDocument(doc)
221
+ }
222
+ finally {
223
+ clearTimeout(timeoutId)
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Parse VAST document
229
+ */
230
+ private parseDocument(doc: Document): Ad[] {
231
+ const ads: Ad[] = []
232
+ const adElements = doc.querySelectorAll('Ad')
233
+
234
+ adElements.forEach((adEl) => {
235
+ const inLine = adEl.querySelector('InLine')
236
+ if (!inLine) return
237
+
238
+ const ad: Ad = {
239
+ id: adEl.getAttribute('id') || this.generateId(),
240
+ type: 'linear',
241
+ adSystem: inLine.querySelector('AdSystem')?.textContent?.trim(),
242
+ title: inLine.querySelector('AdTitle')?.textContent?.trim(),
243
+ description: inLine.querySelector('Description')?.textContent?.trim(),
244
+ duration: 0,
245
+ mediaFiles: [],
246
+ trackingEvents: [],
247
+ }
248
+
249
+ // Parse Linear
250
+ const linear = inLine.querySelector('Linear')
251
+ if (linear) {
252
+ // Duration
253
+ const durationStr = linear.querySelector('Duration')?.textContent?.trim()
254
+ if (durationStr) {
255
+ ad.duration = this.parseDuration(durationStr)
256
+ }
257
+
258
+ // Skip offset
259
+ const skipOffset = linear.getAttribute('skipoffset')
260
+ if (skipOffset) {
261
+ ad.skipOffset = this.parseOffset(skipOffset, ad.duration)
262
+ }
263
+
264
+ // Media files
265
+ linear.querySelectorAll('MediaFile').forEach((mf) => {
266
+ const url = mf.textContent?.trim()
267
+ if (url) {
268
+ ad.mediaFiles.push({
269
+ url,
270
+ type: mf.getAttribute('type') || 'video/mp4',
271
+ width: Number.parseInt(mf.getAttribute('width') || '0', 10),
272
+ height: Number.parseInt(mf.getAttribute('height') || '0', 10),
273
+ bitrate: mf.getAttribute('bitrate')
274
+ ? Number.parseInt(mf.getAttribute('bitrate')!, 10)
275
+ : undefined,
276
+ delivery: (mf.getAttribute('delivery') as any) || 'progressive',
277
+ })
278
+ }
279
+ })
280
+
281
+ // Click through
282
+ const clickThrough = linear.querySelector('VideoClicks > ClickThrough')
283
+ if (clickThrough) {
284
+ ad.clickThroughUrl = clickThrough.textContent?.trim()
285
+ }
286
+
287
+ // Tracking events
288
+ linear.querySelectorAll('TrackingEvents > Tracking').forEach((tracking) => {
289
+ const event = tracking.getAttribute('event')
290
+ const url = tracking.textContent?.trim()
291
+ if (event && url) {
292
+ const trackingEvent: AdTrackingEvent = { event, url }
293
+
294
+ // Handle progress offset
295
+ if (event === 'progress') {
296
+ const offset = tracking.getAttribute('offset')
297
+ if (offset) {
298
+ trackingEvent.offset = this.parseOffset(offset, ad.duration)
299
+ }
300
+ }
301
+
302
+ ad.trackingEvents.push(trackingEvent)
303
+ }
304
+ })
305
+
306
+ // Icons
307
+ linear.querySelectorAll('Icons > Icon').forEach((icon) => {
308
+ const staticResource = icon.querySelector('StaticResource')
309
+ if (staticResource?.textContent) {
310
+ ad.icons = ad.icons || []
311
+ ad.icons.push({
312
+ url: staticResource.textContent.trim(),
313
+ width: Number.parseInt(icon.getAttribute('width') || '0', 10),
314
+ height: Number.parseInt(icon.getAttribute('height') || '0', 10),
315
+ xPosition: icon.getAttribute('xPosition') || 'right',
316
+ yPosition: icon.getAttribute('yPosition') || 'top',
317
+ clickUrl: icon.querySelector('IconClicks > IconClickThrough')?.textContent?.trim(),
318
+ })
319
+ }
320
+ })
321
+ }
322
+
323
+ // Parse Companion ads
324
+ inLine.querySelectorAll('CompanionAds > Companion').forEach((comp) => {
325
+ ad.companions = ad.companions || []
326
+
327
+ const staticResource = comp.querySelector('StaticResource')
328
+ const iframeResource = comp.querySelector('IFrameResource')
329
+ const htmlResource = comp.querySelector('HTMLResource')
330
+
331
+ let resourceUrl = ''
332
+ let resourceType: CompanionAd['resourceType'] = 'static'
333
+
334
+ if (staticResource?.textContent) {
335
+ resourceUrl = staticResource.textContent.trim()
336
+ resourceType = 'static'
337
+ }
338
+ else if (iframeResource?.textContent) {
339
+ resourceUrl = iframeResource.textContent.trim()
340
+ resourceType = 'iframe'
341
+ }
342
+ else if (htmlResource?.textContent) {
343
+ resourceUrl = htmlResource.textContent.trim()
344
+ resourceType = 'html'
345
+ }
346
+
347
+ if (resourceUrl) {
348
+ ad.companions.push({
349
+ width: Number.parseInt(comp.getAttribute('width') || '0', 10),
350
+ height: Number.parseInt(comp.getAttribute('height') || '0', 10),
351
+ resourceUrl,
352
+ resourceType,
353
+ clickThroughUrl: comp.querySelector('CompanionClickThrough')?.textContent?.trim(),
354
+ altText: comp.getAttribute('altText') || undefined,
355
+ trackingUrl: comp.querySelector('TrackingEvents > Tracking')?.textContent?.trim(),
356
+ })
357
+ }
358
+ })
359
+
360
+ if (ad.mediaFiles.length > 0) {
361
+ ads.push(ad)
362
+ }
363
+ })
364
+
365
+ return ads
366
+ }
367
+
368
+ /**
369
+ * Parse duration string (HH:MM:SS.mmm)
370
+ */
371
+ private parseDuration(duration: string): number {
372
+ const parts = duration.split(':')
373
+ if (parts.length !== 3) return 0
374
+
375
+ const hours = Number.parseFloat(parts[0])
376
+ const minutes = Number.parseFloat(parts[1])
377
+ const seconds = Number.parseFloat(parts[2])
378
+
379
+ return hours * 3600 + minutes * 60 + seconds
380
+ }
381
+
382
+ /**
383
+ * Parse offset (time or percentage)
384
+ */
385
+ private parseOffset(offset: string, duration: number): number {
386
+ if (offset.endsWith('%')) {
387
+ const percent = Number.parseFloat(offset) / 100
388
+ return duration * percent
389
+ }
390
+ return this.parseDuration(offset)
391
+ }
392
+
393
+ /**
394
+ * Generate unique ID
395
+ */
396
+ private generateId(): string {
397
+ return `ad-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
398
+ }
399
+ }
400
+
401
+ // =============================================================================
402
+ // Ads Manager
403
+ // =============================================================================
404
+
405
+ export class AdsManager {
406
+ private player: Player | null = null
407
+ private config: AdConfig
408
+ private parser: VASTParser
409
+ private state: AdState
410
+ private container: HTMLElement | null = null
411
+ private adVideo: HTMLVideoElement | null = null
412
+ private adContainer: HTMLElement | null = null
413
+ private skipButton: HTMLButtonElement | null = null
414
+ private adBreaks: AdBreak[] = []
415
+ private currentAds: Ad[] = []
416
+ private currentAdIndex = 0
417
+ private skipTimer: ReturnType<typeof setInterval> | null = null
418
+ private contentPaused = false
419
+
420
+ // Event handlers
421
+ // eslint-disable-next-line pickier/no-unused-vars
422
+ private onAdStart: ((_ad: Ad) => void) | null = null
423
+ // eslint-disable-next-line pickier/no-unused-vars
424
+ private onAdComplete: ((_ad: Ad) => void) | null = null
425
+ // eslint-disable-next-line pickier/no-unused-vars
426
+ private onAdSkip: ((_ad: Ad) => void) | null = null
427
+ // eslint-disable-next-line pickier/no-unused-vars
428
+ private onAdClick: ((_ad: Ad) => void) | null = null
429
+ // eslint-disable-next-line pickier/no-unused-vars
430
+ private onAdErrorCallback: ((_error: string) => void) | null = null
431
+ private onAllAdsComplete: (() => void) | null = null
432
+
433
+ constructor(config: AdConfig = {}) {
434
+ this.config = {
435
+ skipDelay: 5,
436
+ maxRedirects: 5,
437
+ timeout: 10000,
438
+ vpaidEnabled: false,
439
+ preloadAds: true,
440
+ debug: false,
441
+ ...config,
442
+ }
443
+
444
+ this.parser = new VASTParser({
445
+ maxRedirects: this.config.maxRedirects,
446
+ timeout: this.config.timeout,
447
+ })
448
+
449
+ this.state = {
450
+ isPlaying: false,
451
+ currentAd: null,
452
+ currentBreak: null,
453
+ timeRemaining: 0,
454
+ canSkip: false,
455
+ skipCountdown: 0,
456
+ isLoading: false,
457
+ error: null,
458
+ }
459
+
460
+ // Parse schedule
461
+ if (this.config.schedule) {
462
+ this.adBreaks = this.config.schedule
463
+ }
464
+ else if (this.config.vastUrl || this.config.vastXml) {
465
+ // Single preroll
466
+ this.adBreaks = [{
467
+ type: 'preroll',
468
+ vastUrl: this.config.vastUrl,
469
+ vastXml: this.config.vastXml,
470
+ }]
471
+ }
472
+ }
473
+
474
+ /**
475
+ * Attach to player
476
+ */
477
+ attach(player: Player, container: HTMLElement): void {
478
+ this.player = player
479
+ this.container = container
480
+
481
+ this.createAdContainer()
482
+ this.attachPlayerEvents()
483
+
484
+ // Preload ads
485
+ if (this.config.preloadAds) {
486
+ this.preloadAds()
487
+ }
488
+ }
489
+
490
+ /**
491
+ * Create ad container
492
+ */
493
+ private createAdContainer(): void {
494
+ if (!this.container) return
495
+
496
+ this.adContainer = document.createElement('div')
497
+ this.adContainer.className = 'tsvp-ad-container'
498
+ this.adContainer.style.cssText = `
499
+ position: absolute;
500
+ inset: 0;
501
+ z-index: 50;
502
+ display: none;
503
+ background: #000;
504
+ `
505
+
506
+ // Ad video
507
+ this.adVideo = document.createElement('video')
508
+ this.adVideo.style.cssText = `
509
+ width: 100%;
510
+ height: 100%;
511
+ object-fit: contain;
512
+ `
513
+ this.adVideo.playsInline = true
514
+
515
+ // Ad overlay
516
+ const overlay = document.createElement('div')
517
+ overlay.className = 'tsvp-ad-overlay'
518
+ overlay.style.cssText = `
519
+ position: absolute;
520
+ inset: 0;
521
+ display: flex;
522
+ flex-direction: column;
523
+ pointer-events: none;
524
+ `
525
+
526
+ // Ad info
527
+ const adInfo = document.createElement('div')
528
+ adInfo.className = 'tsvp-ad-info'
529
+ adInfo.style.cssText = `
530
+ position: absolute;
531
+ top: 12px;
532
+ left: 12px;
533
+ padding: 6px 12px;
534
+ background: rgba(0, 0, 0, 0.7);
535
+ color: #fff;
536
+ font-size: 12px;
537
+ font-family: var(--tsvp-font-family, sans-serif);
538
+ border-radius: 4px;
539
+ `
540
+ adInfo.textContent = 'Ad'
541
+
542
+ // Skip button
543
+ this.skipButton = document.createElement('button')
544
+ this.skipButton.className = 'tsvp-ad-skip'
545
+ this.skipButton.style.cssText = `
546
+ position: absolute;
547
+ bottom: 80px;
548
+ right: 12px;
549
+ padding: 10px 16px;
550
+ background: rgba(0, 0, 0, 0.8);
551
+ color: #fff;
552
+ border: 1px solid rgba(255, 255, 255, 0.3);
553
+ border-radius: 4px;
554
+ font-size: 14px;
555
+ font-family: var(--tsvp-font-family, sans-serif);
556
+ cursor: pointer;
557
+ pointer-events: auto;
558
+ display: none;
559
+ `
560
+ this.skipButton.addEventListener('click', () => this.skipAd())
561
+
562
+ // Learn more button
563
+ const learnMore = document.createElement('button')
564
+ learnMore.className = 'tsvp-ad-learn-more'
565
+ learnMore.style.cssText = `
566
+ position: absolute;
567
+ bottom: 80px;
568
+ left: 12px;
569
+ padding: 10px 16px;
570
+ background: var(--tsvp-color-primary, #00a8ff);
571
+ color: #fff;
572
+ border: none;
573
+ border-radius: 4px;
574
+ font-size: 14px;
575
+ font-family: var(--tsvp-font-family, sans-serif);
576
+ cursor: pointer;
577
+ pointer-events: auto;
578
+ display: none;
579
+ `
580
+ learnMore.textContent = 'Learn More'
581
+ learnMore.addEventListener('click', () => this.handleAdClick())
582
+
583
+ overlay.appendChild(adInfo)
584
+ overlay.appendChild(this.skipButton)
585
+ overlay.appendChild(learnMore)
586
+
587
+ this.adContainer.appendChild(this.adVideo)
588
+ this.adContainer.appendChild(overlay)
589
+ this.container.appendChild(this.adContainer)
590
+
591
+ // Ad video events
592
+ this.adVideo.addEventListener('timeupdate', () => this.onAdTimeUpdate())
593
+ this.adVideo.addEventListener('ended', () => this.onAdEnded())
594
+ this.adVideo.addEventListener('error', () => this.handleAdError())
595
+ }
596
+
597
+ /**
598
+ * Attach player events
599
+ */
600
+ private attachPlayerEvents(): void {
601
+ if (!this.player) return
602
+
603
+ // Check for preroll on first play
604
+ let firstPlay = true
605
+ this.player.on('play', async () => {
606
+ if (firstPlay) {
607
+ firstPlay = false
608
+ const preroll = this.adBreaks.find(b => b.type === 'preroll' && !b.played)
609
+ if (preroll) {
610
+ await this.playAdBreak(preroll)
611
+ }
612
+ }
613
+ })
614
+
615
+ // Check for midrolls
616
+ this.player.on('timeupdate', () => {
617
+ const state = this.player?.state
618
+ if (!state) return
619
+
620
+ for (const adBreak of this.adBreaks) {
621
+ if (
622
+ adBreak.type === 'midroll'
623
+ && !adBreak.played
624
+ && adBreak.timeOffset
625
+ && state.currentTime >= adBreak.timeOffset
626
+ ) {
627
+ this.playAdBreak(adBreak)
628
+ break
629
+ }
630
+ }
631
+ })
632
+
633
+ // Check for postroll
634
+ this.player.on('ended', async () => {
635
+ const postroll = this.adBreaks.find(b => b.type === 'postroll' && !b.played)
636
+ if (postroll) {
637
+ await this.playAdBreak(postroll)
638
+ }
639
+ })
640
+ }
641
+
642
+ /**
643
+ * Preload ads
644
+ */
645
+ private async preloadAds(): Promise<void> {
646
+ for (const adBreak of this.adBreaks) {
647
+ if (adBreak.vastUrl) {
648
+ try {
649
+ // Pre-parse VAST
650
+ await this.parser.parseFromUrl(adBreak.vastUrl)
651
+ }
652
+ catch (error) {
653
+ this.log('Failed to preload ad:', error)
654
+ }
655
+ }
656
+ }
657
+ }
658
+
659
+ /**
660
+ * Play ad break
661
+ */
662
+ async playAdBreak(adBreak: AdBreak): Promise<void> {
663
+ if (this.state.isPlaying) return
664
+
665
+ this.state.currentBreak = adBreak
666
+ this.state.isLoading = true
667
+
668
+ // Pause content
669
+ this.pauseContent()
670
+
671
+ try {
672
+ // Parse VAST
673
+ if (adBreak.vastUrl) {
674
+ this.currentAds = await this.parser.parseFromUrl(adBreak.vastUrl)
675
+ }
676
+ else if (adBreak.vastXml) {
677
+ this.currentAds = this.parser.parseFromXml(adBreak.vastXml)
678
+ }
679
+
680
+ if (this.currentAds.length === 0) {
681
+ throw new Error('No ads found in VAST response')
682
+ }
683
+
684
+ this.currentAdIndex = 0
685
+ adBreak.played = true
686
+
687
+ await this.playAd(this.currentAds[0])
688
+ }
689
+ catch (error) {
690
+ this.state.error = (error as Error).message
691
+ this.state.isLoading = false
692
+ this.onAdErrorCallback?.((error as Error).message)
693
+ this.resumeContent()
694
+ }
695
+ }
696
+
697
+ /**
698
+ * Play single ad
699
+ */
700
+ private async playAd(ad: Ad): Promise<void> {
701
+ if (!this.adVideo || !this.adContainer) return
702
+
703
+ this.state.currentAd = ad
704
+ this.state.isPlaying = true
705
+ this.state.isLoading = false
706
+ this.state.timeRemaining = ad.duration
707
+ this.state.canSkip = false
708
+ this.state.skipCountdown = this.config.skipDelay || 0
709
+
710
+ // Select best media file
711
+ const mediaFile = this.selectMediaFile(ad.mediaFiles)
712
+ if (!mediaFile) {
713
+ throw new Error('No suitable media file found')
714
+ }
715
+
716
+ // Show ad container
717
+ this.adContainer.style.display = 'block'
718
+
719
+ // Set source and play
720
+ this.adVideo.src = mediaFile.url
721
+ await this.adVideo.play()
722
+
723
+ // Track impression
724
+ this.trackEvent(ad, 'impression')
725
+ this.trackEvent(ad, 'start')
726
+
727
+ // Start skip timer
728
+ this.startSkipTimer(ad)
729
+
730
+ this.onAdStart?.(ad)
731
+ }
732
+
733
+ /**
734
+ * Select best media file
735
+ */
736
+ private selectMediaFile(mediaFiles: AdMediaFile[]): AdMediaFile | null {
737
+ // Sort by bitrate (highest first) and prefer mp4
738
+ const sorted = [...mediaFiles].sort((a, b) => {
739
+ if (a.type.includes('mp4') && !b.type.includes('mp4')) return -1
740
+ if (!a.type.includes('mp4') && b.type.includes('mp4')) return 1
741
+ return (b.bitrate || 0) - (a.bitrate || 0)
742
+ })
743
+
744
+ return sorted[0] || null
745
+ }
746
+
747
+ /**
748
+ * Start skip timer
749
+ */
750
+ private startSkipTimer(ad: Ad): void {
751
+ const skipDelay = this.config.skipDelay === -1
752
+ ? (ad.skipOffset || 5)
753
+ : (this.config.skipDelay || 0)
754
+
755
+ if (skipDelay <= 0) {
756
+ this.state.canSkip = true
757
+ this.updateSkipButton()
758
+ return
759
+ }
760
+
761
+ this.state.skipCountdown = skipDelay
762
+
763
+ this.skipTimer = setInterval(() => {
764
+ this.state.skipCountdown--
765
+
766
+ if (this.state.skipCountdown <= 0) {
767
+ this.state.canSkip = true
768
+ if (this.skipTimer) {
769
+ clearInterval(this.skipTimer)
770
+ this.skipTimer = null
771
+ }
772
+ }
773
+
774
+ this.updateSkipButton()
775
+ }, 1000)
776
+ }
777
+
778
+ /**
779
+ * Update skip button
780
+ */
781
+ private updateSkipButton(): void {
782
+ if (!this.skipButton) return
783
+
784
+ if (this.state.canSkip) {
785
+ this.skipButton.textContent = 'Skip Ad'
786
+ this.skipButton.style.display = 'block'
787
+ }
788
+ else if (this.state.skipCountdown > 0) {
789
+ this.skipButton.textContent = `Skip in ${this.state.skipCountdown}s`
790
+ this.skipButton.style.display = 'block'
791
+ this.skipButton.style.cursor = 'default'
792
+ }
793
+ else {
794
+ this.skipButton.style.display = 'none'
795
+ }
796
+ }
797
+
798
+ /**
799
+ * Skip current ad
800
+ */
801
+ skipAd(): void {
802
+ if (!this.state.canSkip || !this.state.currentAd) return
803
+
804
+ this.trackEvent(this.state.currentAd, 'skip')
805
+ this.onAdSkip?.(this.state.currentAd)
806
+ this.onAdEnded()
807
+ }
808
+
809
+ /**
810
+ * Handle ad click
811
+ */
812
+ private handleAdClick(): void {
813
+ if (!this.state.currentAd?.clickThroughUrl) return
814
+
815
+ // Pause ad
816
+ this.adVideo?.pause()
817
+
818
+ // Track click
819
+ this.trackEvent(this.state.currentAd, 'clickThrough')
820
+ this.onAdClick?.(this.state.currentAd)
821
+
822
+ // Open URL
823
+ window.open(this.state.currentAd.clickThroughUrl, '_blank')
824
+ }
825
+
826
+ /**
827
+ * Handle ad time update
828
+ */
829
+ private onAdTimeUpdate(): void {
830
+ if (!this.adVideo || !this.state.currentAd) return
831
+
832
+ const currentTime = this.adVideo.currentTime
833
+ const duration = this.state.currentAd.duration
834
+
835
+ this.state.timeRemaining = Math.max(0, duration - currentTime)
836
+
837
+ // Track quartiles
838
+ const progress = currentTime / duration
839
+ if (progress >= 0.25) this.trackEvent(this.state.currentAd, 'firstQuartile')
840
+ if (progress >= 0.5) this.trackEvent(this.state.currentAd, 'midpoint')
841
+ if (progress >= 0.75) this.trackEvent(this.state.currentAd, 'thirdQuartile')
842
+
843
+ // Track progress events
844
+ for (const event of this.state.currentAd.trackingEvents) {
845
+ if (event.event === 'progress' && event.offset && currentTime >= event.offset) {
846
+ this.trackUrl(event.url)
847
+ }
848
+ }
849
+ }
850
+
851
+ /**
852
+ * Handle ad ended
853
+ */
854
+ private onAdEnded(): void {
855
+ if (this.state.currentAd) {
856
+ this.trackEvent(this.state.currentAd, 'complete')
857
+ this.onAdComplete?.(this.state.currentAd)
858
+ }
859
+
860
+ // Clean up skip timer
861
+ if (this.skipTimer) {
862
+ clearInterval(this.skipTimer)
863
+ this.skipTimer = null
864
+ }
865
+
866
+ // Play next ad or resume content
867
+ this.currentAdIndex++
868
+ if (this.currentAdIndex < this.currentAds.length) {
869
+ this.playAd(this.currentAds[this.currentAdIndex])
870
+ }
871
+ else {
872
+ this.finishAdBreak()
873
+ }
874
+ }
875
+
876
+ /**
877
+ * Handle ad error
878
+ */
879
+ private handleAdError(): void {
880
+ const error = 'Ad playback failed'
881
+ this.state.error = error
882
+ this.onAdErrorCallback?.(error)
883
+ this.trackEvent(this.state.currentAd!, 'error')
884
+ this.finishAdBreak()
885
+ }
886
+
887
+ /**
888
+ * Finish ad break
889
+ */
890
+ private finishAdBreak(): void {
891
+ this.state.isPlaying = false
892
+ this.state.currentAd = null
893
+ this.state.currentBreak = null
894
+ this.currentAds = []
895
+ this.currentAdIndex = 0
896
+
897
+ // Hide ad container
898
+ if (this.adContainer) {
899
+ this.adContainer.style.display = 'none'
900
+ }
901
+
902
+ this.onAllAdsComplete?.()
903
+ this.resumeContent()
904
+ }
905
+
906
+ /**
907
+ * Pause content
908
+ */
909
+ private pauseContent(): void {
910
+ if (!this.player) return
911
+
912
+ const state = this.player.state
913
+ if (state.playbackState === 'playing') {
914
+ this.contentPaused = true
915
+ this.player.pause()
916
+ }
917
+ }
918
+
919
+ /**
920
+ * Resume content
921
+ */
922
+ private resumeContent(): void {
923
+ if (!this.player) return
924
+
925
+ if (this.contentPaused) {
926
+ this.contentPaused = false
927
+ this.player.play()
928
+ }
929
+ }
930
+
931
+ /**
932
+ * Track ad event
933
+ */
934
+ private trackEvent(ad: Ad, eventName: string): void {
935
+ const events = ad.trackingEvents.filter(e => e.event === eventName)
936
+ for (const event of events) {
937
+ this.trackUrl(event.url)
938
+ }
939
+ }
940
+
941
+ /**
942
+ * Track URL (fire pixel)
943
+ */
944
+ private trackUrl(url: string): void {
945
+ const img = new Image()
946
+ img.src = url
947
+ }
948
+
949
+ /**
950
+ * Log debug message
951
+ */
952
+ private log(...args: unknown[]): void {
953
+ if (this.config.debug) {
954
+ // eslint-disable-next-line no-console
955
+ console.log('[VideoAds]', ...args)
956
+ }
957
+ }
958
+
959
+ /**
960
+ * Get current state
961
+ */
962
+ getState(): AdState {
963
+ return { ...this.state }
964
+ }
965
+
966
+ /**
967
+ * Set event handlers
968
+ */
969
+ on(event: 'adStart', handler: (ad: Ad) => void): void
970
+ on(event: 'adComplete', handler: (ad: Ad) => void): void
971
+ on(event: 'adSkip', handler: (ad: Ad) => void): void
972
+ on(event: 'adClick', handler: (ad: Ad) => void): void
973
+ on(event: 'adError', handler: (error: string) => void): void
974
+ on(event: 'allAdsComplete', handler: () => void): void
975
+ on(event: string, handler: (...args: any[]) => void): void {
976
+ switch (event) {
977
+ case 'adStart':
978
+ this.onAdStart = handler
979
+ break
980
+ case 'adComplete':
981
+ this.onAdComplete = handler
982
+ break
983
+ case 'adSkip':
984
+ this.onAdSkip = handler
985
+ break
986
+ case 'adClick':
987
+ this.onAdClick = handler
988
+ break
989
+ case 'adError':
990
+ this.onAdErrorCallback = handler
991
+ break
992
+ case 'allAdsComplete':
993
+ this.onAllAdsComplete = handler
994
+ break
995
+ }
996
+ }
997
+
998
+ /**
999
+ * Destroy ads manager
1000
+ */
1001
+ destroy(): void {
1002
+ if (this.skipTimer) {
1003
+ clearInterval(this.skipTimer)
1004
+ }
1005
+
1006
+ if (this.adContainer) {
1007
+ this.adContainer.remove()
1008
+ }
1009
+
1010
+ this.player = null
1011
+ this.container = null
1012
+ this.adVideo = null
1013
+ this.adContainer = null
1014
+ }
1015
+ }
1016
+
1017
+ // =============================================================================
1018
+ // Factory
1019
+ // =============================================================================
1020
+
1021
+ export function createAdsManager(config?: AdConfig): AdsManager {
1022
+ return new AdsManager(config)
1023
+ }
1024
+
1025
+ /**
1026
+ * Ads plugin for Player
1027
+ */
1028
+ export function adsPlugin(config: AdConfig): { name: string, install: (player: Player, container: HTMLElement) => () => void } {
1029
+ return {
1030
+ name: 'ads',
1031
+
1032
+ install(player: Player, container: HTMLElement) {
1033
+ const ads = createAdsManager(config)
1034
+ ads.attach(player, container)
1035
+
1036
+ // Expose on player
1037
+ ;(player as any).ads = ads
1038
+
1039
+ return () => {
1040
+ ads.destroy()
1041
+ delete (player as any).ads
1042
+ }
1043
+ },
1044
+ }
1045
+ }