vidply 1.2.0 → 1.2.3

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 (167) hide show
  1. package/README.md +974 -946
  2. package/dist/dev/{vidply.AudioDescriptionManager-R47Q4YR4.js → vidply.AudioDescriptionManager-JYYLK5KB.js} +275 -19
  3. package/dist/dev/vidply.AudioDescriptionManager-JYYLK5KB.js.map +7 -0
  4. package/dist/dev/{vidply.CaptionStyleMenu-7XHRBWUM.js → vidply.CaptionStyleMenu-47RCKQF5.js} +4 -4
  5. package/dist/dev/{vidply.DASHRenderer-PWEMC3BT.js → vidply.DASHRenderer-VEJ4HDHO.js} +3 -3
  6. package/dist/dev/vidply.DASHRenderer-VEJ4HDHO.js.map +7 -0
  7. package/dist/dev/{vidply.FloatingPlayerManager-PKNNNAS2.js → vidply.FloatingPlayerManager-C2UVRPZY.js} +5 -5
  8. package/dist/dev/vidply.FloatingPlayerManager-C2UVRPZY.js.map +7 -0
  9. package/dist/dev/{vidply.HLSRenderer-E7RQ74HI.js → vidply.HLSRenderer-ZVBK2TNQ.js} +4 -4
  10. package/dist/dev/vidply.HLSRenderer-ZVBK2TNQ.js.map +7 -0
  11. package/dist/dev/vidply.HTML5Renderer-HLRQUOCU.js +12 -0
  12. package/dist/dev/{vidply.SignLanguageManager-FET5FQZ7.js → vidply.SignLanguageManager-FLUCBW5H.js} +8 -8
  13. package/dist/dev/vidply.SignLanguageManager-FLUCBW5H.js.map +7 -0
  14. package/dist/dev/{vidply.SoundCloudRenderer-RVSJZODO.js → vidply.SoundCloudRenderer-MGMAJ7NK.js} +3 -3
  15. package/dist/dev/vidply.SoundCloudRenderer-MGMAJ7NK.js.map +7 -0
  16. package/dist/dev/{vidply.TranscriptManager-5GXOQCLX.js → vidply.TranscriptManager-Y2RYXHJW.js} +10 -10
  17. package/dist/dev/vidply.TranscriptManager-Y2RYXHJW.js.map +7 -0
  18. package/dist/dev/{vidply.VimeoRenderer-ER2LSBBW.js → vidply.VimeoRenderer-RQFQD7OE.js} +3 -3
  19. package/dist/dev/vidply.VimeoRenderer-RQFQD7OE.js.map +7 -0
  20. package/dist/dev/{vidply.YouTubeRenderer-NFTOWJAB.js → vidply.YouTubeRenderer-EU2BARZF.js} +3 -3
  21. package/dist/dev/vidply.YouTubeRenderer-EU2BARZF.js.map +7 -0
  22. package/dist/dev/{vidply.chunk-BNFIUL2Z.js → vidply.chunk-6LYZYI5G.js} +3 -3
  23. package/dist/dev/vidply.chunk-6LYZYI5G.js.map +7 -0
  24. package/dist/dev/{vidply.chunk-NQPURMQY.js → vidply.chunk-A4ESEOQ2.js} +6 -6
  25. package/dist/dev/vidply.chunk-A4ESEOQ2.js.map +7 -0
  26. package/dist/dev/{vidply.chunk-W25HUCHS.js → vidply.chunk-CGYIED2I.js} +2 -2
  27. package/dist/dev/vidply.chunk-CGYIED2I.js.map +7 -0
  28. package/dist/dev/{vidply.chunk-SK4V2DAU.js → vidply.chunk-HZ7UOKOU.js} +3 -3
  29. package/dist/dev/{vidply.chunk-RZWZGDV7.js → vidply.chunk-I6EDRYGJ.js} +33 -6
  30. package/dist/dev/vidply.chunk-I6EDRYGJ.js.map +7 -0
  31. package/dist/dev/{vidply.chunk-64WXMTGL.js → vidply.chunk-JKMQGLCJ.js} +5 -5
  32. package/dist/dev/vidply.chunk-JKMQGLCJ.js.map +7 -0
  33. package/dist/dev/{vidply.chunk-DZSDO3B7.js → vidply.chunk-JZNZX32Z.js} +2 -2
  34. package/dist/dev/{vidply.chunk-VMSQAZM7.js → vidply.chunk-P25MRJ6O.js} +2 -2
  35. package/dist/dev/{vidply.chunk-6RQA4ROD.js → vidply.chunk-PRFNESYA.js} +2 -2
  36. package/dist/dev/{vidply.chunk-KI7C3QVL.js → vidply.chunk-UI3RYCVF.js} +4 -3
  37. package/dist/dev/vidply.chunk-UI3RYCVF.js.map +7 -0
  38. package/dist/dev/{vidply.chunk-JJ47NO24.js → vidply.chunk-UNFGMABP.js} +2 -2
  39. package/dist/dev/vidply.chunk-UNFGMABP.js.map +7 -0
  40. package/dist/dev/{vidply.de-4YAT7FFM.js → vidply.de-VAUVSQXC.js} +29 -2
  41. package/dist/dev/vidply.de-VAUVSQXC.js.map +7 -0
  42. package/dist/dev/{vidply.es-LEWIRUBH.js → vidply.es-G65HFWO6.js} +29 -2
  43. package/dist/dev/vidply.es-G65HFWO6.js.map +7 -0
  44. package/dist/dev/vidply.esm.js +648 -35
  45. package/dist/dev/vidply.esm.js.map +3 -3
  46. package/dist/dev/{vidply.fr-Q4GV2NOE.js → vidply.fr-MJNOZELM.js} +29 -2
  47. package/dist/dev/vidply.fr-MJNOZELM.js.map +7 -0
  48. package/dist/dev/{vidply.ja-S6ITA6WU.js → vidply.ja-INMDHIYJ.js} +29 -2
  49. package/dist/dev/vidply.ja-INMDHIYJ.js.map +7 -0
  50. package/dist/legacy/vidply.js +1062 -34
  51. package/dist/legacy/vidply.js.map +3 -3
  52. package/dist/legacy/vidply.min.js +2 -2
  53. package/dist/legacy/vidply.min.meta.json +127 -55
  54. package/dist/prod/vidply.AudioDescriptionManager-EKWUNOPE.min.js +6 -0
  55. package/dist/prod/{vidply.CaptionStyleMenu-M3KVQWB5.min.js → vidply.CaptionStyleMenu-JTWSNHRQ.min.js} +2 -2
  56. package/dist/prod/{vidply.DASHRenderer-DN3S27AV.min.js → vidply.DASHRenderer-CIDAHFWM.min.js} +2 -2
  57. package/dist/prod/{vidply.FloatingPlayerManager-SKAZ4J43.min.js → vidply.FloatingPlayerManager-VZXHPCPM.min.js} +2 -2
  58. package/dist/prod/{vidply.HLSRenderer-62HH4YTK.min.js → vidply.HLSRenderer-Q6EFYKID.min.js} +2 -2
  59. package/dist/prod/vidply.HTML5Renderer-XRDJUTOO.min.js +6 -0
  60. package/dist/prod/{vidply.SignLanguageManager-V3OBTSSF.min.js → vidply.SignLanguageManager-JUOSA5AM.min.js} +2 -2
  61. package/dist/prod/{vidply.SoundCloudRenderer-CXRVZIAJ.min.js → vidply.SoundCloudRenderer-7ODD46PO.min.js} +2 -2
  62. package/dist/prod/{vidply.TranscriptManager-SYM3DFUY.min.js → vidply.TranscriptManager-4B2QKYGQ.min.js} +2 -2
  63. package/dist/prod/{vidply.VimeoRenderer-72TWRYFQ.min.js → vidply.VimeoRenderer-MR3UYLY7.min.js} +2 -2
  64. package/dist/prod/{vidply.YouTubeRenderer-UMHEV3KD.min.js → vidply.YouTubeRenderer-ITRTC7NR.min.js} +2 -2
  65. package/dist/prod/{vidply.chunk-YEENJS3Q.min.js → vidply.chunk-3EEDXU4T.min.js} +2 -2
  66. package/dist/prod/{vidply.chunk-P4UXOHWA.min.js → vidply.chunk-5HTX5QXW.min.js} +1 -1
  67. package/dist/prod/{vidply.chunk-ABPXVDLZ.min.js → vidply.chunk-6OGHHIYP.min.js} +2 -2
  68. package/dist/prod/{vidply.chunk-UK7YGOB5.min.js → vidply.chunk-GUPGMFUZ.min.js} +2 -2
  69. package/dist/prod/{vidply.chunk-WX3MVEAL.min.js → vidply.chunk-MWEBITCP.min.js} +1 -1
  70. package/dist/prod/{vidply.chunk-2TB6MUHO.min.js → vidply.chunk-PL3P3JHR.min.js} +1 -1
  71. package/dist/prod/{vidply.chunk-EKEYJWMG.min.js → vidply.chunk-QQZGV7TD.min.js} +2 -2
  72. package/dist/prod/{vidply.chunk-Z3YDSA7O.min.js → vidply.chunk-U5IRRYG5.min.js} +1 -1
  73. package/dist/prod/{vidply.chunk-TM6AXJVD.min.js → vidply.chunk-VKHV4QHA.min.js} +2 -2
  74. package/dist/prod/{vidply.chunk-P3PQBY7N.min.js → vidply.chunk-XA4M574A.min.js} +1 -1
  75. package/dist/prod/{vidply.chunk-RI3X7WHK.min.js → vidply.chunk-ZPTV4JEP.min.js} +2 -2
  76. package/dist/prod/{vidply.de-BCHXRQ53.min.js → vidply.de-UZHFCEZU.min.js} +2 -2
  77. package/dist/prod/{vidply.es-PICAKMZD.min.js → vidply.es-BOISUV2S.min.js} +2 -2
  78. package/dist/prod/vidply.esm.min.js +2 -2
  79. package/dist/prod/{vidply.fr-ZILI7CDR.min.js → vidply.fr-ICSMKPFM.min.js} +2 -2
  80. package/dist/prod/{vidply.ja-5WQ4OK4J.min.js → vidply.ja-VQ2HKGAY.min.js} +2 -2
  81. package/dist/vidply.css +5852 -5796
  82. package/dist/vidply.esm.min.meta.json +204 -149
  83. package/dist/vidply.min.css +1 -1
  84. package/package.json +11 -11
  85. package/src/controls/CaptionManager.ts +598 -598
  86. package/src/controls/ControlBar.ts +4191 -4159
  87. package/src/controls/KeyboardHelp.ts +326 -0
  88. package/src/controls/KeyboardManager.ts +387 -383
  89. package/src/controls/SettingsDialog.ts +488 -488
  90. package/src/controls/TranscriptManager.ts +2426 -2426
  91. package/src/core/AudioDescriptionManager.ts +863 -774
  92. package/src/core/DescriptionSpeechManager.ts +232 -0
  93. package/src/core/FloatingPlayerManager.ts +674 -674
  94. package/src/core/LazyInit.ts +80 -80
  95. package/src/core/MediaSessionManager.ts +341 -0
  96. package/src/core/MetadataAlertsManager.ts +514 -514
  97. package/src/core/Player.ts +2973 -2872
  98. package/src/core/PosterManager.ts +169 -169
  99. package/src/core/PseudoFullscreen.ts +179 -179
  100. package/src/core/ResponsiveManager.ts +231 -231
  101. package/src/core/ResumeManager.ts +236 -236
  102. package/src/core/SignLanguageManager.ts +1308 -1308
  103. package/src/core/ThemeManager.ts +157 -157
  104. package/src/features/PlaylistManager.ts +1838 -1827
  105. package/src/i18n/i18n.ts +223 -223
  106. package/src/i18n/languages/de.ts +244 -217
  107. package/src/i18n/languages/en.ts +245 -218
  108. package/src/i18n/languages/es.ts +241 -214
  109. package/src/i18n/languages/fr.ts +241 -214
  110. package/src/i18n/languages/ja.ts +241 -214
  111. package/src/icons/Icons.ts +168 -167
  112. package/src/index.ts +196 -193
  113. package/src/renderers/DASHRenderer.ts +1077 -1077
  114. package/src/renderers/HLSRenderer.ts +944 -944
  115. package/src/renderers/HTML5Renderer.ts +389 -389
  116. package/src/renderers/SoundCloudRenderer.ts +437 -437
  117. package/src/renderers/VimeoRenderer.ts +298 -298
  118. package/src/renderers/YouTubeRenderer.ts +290 -290
  119. package/src/styles/vidply.css +5852 -5796
  120. package/src/types/events.ts +128 -122
  121. package/src/types/globals.d.ts +313 -313
  122. package/src/types/options.ts +199 -182
  123. package/src/types/renderer.ts +58 -58
  124. package/src/utils/DOMUtils.ts +281 -281
  125. package/src/utils/DownloadInfo.ts +171 -171
  126. package/src/utils/DraggablePanel.ts +539 -539
  127. package/src/utils/DraggablePanelMenu.ts +195 -195
  128. package/src/utils/DraggableResizable.ts +997 -997
  129. package/src/utils/EventEmitter.ts +59 -59
  130. package/src/utils/FormUtils.ts +95 -95
  131. package/src/utils/MenuFactory.ts +483 -483
  132. package/src/utils/Sanitize.ts +70 -70
  133. package/src/utils/SettingsMenuFactory.ts +289 -289
  134. package/src/utils/StorageManager.ts +287 -287
  135. package/src/utils/TrackLabelUtils.ts +52 -52
  136. package/src/utils/UrlSafe.ts +69 -69
  137. package/src/utils/VideoFrameCapture.ts +114 -114
  138. package/src/utils/WindowComponents.ts +250 -250
  139. package/dist/dev/vidply.AudioDescriptionManager-R47Q4YR4.js.map +0 -7
  140. package/dist/dev/vidply.DASHRenderer-PWEMC3BT.js.map +0 -7
  141. package/dist/dev/vidply.FloatingPlayerManager-PKNNNAS2.js.map +0 -7
  142. package/dist/dev/vidply.HLSRenderer-E7RQ74HI.js.map +0 -7
  143. package/dist/dev/vidply.HTML5Renderer-BU4QJJJY.js +0 -12
  144. package/dist/dev/vidply.SignLanguageManager-FET5FQZ7.js.map +0 -7
  145. package/dist/dev/vidply.SoundCloudRenderer-RVSJZODO.js.map +0 -7
  146. package/dist/dev/vidply.TranscriptManager-5GXOQCLX.js.map +0 -7
  147. package/dist/dev/vidply.VimeoRenderer-ER2LSBBW.js.map +0 -7
  148. package/dist/dev/vidply.YouTubeRenderer-NFTOWJAB.js.map +0 -7
  149. package/dist/dev/vidply.chunk-64WXMTGL.js.map +0 -7
  150. package/dist/dev/vidply.chunk-BNFIUL2Z.js.map +0 -7
  151. package/dist/dev/vidply.chunk-JJ47NO24.js.map +0 -7
  152. package/dist/dev/vidply.chunk-KI7C3QVL.js.map +0 -7
  153. package/dist/dev/vidply.chunk-NQPURMQY.js.map +0 -7
  154. package/dist/dev/vidply.chunk-RZWZGDV7.js.map +0 -7
  155. package/dist/dev/vidply.chunk-W25HUCHS.js.map +0 -7
  156. package/dist/dev/vidply.de-4YAT7FFM.js.map +0 -7
  157. package/dist/dev/vidply.es-LEWIRUBH.js.map +0 -7
  158. package/dist/dev/vidply.fr-Q4GV2NOE.js.map +0 -7
  159. package/dist/dev/vidply.ja-S6ITA6WU.js.map +0 -7
  160. package/dist/prod/vidply.AudioDescriptionManager-RHLIJW5C.min.js +0 -6
  161. package/dist/prod/vidply.HTML5Renderer-IU5T3WEW.min.js +0 -6
  162. /package/dist/dev/{vidply.CaptionStyleMenu-7XHRBWUM.js.map → vidply.CaptionStyleMenu-47RCKQF5.js.map} +0 -0
  163. /package/dist/dev/{vidply.HTML5Renderer-BU4QJJJY.js.map → vidply.HTML5Renderer-HLRQUOCU.js.map} +0 -0
  164. /package/dist/dev/{vidply.chunk-SK4V2DAU.js.map → vidply.chunk-HZ7UOKOU.js.map} +0 -0
  165. /package/dist/dev/{vidply.chunk-DZSDO3B7.js.map → vidply.chunk-JZNZX32Z.js.map} +0 -0
  166. /package/dist/dev/{vidply.chunk-VMSQAZM7.js.map → vidply.chunk-P25MRJ6O.js.map} +0 -0
  167. /package/dist/dev/{vidply.chunk-6RQA4ROD.js.map → vidply.chunk-PRFNESYA.js.map} +0 -0
