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,537 @@
1
+ /**
2
+ * Watermark Plugin
3
+ *
4
+ * Display watermarks on video playback.
5
+ *
6
+ * @module plugins/watermark
7
+ */
8
+
9
+ import type { Player } from '../player'
10
+
11
+ // =============================================================================
12
+ // Types
13
+ // =============================================================================
14
+
15
+ export type WatermarkPosition =
16
+ | 'top-left'
17
+ | 'top-center'
18
+ | 'top-right'
19
+ | 'center-left'
20
+ | 'center'
21
+ | 'center-right'
22
+ | 'bottom-left'
23
+ | 'bottom-center'
24
+ | 'bottom-right'
25
+
26
+ export interface WatermarkConfig {
27
+ /** Image URL */
28
+ image?: string
29
+ /** Text content */
30
+ text?: string
31
+ /** HTML content */
32
+ html?: string
33
+ /** Position */
34
+ position?: WatermarkPosition
35
+ /** Opacity (0-1) */
36
+ opacity?: number
37
+ /** Size (CSS value) */
38
+ size?: string
39
+ /** Offset from edges */
40
+ offset?: {
41
+ x?: number | string
42
+ y?: number | string
43
+ }
44
+ /** Link URL */
45
+ url?: string
46
+ /** Open link in new tab */
47
+ newTab?: boolean
48
+ /** Show only during playback */
49
+ showOnlyDuringPlayback?: boolean
50
+ /** Fade in duration (ms) */
51
+ fadeIn?: number
52
+ /** Fade out duration (ms) */
53
+ fadeOut?: number
54
+ /** Animation */
55
+ animation?: WatermarkAnimation
56
+ /** Custom styles */
57
+ styles?: Partial<CSSStyleDeclaration>
58
+ /** Rotate degrees */
59
+ rotate?: number
60
+ }
61
+
62
+ export interface WatermarkAnimation {
63
+ /** Animation type */
64
+ type: 'fade' | 'slide' | 'bounce' | 'pulse' | 'none'
65
+ /** Animation duration (ms) */
66
+ duration?: number
67
+ /** Animation delay (ms) */
68
+ delay?: number
69
+ /** Repeat animation */
70
+ repeat?: boolean
71
+ }
72
+
73
+ export interface DynamicWatermark {
74
+ /** User ID or session info */
75
+ userId?: string
76
+ /** Timestamp */
77
+ showTimestamp?: boolean
78
+ /** IP address (for forensic marking) */
79
+ showIp?: boolean
80
+ /** Custom text */
81
+ customText?: string
82
+ /** Update interval (ms) */
83
+ updateInterval?: number
84
+ /** Position randomization */
85
+ randomizePosition?: boolean
86
+ /** Move interval (ms) */
87
+ moveInterval?: number
88
+ }
89
+
90
+ // =============================================================================
91
+ // Watermark Manager
92
+ // =============================================================================
93
+
94
+ export class WatermarkManager {
95
+ private player: Player | null = null
96
+ private container: HTMLElement | null = null
97
+ private elements: Map<string, HTMLElement> = new Map()
98
+ private dynamicTimers: Map<string, ReturnType<typeof setInterval>> = new Map()
99
+ private unsubscribers: Array<() => void> = []
100
+
101
+ constructor() {}
102
+
103
+ /**
104
+ * Attach to player
105
+ */
106
+ attach(player: Player, container: HTMLElement): void {
107
+ this.player = player
108
+ this.container = container
109
+ }
110
+
111
+ /**
112
+ * Add a watermark
113
+ */
114
+ add(id: string, config: WatermarkConfig): HTMLElement {
115
+ if (!this.container) {
116
+ throw new Error('Watermark manager not attached to container')
117
+ }
118
+
119
+ // Remove existing watermark with same ID
120
+ this.remove(id)
121
+
122
+ const element = this.createElement(config)
123
+ element.dataset.watermarkId = id
124
+
125
+ this.elements.set(id, element)
126
+ this.container.appendChild(element)
127
+
128
+ // Handle playback visibility
129
+ if (config.showOnlyDuringPlayback && this.player) {
130
+ this.setupPlaybackVisibility(id, element)
131
+ }
132
+
133
+ // Apply animation
134
+ if (config.animation && config.animation.type !== 'none') {
135
+ this.applyAnimation(element, config.animation)
136
+ }
137
+
138
+ return element
139
+ }
140
+
141
+ /**
142
+ * Create watermark element
143
+ */
144
+ private createElement(config: WatermarkConfig): HTMLElement {
145
+ const wrapper = document.createElement('div')
146
+ wrapper.className = 'tsvp-watermark'
147
+
148
+ const positionStyles = this.getPositionStyles(config.position || 'bottom-right', config.offset)
149
+
150
+ wrapper.style.cssText = `
151
+ position: absolute;
152
+ ${positionStyles}
153
+ z-index: 25;
154
+ pointer-events: ${config.url ? 'auto' : 'none'};
155
+ opacity: ${config.opacity ?? 0.7};
156
+ transition: opacity ${config.fadeIn || 300}ms ease;
157
+ ${config.rotate ? `transform: rotate(${config.rotate}deg);` : ''}
158
+ `
159
+
160
+ // Apply custom styles
161
+ if (config.styles) {
162
+ Object.assign(wrapper.style, config.styles)
163
+ }
164
+
165
+ // Create content
166
+ let content: HTMLElement
167
+
168
+ if (config.html) {
169
+ content = document.createElement('div')
170
+ content.innerHTML = config.html
171
+ }
172
+ else if (config.image) {
173
+ const img = document.createElement('img')
174
+ img.src = config.image
175
+ img.alt = 'Watermark'
176
+ img.style.cssText = `
177
+ display: block;
178
+ max-width: 100%;
179
+ height: auto;
180
+ ${config.size ? `width: ${config.size};` : ''}
181
+ `
182
+ content = img
183
+ }
184
+ else if (config.text) {
185
+ content = document.createElement('span')
186
+ content.textContent = config.text
187
+ content.style.cssText = `
188
+ font-family: var(--tsvp-font-family, sans-serif);
189
+ font-size: ${config.size || '14px'};
190
+ color: rgba(255, 255, 255, 0.8);
191
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
192
+ `
193
+ }
194
+ else {
195
+ content = document.createElement('span')
196
+ }
197
+
198
+ // Wrap in link if URL provided
199
+ if (config.url) {
200
+ const link = document.createElement('a')
201
+ link.href = config.url
202
+ link.target = config.newTab ? '_blank' : '_self'
203
+ link.rel = config.newTab ? 'noopener noreferrer' : ''
204
+ link.style.textDecoration = 'none'
205
+ link.appendChild(content)
206
+ wrapper.appendChild(link)
207
+ }
208
+ else {
209
+ wrapper.appendChild(content)
210
+ }
211
+
212
+ return wrapper
213
+ }
214
+
215
+ /**
216
+ * Get position styles
217
+ */
218
+ private getPositionStyles(
219
+ position: WatermarkPosition,
220
+ offset?: { x?: number | string; y?: number | string }
221
+ ): string {
222
+ const offsetX = typeof offset?.x === 'number' ? `${offset.x}px` : (offset?.x || '16px')
223
+ const offsetY = typeof offset?.y === 'number' ? `${offset.y}px` : (offset?.y || '16px')
224
+
225
+ const positions: Record<WatermarkPosition, string> = {
226
+ 'top-left': `top: ${offsetY}; left: ${offsetX};`,
227
+ 'top-center': `top: ${offsetY}; left: 50%; transform: translateX(-50%);`,
228
+ 'top-right': `top: ${offsetY}; right: ${offsetX};`,
229
+ 'center-left': `top: 50%; left: ${offsetX}; transform: translateY(-50%);`,
230
+ 'center': `top: 50%; left: 50%; transform: translate(-50%, -50%);`,
231
+ 'center-right': `top: 50%; right: ${offsetX}; transform: translateY(-50%);`,
232
+ 'bottom-left': `bottom: ${offsetY}; left: ${offsetX};`,
233
+ 'bottom-center': `bottom: ${offsetY}; left: 50%; transform: translateX(-50%);`,
234
+ 'bottom-right': `bottom: ${offsetY}; right: ${offsetX};`,
235
+ }
236
+
237
+ return positions[position] || positions['bottom-right']
238
+ }
239
+
240
+ /**
241
+ * Setup playback visibility
242
+ */
243
+ private setupPlaybackVisibility(_id: string, element: HTMLElement): void {
244
+ if (!this.player) return
245
+
246
+ element.style.opacity = '0'
247
+
248
+ const handlePlay = () => {
249
+ element.style.opacity = ''
250
+ }
251
+
252
+ const handlePause = () => {
253
+ element.style.opacity = '0'
254
+ }
255
+
256
+ this.player.on('play', handlePlay)
257
+ this.player.on('pause', handlePause)
258
+ this.player.on('ended', handlePause)
259
+
260
+ this.unsubscribers.push(() => {
261
+ this.player?.off('play', handlePlay)
262
+ this.player?.off('pause', handlePause)
263
+ this.player?.off('ended', handlePause)
264
+ })
265
+ }
266
+
267
+ /**
268
+ * Apply animation to watermark
269
+ */
270
+ private applyAnimation(element: HTMLElement, animation: WatermarkAnimation): void {
271
+ const duration = animation.duration || 1000
272
+ const delay = animation.delay || 0
273
+ const repeat = animation.repeat ? 'infinite' : '1'
274
+
275
+ // Add keyframes if not exists
276
+ if (!document.querySelector('#tsvp-watermark-animations')) {
277
+ const style = document.createElement('style')
278
+ style.id = 'tsvp-watermark-animations'
279
+ style.textContent = `
280
+ @keyframes tsvp-watermark-fade {
281
+ 0%, 100% { opacity: 0.3; }
282
+ 50% { opacity: 1; }
283
+ }
284
+ @keyframes tsvp-watermark-slide {
285
+ 0% { transform: translateX(-10px); }
286
+ 50% { transform: translateX(10px); }
287
+ 100% { transform: translateX(-10px); }
288
+ }
289
+ @keyframes tsvp-watermark-bounce {
290
+ 0%, 100% { transform: translateY(0); }
291
+ 50% { transform: translateY(-10px); }
292
+ }
293
+ @keyframes tsvp-watermark-pulse {
294
+ 0%, 100% { transform: scale(1); }
295
+ 50% { transform: scale(1.1); }
296
+ }
297
+ `
298
+ document.head.appendChild(style)
299
+ }
300
+
301
+ const animationName = `tsvp-watermark-${animation.type}`
302
+ element.style.animation = `${animationName} ${duration}ms ease ${delay}ms ${repeat}`
303
+ }
304
+
305
+ /**
306
+ * Add dynamic watermark (forensic marking)
307
+ */
308
+ addDynamic(id: string, config: DynamicWatermark, baseConfig: WatermarkConfig = {}): HTMLElement {
309
+ const getText = (): string => {
310
+ const parts: string[] = []
311
+
312
+ if (config.userId) {
313
+ parts.push(config.userId)
314
+ }
315
+
316
+ if (config.showTimestamp) {
317
+ parts.push(new Date().toISOString())
318
+ }
319
+
320
+ if (config.customText) {
321
+ parts.push(config.customText)
322
+ }
323
+
324
+ return parts.join(' | ')
325
+ }
326
+
327
+ const positions: WatermarkPosition[] = [
328
+ 'top-left', 'top-right', 'bottom-left', 'bottom-right',
329
+ 'center-left', 'center-right',
330
+ ]
331
+
332
+ let currentPosition = baseConfig.position || 'bottom-right'
333
+
334
+ const element = this.add(id, {
335
+ ...baseConfig,
336
+ text: getText(),
337
+ position: currentPosition,
338
+ opacity: baseConfig.opacity ?? 0.3,
339
+ })
340
+
341
+ // Update text periodically
342
+ if (config.updateInterval && config.updateInterval > 0) {
343
+ const textTimer = setInterval(() => {
344
+ const textEl = element.querySelector('span')
345
+ if (textEl) {
346
+ textEl.textContent = getText()
347
+ }
348
+ }, config.updateInterval)
349
+
350
+ this.dynamicTimers.set(`${id}-text`, textTimer)
351
+ }
352
+
353
+ // Move position periodically
354
+ if (config.randomizePosition && config.moveInterval && config.moveInterval > 0) {
355
+ const moveTimer = setInterval(() => {
356
+ const randomIndex = Math.floor(Math.random() * positions.length)
357
+ currentPosition = positions[randomIndex]
358
+
359
+ const positionStyles = this.getPositionStyles(currentPosition, baseConfig.offset)
360
+ element.style.cssText = element.style.cssText.replace(
361
+ /top:.*?;|bottom:.*?;|left:.*?;|right:.*?;|transform:.*?;/g,
362
+ ''
363
+ )
364
+ element.style.cssText += positionStyles
365
+ }, config.moveInterval)
366
+
367
+ this.dynamicTimers.set(`${id}-move`, moveTimer)
368
+ }
369
+
370
+ return element
371
+ }
372
+
373
+ /**
374
+ * Remove watermark
375
+ */
376
+ remove(id: string): void {
377
+ const element = this.elements.get(id)
378
+ if (element) {
379
+ element.remove()
380
+ this.elements.delete(id)
381
+ }
382
+
383
+ // Clear dynamic timers
384
+ const textTimer = this.dynamicTimers.get(`${id}-text`)
385
+ if (textTimer) {
386
+ clearInterval(textTimer)
387
+ this.dynamicTimers.delete(`${id}-text`)
388
+ }
389
+
390
+ const moveTimer = this.dynamicTimers.get(`${id}-move`)
391
+ if (moveTimer) {
392
+ clearInterval(moveTimer)
393
+ this.dynamicTimers.delete(`${id}-move`)
394
+ }
395
+ }
396
+
397
+ /**
398
+ * Remove all watermarks
399
+ */
400
+ removeAll(): void {
401
+ for (const id of this.elements.keys()) {
402
+ this.remove(id)
403
+ }
404
+ }
405
+
406
+ /**
407
+ * Update watermark
408
+ */
409
+ update(id: string, config: Partial<WatermarkConfig>): void {
410
+ const element = this.elements.get(id)
411
+ if (!element) return
412
+
413
+ if (config.opacity !== undefined) {
414
+ element.style.opacity = String(config.opacity)
415
+ }
416
+
417
+ if (config.position) {
418
+ const positionStyles = this.getPositionStyles(config.position, config.offset)
419
+ element.style.cssText = element.style.cssText.replace(
420
+ /top:.*?;|bottom:.*?;|left:.*?;|right:.*?;|transform:.*?;/g,
421
+ ''
422
+ )
423
+ element.style.cssText += positionStyles
424
+ }
425
+
426
+ if (config.text) {
427
+ const textEl = element.querySelector('span')
428
+ if (textEl) {
429
+ textEl.textContent = config.text
430
+ }
431
+ }
432
+
433
+ if (config.image) {
434
+ const imgEl = element.querySelector('img')
435
+ if (imgEl) {
436
+ imgEl.src = config.image
437
+ }
438
+ }
439
+ }
440
+
441
+ /**
442
+ * Show watermark
443
+ */
444
+ show(id: string): void {
445
+ const element = this.elements.get(id)
446
+ if (element) {
447
+ element.style.display = ''
448
+ }
449
+ }
450
+
451
+ /**
452
+ * Hide watermark
453
+ */
454
+ hide(id: string): void {
455
+ const element = this.elements.get(id)
456
+ if (element) {
457
+ element.style.display = 'none'
458
+ }
459
+ }
460
+
461
+ /**
462
+ * Show all watermarks
463
+ */
464
+ showAll(): void {
465
+ for (const id of this.elements.keys()) {
466
+ this.show(id)
467
+ }
468
+ }
469
+
470
+ /**
471
+ * Hide all watermarks
472
+ */
473
+ hideAll(): void {
474
+ for (const id of this.elements.keys()) {
475
+ this.hide(id)
476
+ }
477
+ }
478
+
479
+ /**
480
+ * Get watermark element
481
+ */
482
+ get(id: string): HTMLElement | undefined {
483
+ return this.elements.get(id)
484
+ }
485
+
486
+ /**
487
+ * Get all watermark IDs
488
+ */
489
+ getAll(): string[] {
490
+ return Array.from(this.elements.keys())
491
+ }
492
+
493
+ /**
494
+ * Destroy manager
495
+ */
496
+ destroy(): void {
497
+ this.removeAll()
498
+ this.unsubscribers.forEach(unsub => unsub())
499
+ this.player = null
500
+ this.container = null
501
+ }
502
+ }
503
+
504
+ // =============================================================================
505
+ // Factory
506
+ // =============================================================================
507
+
508
+ export function createWatermarkManager(): WatermarkManager {
509
+ return new WatermarkManager()
510
+ }
511
+
512
+ /**
513
+ * Watermark plugin for Player
514
+ */
515
+ export function watermarkPlugin(watermarks: Array<{ id: string } & WatermarkConfig>): { name: string, install: (player: Player, container: HTMLElement) => () => void } {
516
+ return {
517
+ name: 'watermark',
518
+
519
+ install(player: Player, container: HTMLElement): () => void {
520
+ const manager = createWatermarkManager()
521
+ manager.attach(player, container)
522
+
523
+ // Add initial watermarks
524
+ for (const { id, ...config } of watermarks) {
525
+ manager.add(id, config)
526
+ }
527
+
528
+ // Expose on player
529
+ ;(player as any).watermark = manager
530
+
531
+ return () => {
532
+ manager.destroy()
533
+ delete (player as any).watermark
534
+ }
535
+ },
536
+ }
537
+ }