@@ -1,389 +1,389 @@
1
- import type { Renderer } from '../types/renderer.js';
2
- import type { Player } from '../core/Player.js';
3
-
4
- export class HTML5Renderer implements Renderer {
5
- player: Player;
6
- media: HTMLMediaElement;
7
- _didDeferredLoad: boolean;
8
- // All media listeners are registered with this controller's signal so a
9
- // single abort() in destroy() detaches every one of them. Avoids the leak
10
- // of removeEventListener being called with fresh, non-matching callbacks.
11
- private _listenerController: AbortController;
12
-
13
- constructor(player: Player) {
14
- this.player = player;
15
- this.media = player.element;
16
- this._didDeferredLoad = false;
17
- this._listenerController = new AbortController();
18
- }
19
-
20
- async init() {
21
- // Hide native controls
22
- this.media.controls = false;
23
- this.media.removeAttribute('controls');
24
-
25
- this.attachEvents();
26
-
27
- // Set preload + optionally defer network loading until user play
28
- if (this.player.options.deferLoad) {
29
- // Allow metadata preload while still avoiding full media download.
30
- // Note: browsers may still fetch metadata automatically when preload="metadata".
31
- this.media.preload = this.player.options.preload || 'none';
32
-
33
- // Firefox requires an explicit load() call to fetch metadata even with preload="metadata".
34
- // Only call load() if preload is set to "metadata" to ensure duration is available.
35
- if (this.player.options.preload === 'metadata') {
36
- this.media.load();
37
- }
38
- } else {
39
- this.media.preload = this.player.options.preload;
40
- // Load media (eager)
41
- this.media.load();
42
- }
43
-
44
- // Show VidPly controls (remove external controls class if present)
45
- if (this.player.container) {
46
- this.player.container.classList.remove('vidply-external-controls');
47
- }
48
- }
49
-
50
- attachEvents() {
51
- const { signal } = this._listenerController;
52
-
53
- // Playback events
54
- this.media.addEventListener('loadedmetadata', () => {
55
- this.player.state.duration = this.media.duration;
56
- this.player.emit('loadedmetadata');
57
-
58
- // Auto-generate poster if none exists (for HTML5 video only)
59
- if (this.media.tagName === 'VIDEO') {
60
- this.player.autoGeneratePoster().catch((error: unknown) => {
61
- this.player.log('Failed to auto-generate poster:', error, 'warn');
62
- });
63
- }
64
- }, { signal });
65
-
66
- this.media.addEventListener('durationchange', () => {
67
- const duration = this.media.duration;
68
- if (duration && isFinite(duration) && duration > 0) {
69
- this.player.state.duration = duration;
70
- this.player.emit('durationchange', duration);
71
- }
72
- }, { signal });
73
-
74
- this.media.addEventListener('play', () => {
75
- this.player.state.playing = true;
76
- this.player.state.paused = false;
77
- this.player.state.ended = false;
78
- this.player.emit('play');
79
-
80
- if (this.player.options.onPlay) {
81
- this.player.options.onPlay.call(this.player);
82
- }
83
-
84
- // Pause other players if enabled
85
- if (this.player.options.pauseOthersOnPlay) {
86
- this.pauseOtherPlayers();
87
- }
88
- }, { signal });
89
-
90
- this.media.addEventListener('pause', () => {
91
- this.player.state.playing = false;
92
- this.player.state.paused = true;
93
- this.player.emit('pause');
94
-
95
- if (this.player.options.onPause) {
96
- this.player.options.onPause.call(this.player);
97
- }
98
- }, { signal });
99
-
100
- this.media.addEventListener('ended', () => {
101
- this.player.state.playing = false;
102
- this.player.state.paused = true;
103
- this.player.state.ended = true;
104
- this.player.emit('ended');
105
-
106
- if (this.player.options.onEnded) {
107
- this.player.options.onEnded.call(this.player);
108
- }
109
-
110
- // Handle loop
111
- if (this.player.options.loop) {
112
- this.player.seek(0);
113
- this.player.play();
114
- }
115
- }, { signal });
116
-
117
- this.media.addEventListener('timeupdate', () => {
118
- this.player.state.currentTime = this.media.currentTime;
119
- this.player.emit('timeupdate', this.media.currentTime);
120
-
121
- if (this.player.options.onTimeUpdate) {
122
- this.player.options.onTimeUpdate.call(this.player, this.media.currentTime);
123
- }
124
- }, { signal });
125
-
126
- this.media.addEventListener('volumechange', () => {
127
- this.player.state.volume = this.media.volume;
128
- this.player.state.muted = this.media.muted;
129
- this.player.emit('volumechange', this.media.volume);
130
-
131
- if (this.player.options.onVolumeChange) {
132
- this.player.options.onVolumeChange.call(this.player, this.media.volume);
133
- }
134
- }, { signal });
135
-
136
- this.media.addEventListener('seeking', () => {
137
- this.player.state.seeking = true;
138
- this.player.emit('seeking');
139
- }, { signal });
140
-
141
- this.media.addEventListener('seeked', () => {
142
- this.player.state.seeking = false;
143
- this.player.emit('seeked');
144
- }, { signal });
145
-
146
- this.media.addEventListener('waiting', () => {
147
- this.player.state.buffering = true;
148
- this.player.emit('waiting');
149
- }, { signal });
150
-
151
- this.media.addEventListener('canplay', () => {
152
- this.player.state.buffering = false;
153
- this.player.emit('canplay');
154
- }, { signal });
155
-
156
- this.media.addEventListener('progress', () => {
157
- if (this.media.buffered.length > 0) {
158
- const buffered = this.media.buffered.end(this.media.buffered.length - 1);
159
- this.player.emit('progress', buffered);
160
- }
161
- }, { signal });
162
-
163
- this.media.addEventListener('error', (_e: Event) => {
164
- this.player.handleError(this.media.error);
165
- }, { signal });
166
-
167
- this.media.addEventListener('ratechange', () => {
168
- this.player.state.playbackSpeed = this.media.playbackRate;
169
- this.player.emit('ratechange', this.media.playbackRate);
170
- }, { signal });
171
- }
172
-
173
- pauseOtherPlayers() {
174
- // Pause other VidPly instances
175
- const allPlayers = document.querySelectorAll('.vidply-player');
176
- allPlayers.forEach(playerEl => {
177
- if (playerEl !== this.player.container) {
178
- const video = playerEl.querySelector('video, audio') as HTMLMediaElement | null;
179
- if (video && !video.paused) {
180
- video.pause();
181
- }
182
- }
183
- });
184
- }
185
-
186
- play() {
187
- // Save scroll position to prevent browser from scrolling to video
188
- const scrollX = window.scrollX;
189
- const scrollY = window.scrollY;
190
-
191
- // If deferLoad is enabled, trigger load only on the first user play request.
192
- if (this.player.options.deferLoad && !this._didDeferredLoad) {
193
- try {
194
- // Only call load() if browser hasn't loaded anything yet.
195
- if (this.media.readyState === 0) {
196
- this.media.load();
197
- }
198
- } catch {
199
- // ignore
200
- }
201
- this._didDeferredLoad = true;
202
- }
203
-
204
- const promise = this.media.play();
205
-
206
- // Restore scroll position immediately to prevent auto-scroll
207
- window.scrollTo(scrollX, scrollY);
208
-
209
- if (promise !== undefined) {
210
- promise.catch(error => {
211
- this.player.log('Play failed:', error, 'warn');
212
-
213
- // If autoplay failed, try muted autoplay
214
- if (this.player.options.autoplay && !this.player.state.muted) {
215
- this.player.log('Retrying play with muted audio', 'info');
216
- this.media.muted = true;
217
-
218
- // Save scroll position again for retry
219
- const retryScrollX = window.scrollX;
220
- const retryScrollY = window.scrollY;
221
- this.media.play().then(() => {
222
- window.scrollTo(retryScrollX, retryScrollY);
223
- }).catch(err => {
224
- this.player.handleError(err);
225
- });
226
- }
227
- });
228
- return promise;
229
- }
230
- return Promise.resolve();
231
- }
232
-
233
- /**
234
- * Ensure the media element has been loaded at least once (metadata/initial state)
235
- * without starting playback. Useful for playlists to behave like single videos.
236
- */
237
- ensureLoaded() {
238
- if (!this.player.options.deferLoad || this._didDeferredLoad) {
239
- return;
240
- }
241
-
242
- try {
243
- if (this.media.readyState === 0) {
244
- this.media.load();
245
- }
246
- } catch {
247
- // ignore
248
- }
249
- this._didDeferredLoad = true;
250
- }
251
-
252
- pause() {
253
- this.media.pause();
254
- }
255
-
256
- seek(time: number) {
257
- this.media.currentTime = time;
258
- }
259
-
260
- setVolume(volume: number) {
261
- this.media.volume = volume;
262
- }
263
-
264
- setMuted(muted: boolean) {
265
- this.media.muted = muted;
266
- }
267
-
268
- setPlaybackSpeed(speed: number) {
269
- this.media.playbackRate = speed;
270
- }
271
-
272
- /**
273
- * Get available quality levels from source elements
274
- * @returns {Array} Array of quality objects with index, height, width, and src
275
- */
276
- getQualities() {
277
- const sources = Array.from(this.media.querySelectorAll('source'));
278
-
279
- if (sources.length <= 1) {
280
- return [];
281
- }
282
-
283
- return sources.map((source, index) => {
284
- // Extract quality from data-quality attribute (preferred, HTML5-valid)
285
- // Fallback to deprecated 'label' attribute for backward compatibility
286
- // Note: 'label' is not a valid HTML attribute on <source> elements
287
- const label = source.getAttribute('data-quality') || source.getAttribute('data-label') || source.getAttribute('label') || '';
288
- const height = source.getAttribute('data-height') || String(this.extractHeightFromLabel(label));
289
- const width = source.getAttribute('data-width') || '';
290
-
291
- return {
292
- index,
293
- height: height ? parseInt(height) : 0,
294
- width: width ? parseInt(width) : 0,
295
- src: source.src,
296
- type: source.type,
297
- name: label || (height ? `${height}p` : `Quality ${index + 1}`)
298
- };
299
- }).filter(q => q.height > 0); // Only return qualities with valid height
300
- }
301
-
302
- /**
303
- * Extract height from quality label (e.g., "1080p" -> 1080)
304
- * @param {string} label
305
- * @returns {number}
306
- */
307
- extractHeightFromLabel(label: string) {
308
- const match = label.match(/(\d+)p/i);
309
- return match && match[1] ? parseInt(match[1], 10) : 0;
310
- }
311
-
312
- /**
313
- * Switch to a specific quality level
314
- * @param {number} qualityIndex - Index of the quality level (-1 for auto, not applicable for HTML5)
315
- */
316
- switchQuality(qualityIndex: number) {
317
- const qualities = this.getQualities();
318
-
319
- if (qualityIndex < 0 || qualityIndex >= qualities.length) {
320
- this.player.log('Invalid quality index', 'warn');
321
- return;
322
- }
323
-
324
- const quality = qualities[qualityIndex];
325
- if (!quality || !quality.src) {
326
- return;
327
- }
328
- const currentTime = this.media.currentTime;
329
- const wasPlaying = !this.media.paused;
330
-
331
- // Store the current source for comparison
332
- const currentSrc = this.media.currentSrc;
333
-
334
- // Don't switch if already at this quality
335
- if (currentSrc === quality.src) {
336
- this.player.log('Already at this quality level', 'info');
337
- return;
338
- }
339
-
340
- this.player.log(`Switching to quality: ${quality.name}`, 'info');
341
-
342
- // Update the src
343
- this.media.src = quality.src;
344
-
345
- // Wait for the new source to load, then restore playback state
346
- const onLoadedMetadata = () => {
347
- this.media.removeEventListener('loadedmetadata', onLoadedMetadata);
348
-
349
- // Restore playback position
350
- this.media.currentTime = currentTime;
351
-
352
- // Resume playback if it was playing
353
- if (wasPlaying) {
354
- this.media.play().catch(() => {
355
- this.player.log('Failed to resume playback after quality switch', 'warn');
356
- });
357
- }
358
-
359
- // Emit quality change event
360
- this.player.emit('qualitychange', { quality: quality.name, index: qualityIndex });
361
- };
362
-
363
- this.media.addEventListener('loadedmetadata', onLoadedMetadata);
364
- this.media.load();
365
- }
366
-
367
- /**
368
- * Get current quality index
369
- * @returns {number}
370
- */
371
- getCurrentQuality() {
372
- const qualities = this.getQualities();
373
- const currentSrc = this.media.currentSrc;
374
-
375
- for (let i = 0; i < qualities.length; i++) {
376
- if (qualities[i]?.src === currentSrc) {
377
- return i;
378
- }
379
- }
380
-
381
- return 0; // Default to first quality if not found
382
- }
383
-
384
- destroy() {
385
- // Detach every media listener registered in attachEvents() in one shot.
386
- this._listenerController.abort();
387
- }
388
- }
389
-
1
+ import type { Renderer } from '../types/renderer.js';
2
+ import type { Player } from '../core/Player.js';
3
+
4
+ export class HTML5Renderer implements Renderer {
5
+ player: Player;
6
+ media: HTMLMediaElement;
7
+ _didDeferredLoad: boolean;
8
+ // All media listeners are registered with this controller's signal so a
9
+ // single abort() in destroy() detaches every one of them. Avoids the leak
10
+ // of removeEventListener being called with fresh, non-matching callbacks.
11
+ private _listenerController: AbortController;
12
+
13
+ constructor(player: Player) {
14
+ this.player = player;
15
+ this.media = player.element;
16
+ this._didDeferredLoad = false;
17
+ this._listenerController = new AbortController();
18
+ }
19
+
20
+ async init() {
21
+ // Hide native controls
22
+ this.media.controls = false;
23
+ this.media.removeAttribute('controls');
24
+
25
+ this.attachEvents();
26
+
27
+ // Set preload + optionally defer network loading until user play
28
+ if (this.player.options.deferLoad) {
29
+ // Allow metadata preload while still avoiding full media download.
30
+ // Note: browsers may still fetch metadata automatically when preload="metadata".
31
+ this.media.preload = this.player.options.preload || 'none';
32
+
33
+ // Firefox requires an explicit load() call to fetch metadata even with preload="metadata".
34
+ // Only call load() if preload is set to "metadata" to ensure duration is available.
35
+ if (this.player.options.preload === 'metadata') {
36
+ this.media.load();
37
+ }
38
+ } else {
39
+ this.media.preload = this.player.options.preload;
40
+ // Load media (eager)
41
+ this.media.load();
42
+ }
43
+
44
+ // Show VidPly controls (remove external controls class if present)
45
+ if (this.player.container) {
46
+ this.player.container.classList.remove('vidply-external-controls');
47
+ }
48
+ }
49
+
50
+ attachEvents() {
51
+ const { signal } = this._listenerController;
52
+
53
+ // Playback events
54
+ this.media.addEventListener('loadedmetadata', () => {
55
+ this.player.state.duration = this.media.duration;
56
+ this.player.emit('loadedmetadata');
57
+
58
+ // Auto-generate poster if none exists (for HTML5 video only)
59
+ if (this.media.tagName === 'VIDEO') {
60
+ this.player.autoGeneratePoster().catch((error: unknown) => {
61
+ this.player.log('Failed to auto-generate poster:', error, 'warn');
62
+ });
63
+ }
64
+ }, { signal });
65
+
66
+ this.media.addEventListener('durationchange', () => {
67
+ const duration = this.media.duration;
68
+ if (duration && isFinite(duration) && duration > 0) {
69
+ this.player.state.duration = duration;
70
+ this.player.emit('durationchange', duration);
71
+ }
72
+ }, { signal });
73
+
74
+ this.media.addEventListener('play', () => {
75
+ this.player.state.playing = true;
76
+ this.player.state.paused = false;
77
+ this.player.state.ended = false;
78
+ this.player.emit('play');
79
+
80
+ if (this.player.options.onPlay) {
81
+ this.player.options.onPlay.call(this.player);
82
+ }
83
+
84
+ // Pause other players if enabled
85
+ if (this.player.options.pauseOthersOnPlay) {
86
+ this.pauseOtherPlayers();
87
+ }
88
+ }, { signal });
89
+
90
+ this.media.addEventListener('pause', () => {
91
+ this.player.state.playing = false;
92
+ this.player.state.paused = true;
93
+ this.player.emit('pause');
94
+
95
+ if (this.player.options.onPause) {
96
+ this.player.options.onPause.call(this.player);
97
+ }
98
+ }, { signal });
99
+
100
+ this.media.addEventListener('ended', () => {
101
+ this.player.state.playing = false;
102
+ this.player.state.paused = true;
103
+ this.player.state.ended = true;
104
+ this.player.emit('ended');
105
+
106
+ if (this.player.options.onEnded) {
107
+ this.player.options.onEnded.call(this.player);
108
+ }
109
+
110
+ // Handle loop
111
+ if (this.player.options.loop) {
112
+ this.player.seek(0);
113
+ this.player.play();
114
+ }
115
+ }, { signal });
116
+
117
+ this.media.addEventListener('timeupdate', () => {
118
+ this.player.state.currentTime = this.media.currentTime;
119
+ this.player.emit('timeupdate', this.media.currentTime);
120
+
121
+ if (this.player.options.onTimeUpdate) {
122
+ this.player.options.onTimeUpdate.call(this.player, this.media.currentTime);
123
+ }
124
+ }, { signal });
125
+
126
+ this.media.addEventListener('volumechange', () => {
127
+ this.player.state.volume = this.media.volume;
128
+ this.player.state.muted = this.media.muted;
129
+ this.player.emit('volumechange', this.media.volume);
130
+
131
+ if (this.player.options.onVolumeChange) {
132
+ this.player.options.onVolumeChange.call(this.player, this.media.volume);
133
+ }
134
+ }, { signal });
135
+
136
+ this.media.addEventListener('seeking', () => {
137
+ this.player.state.seeking = true;
138
+ this.player.emit('seeking');
139
+ }, { signal });
140
+
141
+ this.media.addEventListener('seeked', () => {
142
+ this.player.state.seeking = false;
143
+ this.player.emit('seeked');
144
+ }, { signal });
145
+
146
+ this.media.addEventListener('waiting', () => {
147
+ this.player.state.buffering = true;
148
+ this.player.emit('waiting');
149
+ }, { signal });
150
+
151
+ this.media.addEventListener('canplay', () => {
152
+ this.player.state.buffering = false;
153
+ this.player.emit('canplay');
154
+ }, { signal });
155
+
156
+ this.media.addEventListener('progress', () => {
157
+ if (this.media.buffered.length > 0) {
158
+ const buffered = this.media.buffered.end(this.media.buffered.length - 1);
159
+ this.player.emit('progress', buffered);
160
+ }
161
+ }, { signal });
162
+
163
+ this.media.addEventListener('error', (_e: Event) => {
164
+ this.player.handleError(this.media.error);
165
+ }, { signal });
166
+
167
+ this.media.addEventListener('ratechange', () => {
168
+ this.player.state.playbackSpeed = this.media.playbackRate;
169
+ this.player.emit('ratechange', this.media.playbackRate);
170
+ }, { signal });
171
+ }
172
+
173
+ pauseOtherPlayers() {
174
+ // Pause other VidPly instances
175
+ const allPlayers = document.querySelectorAll('.vidply-player');
176
+ allPlayers.forEach(playerEl => {
177
+ if (playerEl !== this.player.container) {
178
+ const video = playerEl.querySelector('video, audio') as HTMLMediaElement | null;
179
+ if (video && !video.paused) {
180
+ video.pause();
181
+ }
182
+ }
183
+ });
184
+ }
185
+
186
+ play() {
187
+ // Save scroll position to prevent browser from scrolling to video
188
+ const scrollX = window.scrollX;
189
+ const scrollY = window.scrollY;
190
+
191
+ // If deferLoad is enabled, trigger load only on the first user play request.
192
+ if (this.player.options.deferLoad && !this._didDeferredLoad) {
193
+ try {
194
+ // Only call load() if browser hasn't loaded anything yet.
195
+ if (this.media.readyState === 0) {
196
+ this.media.load();
197
+ }
198
+ } catch {
199
+ // ignore
200
+ }
201
+ this._didDeferredLoad = true;
202
+ }
203
+
204
+ const promise = this.media.play();
205
+
206
+ // Restore scroll position immediately to prevent auto-scroll
207
+ window.scrollTo(scrollX, scrollY);
208
+
209
+ if (promise !== undefined) {
210
+ promise.catch(error => {
211
+ this.player.log('Play failed:', error, 'warn');
212
+
213
+ // If autoplay failed, try muted autoplay
214
+ if (this.player.options.autoplay && !this.player.state.muted) {
215
+ this.player.log('Retrying play with muted audio', 'info');
216
+ this.media.muted = true;
217
+
218
+ // Save scroll position again for retry
219
+ const retryScrollX = window.scrollX;
220
+ const retryScrollY = window.scrollY;
221
+ this.media.play().then(() => {
222
+ window.scrollTo(retryScrollX, retryScrollY);
223
+ }).catch(err => {
224
+ this.player.handleError(err);
225
+ });
226
+ }
227
+ });
228
+ return promise;
229
+ }
230
+ return Promise.resolve();
231
+ }
232
+
233
+ /**
234
+ * Ensure the media element has been loaded at least once (metadata/initial state)
235
+ * without starting playback. Useful for playlists to behave like single videos.
236
+ */
237
+ ensureLoaded() {
238
+ if (!this.player.options.deferLoad || this._didDeferredLoad) {
239
+ return;
240
+ }
241
+
242
+ try {
243
+ if (this.media.readyState === 0) {
244
+ this.media.load();
245
+ }
246
+ } catch {
247
+ // ignore
248
+ }
249
+ this._didDeferredLoad = true;
250
+ }
251
+
252
+ pause() {
253
+ this.media.pause();
254
+ }
255
+
256
+ seek(time: number) {
257
+ this.media.currentTime = time;
258
+ }
259
+
260
+ setVolume(volume: number) {
261
+ this.media.volume = volume;
262
+ }
263
+
264
+ setMuted(muted: boolean) {
265
+ this.media.muted = muted;
266
+ }
267
+
268
+ setPlaybackSpeed(speed: number) {
269
+ this.media.playbackRate = speed;
270
+ }
271
+
272
+ /**
273
+ * Get available quality levels from source elements
274
+ * @returns {Array} Array of quality objects with index, height, width, and src
275
+ */
276
+ getQualities() {
277
+ const sources = Array.from(this.media.querySelectorAll('source'));
278
+
279
+ if (sources.length <= 1) {
280
+ return [];
281
+ }
282
+
283
+ return sources.map((source, index) => {
284
+ // Extract quality from data-quality attribute (preferred, HTML5-valid)
285
+ // Fallback to deprecated 'label' attribute for backward compatibility
286
+ // Note: 'label' is not a valid HTML attribute on <source> elements
287
+ const label = source.getAttribute('data-quality') || source.getAttribute('data-label') || source.getAttribute('label') || '';
288
+ const height = source.getAttribute('data-height') || String(this.extractHeightFromLabel(label));
289
+ const width = source.getAttribute('data-width') || '';
290
+
291
+ return {
292
+ index,
293
+ height: height ? parseInt(height) : 0,
294
+ width: width ? parseInt(width) : 0,
295
+ src: source.src,
296
+ type: source.type,
297
+ name: label || (height ? `${height}p` : `Quality ${index + 1}`)
298
+ };
299
+ }).filter(q => q.height > 0); // Only return qualities with valid height
300
+ }
301
+
302
+ /**
303
+ * Extract height from quality label (e.g., "1080p" -> 1080)
304
+ * @param {string} label
305
+ * @returns {number}
306
+ */
307
+ extractHeightFromLabel(label: string) {
308
+ const match = label.match(/(\d+)p/i);
309
+ return match && match[1] ? parseInt(match[1], 10) : 0;
310
+ }
311
+
312
+ /**
313
+ * Switch to a specific quality level
314
+ * @param {number} qualityIndex - Index of the quality level (-1 for auto, not applicable for HTML5)
315
+ */
316
+ switchQuality(qualityIndex: number) {
317
+ const qualities = this.getQualities();
318
+
319
+ if (qualityIndex < 0 || qualityIndex >= qualities.length) {
320
+ this.player.log('Invalid quality index', 'warn');
321
+ return;
322
+ }
323
+
324
+ const quality = qualities[qualityIndex];
325
+ if (!quality || !quality.src) {
326
+ return;
327
+ }
328
+ const currentTime = this.media.currentTime;
329
+ const wasPlaying = !this.media.paused;
330
+
331
+ // Store the current source for comparison
332
+ const currentSrc = this.media.currentSrc;
333
+
334
+ // Don't switch if already at this quality
335
+ if (currentSrc === quality.src) {
336
+ this.player.log('Already at this quality level', 'info');
337
+ return;
338
+ }
339
+
340
+ this.player.log(`Switching to quality: ${quality.name}`, 'info');
341
+
342
+ // Update the src
343
+ this.media.src = quality.src;
344
+
345
+ // Wait for the new source to load, then restore playback state
346
+ const onLoadedMetadata = () => {
347
+ this.media.removeEventListener('loadedmetadata', onLoadedMetadata);
348
+
349
+ // Restore playback position
350
+ this.media.currentTime = currentTime;
351
+
352
+ // Resume playback if it was playing
353
+ if (wasPlaying) {
354
+ this.media.play().catch(() => {
355
+ this.player.log('Failed to resume playback after quality switch', 'warn');
356
+ });
357
+ }
358
+
359
+ // Emit quality change event
360
+ this.player.emit('qualitychange', { quality: quality.name, index: qualityIndex });
361
+ };
362
+
363
+ this.media.addEventListener('loadedmetadata', onLoadedMetadata);
364
+ this.media.load();
365
+ }
366
+
367
+ /**
368
+ * Get current quality index
369
+ * @returns {number}
370
+ */
371
+ getCurrentQuality() {
372
+ const qualities = this.getQualities();
373
+ const currentSrc = this.media.currentSrc;
374
+
375
+ for (let i = 0; i < qualities.length; i++) {
376
+ if (qualities[i]?.src === currentSrc) {
377
+ return i;
378
+ }
379
+ }
380
+
381
+ return 0; // Default to first quality if not found
382
+ }
383
+
384
+ destroy() {
385
+ // Detach every media listener registered in attachEvents() in one shot.
386
+ this._listenerController.abort();
387
+ }
388
+ }
389
+