@therealtinhtute/baroiplayer 1.0.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 (244) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +384 -0
  3. package/dist/context/media.js +2 -0
  4. package/dist/context/media.js.map +1 -0
  5. package/dist/context/media.mjs +15 -0
  6. package/dist/context/media.mjs.map +1 -0
  7. package/dist/context/store.js +2 -0
  8. package/dist/context/store.js.map +1 -0
  9. package/dist/context/store.mjs +5 -0
  10. package/dist/context/store.mjs.map +1 -0
  11. package/dist/context-BMteAJDN.js +1140 -0
  12. package/dist/context-BMteAJDN.js.map +1 -0
  13. package/dist/context-DCJ_scmO.cjs +23 -0
  14. package/dist/context-DCJ_scmO.cjs.map +1 -0
  15. package/dist/core-events-B5W_j8yU.js +480 -0
  16. package/dist/core-events-B5W_j8yU.js.map +1 -0
  17. package/dist/core-events-CN0-Vhhq.cjs +2 -0
  18. package/dist/core-events-CN0-Vhhq.cjs.map +1 -0
  19. package/dist/core-plugins-Bq9KLany.js +696 -0
  20. package/dist/core-plugins-Bq9KLany.js.map +1 -0
  21. package/dist/core-plugins-CPDSHMiQ.cjs +2 -0
  22. package/dist/core-plugins-CPDSHMiQ.cjs.map +1 -0
  23. package/dist/core.js +37 -0
  24. package/dist/core.js.map +1 -0
  25. package/dist/core.mjs +1217 -0
  26. package/dist/core.mjs.map +1 -0
  27. package/dist/enhanced-components-DHv0MFvG.js +1073 -0
  28. package/dist/enhanced-components-DHv0MFvG.js.map +1 -0
  29. package/dist/enhanced-components-DTIObgB7.cjs +2 -0
  30. package/dist/enhanced-components-DTIObgB7.cjs.map +1 -0
  31. package/dist/hooks-C_r8wexn.cjs +4 -0
  32. package/dist/hooks-C_r8wexn.cjs.map +1 -0
  33. package/dist/hooks-Da2wVXlF.js +1630 -0
  34. package/dist/hooks-Da2wVXlF.js.map +1 -0
  35. package/dist/index.css +1 -0
  36. package/dist/index.js +112 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/index.mjs +6126 -0
  39. package/dist/index.mjs.map +1 -0
  40. package/dist/minimal-components-DSq5sQpy.js +2002 -0
  41. package/dist/minimal-components-DSq5sQpy.js.map +1 -0
  42. package/dist/minimal-components-DflWiihN.cjs +2 -0
  43. package/dist/minimal-components-DflWiihN.cjs.map +1 -0
  44. package/dist/minimal-components.css +1 -0
  45. package/dist/mona-loading.gif +0 -0
  46. package/dist/performance-monitor-CUW6f5ll.cjs +2 -0
  47. package/dist/performance-monitor-CUW6f5ll.cjs.map +1 -0
  48. package/dist/performance-monitor-rMW1RgV3.js +302 -0
  49. package/dist/performance-monitor-rMW1RgV3.js.map +1 -0
  50. package/dist/utils-BnscpYYd.js +2798 -0
  51. package/dist/utils-BnscpYYd.js.map +1 -0
  52. package/dist/utils-D_czS_0K.cjs +9 -0
  53. package/dist/utils-D_czS_0K.cjs.map +1 -0
  54. package/dist/utils.js +2 -0
  55. package/dist/utils.js.map +1 -0
  56. package/dist/utils.mjs +7 -0
  57. package/dist/utils.mjs.map +1 -0
  58. package/package.json +114 -0
  59. package/src/adapters/videojs-adapter.tsx +470 -0
  60. package/src/assets/mona-loading.gif +0 -0
  61. package/src/components/AnimateLoading.tsx +115 -0
  62. package/src/components/LoadingState.tsx +248 -0
  63. package/src/components/accessibility/keyboard-help-overlay.tsx +213 -0
  64. package/src/components/audio-player.tsx +85 -0
  65. package/src/components/basic/index.ts +37 -0
  66. package/src/components/enhanced/advanced-features.tsx +316 -0
  67. package/src/components/enhanced/debug-panel.tsx +422 -0
  68. package/src/components/enhanced/enhanced-controls.tsx +600 -0
  69. package/src/components/enhanced/enhanced-media-player.tsx +509 -0
  70. package/src/components/enhanced/error-handling.tsx +345 -0
  71. package/src/components/enhanced/index.ts +39 -0
  72. package/src/components/enhanced/media-player-enhanced.tsx +349 -0
  73. package/src/components/examples/basic-video-player.tsx +55 -0
  74. package/src/components/examples/custom-controls-example.tsx +63 -0
  75. package/src/components/examples/enhanced-controls-example.tsx +114 -0
  76. package/src/components/examples/enhanced-media-player-example.tsx +293 -0
  77. package/src/components/examples/loading-states-demo.tsx +142 -0
  78. package/src/components/examples/react-19-demo.tsx +381 -0
  79. package/src/components/examples/volume-orientation-demo.tsx +114 -0
  80. package/src/components/icon-demo.tsx +209 -0
  81. package/src/components/icons/index.tsx +166 -0
  82. package/src/components/icons/svg-icons/ArrowLeftIcon.tsx +18 -0
  83. package/src/components/icons/svg-icons/ArrowRightIcon.tsx +18 -0
  84. package/src/components/icons/svg-icons/AudioIcon.tsx +22 -0
  85. package/src/components/icons/svg-icons/BackwardIcon.tsx +12 -0
  86. package/src/components/icons/svg-icons/CameraIcon.tsx +19 -0
  87. package/src/components/icons/svg-icons/CheckIcon.tsx +19 -0
  88. package/src/components/icons/svg-icons/EnablePiPIcon.tsx +17 -0
  89. package/src/components/icons/svg-icons/ForwardIcon.tsx +12 -0
  90. package/src/components/icons/svg-icons/FullscreenEnterIcon.tsx +23 -0
  91. package/src/components/icons/svg-icons/FullscreenExitIcon.tsx +23 -0
  92. package/src/components/icons/svg-icons/LoadingIcon.tsx +32 -0
  93. package/src/components/icons/svg-icons/PauseIcon.tsx +23 -0
  94. package/src/components/icons/svg-icons/PlayIcon.tsx +21 -0
  95. package/src/components/icons/svg-icons/PlaybackSpeedIcon.tsx +23 -0
  96. package/src/components/icons/svg-icons/QualityIcon.tsx +21 -0
  97. package/src/components/icons/svg-icons/SettingsIcon.tsx +12 -0
  98. package/src/components/icons/svg-icons/SliderIcon.tsx +21 -0
  99. package/src/components/icons/svg-icons/SubtitleIcon.tsx +21 -0
  100. package/src/components/icons/svg-icons/SubtitleOffIcon.tsx +25 -0
  101. package/src/components/icons/svg-icons/UnPiPIcon.tsx +17 -0
  102. package/src/components/icons/svg-icons/VolumeMutedIcon.tsx +16 -0
  103. package/src/components/icons/svg-icons/VolumeOneIcon.tsx +22 -0
  104. package/src/components/icons/svg-icons/VolumeThreeIcon.tsx +16 -0
  105. package/src/components/icons/svg-icons/VolumeTwoIcon.tsx +16 -0
  106. package/src/components/legacy.tsx +93 -0
  107. package/src/components/media-metadata-display.tsx +100 -0
  108. package/src/components/media-player-audio.tsx +39 -0
  109. package/src/components/media-player-controls.tsx +226 -0
  110. package/src/components/media-player-error.tsx +163 -0
  111. package/src/components/media-player-fullscreen.tsx +109 -0
  112. package/src/components/media-player-hls.tsx +111 -0
  113. package/src/components/media-player-loading.tsx +76 -0
  114. package/src/components/media-player-play.tsx +75 -0
  115. package/src/components/media-player-root.tsx +340 -0
  116. package/src/components/media-player-seek.tsx +168 -0
  117. package/src/components/media-player-time.tsx +99 -0
  118. package/src/components/media-player-tooltip.tsx +58 -0
  119. package/src/components/media-player-video.tsx +39 -0
  120. package/src/components/media-player-volume.tsx +155 -0
  121. package/src/components/media-player.tsx +257 -0
  122. package/src/components/media-suspense.tsx +362 -0
  123. package/src/components/player-settings-form.tsx +163 -0
  124. package/src/components/react-player-wrapper.tsx +651 -0
  125. package/src/components/simple-video-player.tsx +60 -0
  126. package/src/components/ui/enhanced-seek-bar.tsx +235 -0
  127. package/src/components/ui/fullscreen-button.tsx +60 -0
  128. package/src/components/ui/media-resource-preloader.tsx +395 -0
  129. package/src/components/ui/play-button.tsx +107 -0
  130. package/src/components/ui/seek-bar.tsx +161 -0
  131. package/src/components/ui/time-display.tsx +40 -0
  132. package/src/components/ui/volume-control.tsx +143 -0
  133. package/src/components/unified/base-media-player.tsx +246 -0
  134. package/src/components/unified/media-player.tsx +75 -0
  135. package/src/components/variants/minimal/AccessibleMinimalPlayer.tsx +561 -0
  136. package/src/components/variants/minimal/EnhancedMinimalMediaPlayer.tsx +552 -0
  137. package/src/components/variants/minimal/MinimalControls.tsx +142 -0
  138. package/src/components/variants/minimal/MinimalLoading.tsx +32 -0
  139. package/src/components/variants/minimal/MinimalMediaPlayer.tsx +315 -0
  140. package/src/components/variants/minimal/MinimalOverlay.tsx +64 -0
  141. package/src/components/variants/minimal/MinimalPlayButton.tsx +65 -0
  142. package/src/components/variants/minimal/MinimalSeekBar.tsx +150 -0
  143. package/src/components/variants/minimal/MinimalVolumeControl.tsx +80 -0
  144. package/src/components/variants/minimal/SimpleMinimalPlayer.tsx +210 -0
  145. package/src/components/variants/minimal/components/CenterPlayButton.tsx +22 -0
  146. package/src/components/variants/minimal/components/ControlBar.tsx +146 -0
  147. package/src/components/variants/minimal/components/EnhancedSettingsMenu.tsx +361 -0
  148. package/src/components/variants/minimal/components/PlaybackControls.tsx +51 -0
  149. package/src/components/variants/minimal/components/RightControls.tsx +173 -0
  150. package/src/components/variants/minimal/components/SeekBar.tsx +131 -0
  151. package/src/components/variants/minimal/components/SettingsDropdown.tsx +70 -0
  152. package/src/components/variants/minimal/components/SubtitleDisplay.tsx +21 -0
  153. package/src/components/variants/minimal/components/TimeDisplay.tsx +18 -0
  154. package/src/components/variants/minimal/components/VideoElement.tsx +40 -0
  155. package/src/components/variants/minimal/components/VolumeControl.tsx +217 -0
  156. package/src/components/variants/minimal/index.ts +23 -0
  157. package/src/components/video-player.tsx +100 -0
  158. package/src/context/error-handling.ts +394 -0
  159. package/src/context/media-context.tsx +208 -0
  160. package/src/context/media-provider.tsx +265 -0
  161. package/src/context/media-store.ts +379 -0
  162. package/src/context/persistence.ts +453 -0
  163. package/src/core/events/event-system.ts +442 -0
  164. package/src/core/events/hooks.ts +302 -0
  165. package/src/core/events/index.ts +22 -0
  166. package/src/core/events/middleware.ts +269 -0
  167. package/src/core/index.ts +22 -0
  168. package/src/core/optimization/bundle-optimizer.tsx +308 -0
  169. package/src/core/performance/cicd.ts +96 -0
  170. package/src/core/performance/index.ts +38 -0
  171. package/src/core/performance/media-loading-optimizer.ts +538 -0
  172. package/src/core/performance/performance-budgets.ts +432 -0
  173. package/src/core/performance/performance-monitor.tsx +476 -0
  174. package/src/core/performance/performance-testing.ts +459 -0
  175. package/src/core/plugins/base-plugin.ts +224 -0
  176. package/src/core/plugins/enhanced-plugin-system.ts +411 -0
  177. package/src/core/plugins/examples/analytics-plugin.ts +242 -0
  178. package/src/core/plugins/index.ts +39 -0
  179. package/src/core/plugins/registry.ts +327 -0
  180. package/src/core/plugins/types.ts +159 -0
  181. package/src/core/plugins/utils.ts +316 -0
  182. package/src/core/progressive-loading.tsx +322 -0
  183. package/src/core/providers/enhanced-hls-provider.ts +339 -0
  184. package/src/core/providers/service-provider.tsx +168 -0
  185. package/src/core/utils.tsx +38 -0
  186. package/src/features/media-session/media-session-manager.ts +429 -0
  187. package/src/features/picture-in-picture/pip-manager.ts +429 -0
  188. package/src/features/playlist/playlist-manager.ts +606 -0
  189. package/src/features/subtitles/subtitle-manager.ts +441 -0
  190. package/src/features/subtitles/subtitle-parser.ts +280 -0
  191. package/src/features/thumbnails/thumbnail-generator.ts +519 -0
  192. package/src/hooks/accessibility/index.ts +27 -0
  193. package/src/hooks/accessibility/use-accessibility-preferences.ts +188 -0
  194. package/src/hooks/accessibility/use-focus-management.ts +176 -0
  195. package/src/hooks/accessibility/use-keyboard-shortcuts.ts +144 -0
  196. package/src/hooks/accessibility/use-screen-reader.ts +182 -0
  197. package/src/hooks/core/index.ts +47 -0
  198. package/src/hooks/legacy.ts +27 -0
  199. package/src/hooks/use-enhanced-media-player.ts +468 -0
  200. package/src/hooks/use-intelligent-preloading.ts +358 -0
  201. package/src/hooks/use-keyboard-controls.ts +115 -0
  202. package/src/hooks/use-media-actions.ts +175 -0
  203. package/src/hooks/use-media-metadata.ts +89 -0
  204. package/src/hooks/use-media-optimistic.ts +270 -0
  205. package/src/hooks/use-media-player.ts +312 -0
  206. package/src/hooks/use-media-state.ts +190 -0
  207. package/src/hooks/use-memory-optimization.ts +358 -0
  208. package/src/hooks/use-optimistic-controls.ts +92 -0
  209. package/src/hooks/use-optimized-media.ts +263 -0
  210. package/src/hooks/use-touch-gestures.ts +417 -0
  211. package/src/hooks/useEnhancedSubtitles.ts +258 -0
  212. package/src/hooks/useKeyboardShortcuts.ts +74 -0
  213. package/src/hooks/useMediaControls.ts +52 -0
  214. package/src/hooks/useScreenshot.ts +57 -0
  215. package/src/hooks/useSubtitles.ts +91 -0
  216. package/src/hooks/useVideoPlayer.ts +232 -0
  217. package/src/icons/components/FullscreenIcon.tsx +37 -0
  218. package/src/icons/components/SettingsIcon.tsx +38 -0
  219. package/src/icons/components/animated/LoadingIcon.tsx +43 -0
  220. package/src/icons/components/animated/PlayPauseIcon.tsx +50 -0
  221. package/src/icons/components/media/PauseIcon.tsx +39 -0
  222. package/src/icons/components/media/PlayIcon.tsx +38 -0
  223. package/src/icons/components/volume/VolumeIcon.tsx +39 -0
  224. package/src/icons/components/volume/VolumeOffIcon.tsx +39 -0
  225. package/src/icons/index.ts +56 -0
  226. package/src/icons/provider.tsx +64 -0
  227. package/src/icons/types.ts +40 -0
  228. package/src/index-enhanced.ts +35 -0
  229. package/src/index-legacy.ts +326 -0
  230. package/src/index.css +102 -0
  231. package/src/index.ts +216 -0
  232. package/src/lib/deprecation.ts +51 -0
  233. package/src/lib/format-time.ts +37 -0
  234. package/src/lib/index.ts +3 -0
  235. package/src/lib/utils.ts +6 -0
  236. package/src/styles/globals.css +462 -0
  237. package/src/styles/minimal-player.css +589 -0
  238. package/src/styles/pixel-art.css +46 -0
  239. package/src/styles/videojs.css +100 -0
  240. package/src/test/setup.ts +79 -0
  241. package/src/types/events.ts +47 -0
  242. package/src/types/formats.ts +95 -0
  243. package/src/types/index.ts +190 -0
  244. package/src/types/videojs-plugins.d.ts +14 -0
@@ -0,0 +1,606 @@
1
+ export interface PlaylistItem {
2
+ id: string
3
+ title: string
4
+ src: string
5
+ duration?: number
6
+ thumbnail?: string
7
+ metadata?: {
8
+ artist?: string
9
+ album?: string
10
+ genre?: string
11
+ year?: number
12
+ description?: string
13
+ tags?: string[]
14
+ }
15
+ subtitles?: SubtitleTrack[]
16
+ chapters?: ChapterMarker[]
17
+ }
18
+
19
+ export interface SubtitleTrack {
20
+ id: string
21
+ label: string
22
+ language: string
23
+ src: string
24
+ format: 'vtt' | 'srt'
25
+ default?: boolean
26
+ }
27
+
28
+ export interface ChapterMarker {
29
+ id: string
30
+ title: string
31
+ startTime: number
32
+ endTime?: number
33
+ thumbnail?: string
34
+ }
35
+
36
+ export interface PlaylistConfig {
37
+ shuffle?: boolean
38
+ repeat?: 'none' | 'one' | 'all'
39
+ autoplay?: boolean
40
+ crossfade?: number
41
+ gapless?: boolean
42
+ }
43
+
44
+ export interface PlaylistEventData {
45
+ playlist?: PlaylistItem[]
46
+ currentItem?: PlaylistItem
47
+ currentIndex?: number
48
+ previousItem?: PlaylistItem
49
+ nextItem?: PlaylistItem
50
+ }
51
+
52
+ export type PlaylistEvent =
53
+ | 'itemAdded'
54
+ | 'itemRemoved'
55
+ | 'itemChanged'
56
+ | 'playlistChanged'
57
+ | 'currentItemChanged'
58
+ | 'configChanged'
59
+ | 'shuffleChanged'
60
+ | 'repeatChanged'
61
+
62
+ export class PlaylistManager {
63
+ private items: PlaylistItem[] = []
64
+ private currentIndex: number = -1
65
+ private originalOrder: PlaylistItem[] = []
66
+ private config: PlaylistConfig = {
67
+ shuffle: false,
68
+ repeat: 'none',
69
+ autoplay: true,
70
+ crossfade: 0,
71
+ gapless: false
72
+ }
73
+ private eventListeners: Map<PlaylistEvent, Set<Function>> = new Map()
74
+ private history: number[] = []
75
+ private historyIndex: number = -1
76
+
77
+ constructor(config?: Partial<PlaylistConfig>) {
78
+ if (config) {
79
+ this.updateConfig(config)
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Add item(s) to playlist
85
+ */
86
+ add(items: PlaylistItem | PlaylistItem[], index?: number): void {
87
+ const itemsToAdd = Array.isArray(items) ? items : [items]
88
+
89
+ if (index !== undefined && index >= 0 && index <= this.items.length) {
90
+ this.items.splice(index, 0, ...itemsToAdd)
91
+ this.originalOrder.splice(index, 0, ...itemsToAdd)
92
+
93
+ // Adjust current index if needed
94
+ if (index <= this.currentIndex) {
95
+ this.currentIndex += itemsToAdd.length
96
+ }
97
+ } else {
98
+ this.items.push(...itemsToAdd)
99
+ this.originalOrder.push(...itemsToAdd)
100
+ }
101
+
102
+ // If this is the first item and no current item is set
103
+ if (this.currentIndex === -1 && this.items.length > 0) {
104
+ this.currentIndex = 0
105
+ this.addToHistory(0)
106
+ }
107
+
108
+ itemsToAdd.forEach(item => {
109
+ this.emit('itemAdded', { currentItem: item, playlist: this.items })
110
+ })
111
+
112
+ this.emit('playlistChanged', { playlist: this.items })
113
+ }
114
+
115
+ /**
116
+ * Remove item(s) from playlist
117
+ */
118
+ remove(indices: number | number[]): PlaylistItem[] {
119
+ const indicesToRemove = Array.isArray(indices) ? indices.sort((a, b) => b - a) : [indices]
120
+ const removedItems: PlaylistItem[] = []
121
+
122
+ indicesToRemove.forEach(index => {
123
+ if (index >= 0 && index < this.items.length) {
124
+ const [removedItem] = this.items.splice(index, 1)
125
+ const [originalItem] = this.originalOrder.splice(
126
+ this.originalOrder.findIndex(item => item.id === removedItem.id), 1
127
+ )
128
+
129
+ removedItems.push(removedItem)
130
+
131
+ // Adjust current index
132
+ if (index < this.currentIndex) {
133
+ this.currentIndex--
134
+ } else if (index === this.currentIndex) {
135
+ // If we removed the current item, stay at the same index
136
+ // (which now points to the next item)
137
+ if (this.currentIndex >= this.items.length) {
138
+ this.currentIndex = this.items.length - 1
139
+ }
140
+ }
141
+
142
+ this.emit('itemRemoved', { currentItem: removedItem, playlist: this.items })
143
+ }
144
+ })
145
+
146
+ // Update current index bounds
147
+ if (this.items.length === 0) {
148
+ this.currentIndex = -1
149
+ this.clearHistory()
150
+ } else if (this.currentIndex >= this.items.length) {
151
+ this.currentIndex = this.items.length - 1
152
+ }
153
+
154
+ this.emit('playlistChanged', { playlist: this.items })
155
+ return removedItems
156
+ }
157
+
158
+ /**
159
+ * Move item to new position
160
+ */
161
+ move(fromIndex: number, toIndex: number): boolean {
162
+ if (fromIndex < 0 || fromIndex >= this.items.length ||
163
+ toIndex < 0 || toIndex >= this.items.length ||
164
+ fromIndex === toIndex) {
165
+ return false
166
+ }
167
+
168
+ const [movedItem] = this.items.splice(fromIndex, 1)
169
+ this.items.splice(toIndex, 0, movedItem)
170
+
171
+ // Update current index
172
+ if (fromIndex === this.currentIndex) {
173
+ this.currentIndex = toIndex
174
+ } else if (fromIndex < this.currentIndex && toIndex >= this.currentIndex) {
175
+ this.currentIndex--
176
+ } else if (fromIndex > this.currentIndex && toIndex <= this.currentIndex) {
177
+ this.currentIndex++
178
+ }
179
+
180
+ this.emit('playlistChanged', { playlist: this.items })
181
+ return true
182
+ }
183
+
184
+ /**
185
+ * Clear all items
186
+ */
187
+ clear(): void {
188
+ this.items = []
189
+ this.originalOrder = []
190
+ this.currentIndex = -1
191
+ this.clearHistory()
192
+ this.emit('playlistChanged', { playlist: this.items })
193
+ }
194
+
195
+ /**
196
+ * Get current item
197
+ */
198
+ getCurrentItem(): PlaylistItem | null {
199
+ return this.currentIndex >= 0 && this.currentIndex < this.items.length
200
+ ? this.items[this.currentIndex]
201
+ : null
202
+ }
203
+
204
+ /**
205
+ * Get current index
206
+ */
207
+ getCurrentIndex(): number {
208
+ return this.currentIndex
209
+ }
210
+
211
+ /**
212
+ * Set current item by index
213
+ */
214
+ setCurrentIndex(index: number): boolean {
215
+ if (index >= 0 && index < this.items.length) {
216
+ const previousIndex = this.currentIndex
217
+ this.currentIndex = index
218
+ this.addToHistory(index)
219
+
220
+ this.emit('currentItemChanged', {
221
+ currentItem: this.items[index],
222
+ currentIndex: index,
223
+ previousItem: previousIndex >= 0 ? this.items[previousIndex] : undefined
224
+ })
225
+ return true
226
+ }
227
+ return false
228
+ }
229
+
230
+ /**
231
+ * Set current item by ID
232
+ */
233
+ setCurrentById(id: string): boolean {
234
+ const index = this.items.findIndex(item => item.id === id)
235
+ return index >= 0 ? this.setCurrentIndex(index) : false
236
+ }
237
+
238
+ /**
239
+ * Get next item
240
+ */
241
+ getNext(): PlaylistItem | null {
242
+ const nextIndex = this.getNextIndex()
243
+ return nextIndex >= 0 ? this.items[nextIndex] : null
244
+ }
245
+
246
+ /**
247
+ * Get previous item
248
+ */
249
+ getPrevious(): PlaylistItem | null {
250
+ const prevIndex = this.getPreviousIndex()
251
+ return prevIndex >= 0 ? this.items[prevIndex] : null
252
+ }
253
+
254
+ /**
255
+ * Move to next item
256
+ */
257
+ next(): boolean {
258
+ const nextIndex = this.getNextIndex()
259
+ return nextIndex >= 0 ? this.setCurrentIndex(nextIndex) : false
260
+ }
261
+
262
+ /**
263
+ * Move to previous item
264
+ */
265
+ previous(): boolean {
266
+ // Check history first for better navigation
267
+ if (this.historyIndex > 0) {
268
+ this.historyIndex--
269
+ const historyIndex = this.history[this.historyIndex]
270
+ if (historyIndex >= 0 && historyIndex < this.items.length) {
271
+ this.currentIndex = historyIndex
272
+ this.emit('currentItemChanged', {
273
+ currentItem: this.items[this.currentIndex],
274
+ currentIndex: this.currentIndex
275
+ })
276
+ return true
277
+ }
278
+ }
279
+
280
+ const prevIndex = this.getPreviousIndex()
281
+ return prevIndex >= 0 ? this.setCurrentIndex(prevIndex) : false
282
+ }
283
+
284
+ /**
285
+ * Get all items
286
+ */
287
+ getItems(): PlaylistItem[] {
288
+ return [...this.items]
289
+ }
290
+
291
+ /**
292
+ * Get item by index
293
+ */
294
+ getItem(index: number): PlaylistItem | null {
295
+ return index >= 0 && index < this.items.length ? this.items[index] : null
296
+ }
297
+
298
+ /**
299
+ * Get item by ID
300
+ */
301
+ getItemById(id: string): PlaylistItem | null {
302
+ return this.items.find(item => item.id === id) || null
303
+ }
304
+
305
+ /**
306
+ * Find item index by ID
307
+ */
308
+ findIndex(id: string): number {
309
+ return this.items.findIndex(item => item.id === id)
310
+ }
311
+
312
+ /**
313
+ * Get playlist length
314
+ */
315
+ length(): number {
316
+ return this.items.length
317
+ }
318
+
319
+ /**
320
+ * Check if playlist is empty
321
+ */
322
+ isEmpty(): boolean {
323
+ return this.items.length === 0
324
+ }
325
+
326
+ /**
327
+ * Toggle shuffle mode
328
+ */
329
+ toggleShuffle(): boolean {
330
+ this.config.shuffle = !this.config.shuffle
331
+
332
+ if (this.config.shuffle) {
333
+ this.shufflePlaylist()
334
+ } else {
335
+ this.unshufflePlaylist()
336
+ }
337
+
338
+ this.emit('shuffleChanged', { playlist: this.items })
339
+ this.emit('configChanged', { playlist: this.items })
340
+ return this.config.shuffle
341
+ }
342
+
343
+ /**
344
+ * Set repeat mode
345
+ */
346
+ setRepeat(mode: 'none' | 'one' | 'all'): void {
347
+ this.config.repeat = mode
348
+ this.emit('repeatChanged', { playlist: this.items })
349
+ this.emit('configChanged', { playlist: this.items })
350
+ }
351
+
352
+ /**
353
+ * Update configuration
354
+ */
355
+ updateConfig(config: Partial<PlaylistConfig>): void {
356
+ this.config = { ...this.config, ...config }
357
+ this.emit('configChanged', { playlist: this.items })
358
+ }
359
+
360
+ /**
361
+ * Get configuration
362
+ */
363
+ getConfig(): PlaylistConfig {
364
+ return { ...this.config }
365
+ }
366
+
367
+ /**
368
+ * Add event listener
369
+ */
370
+ addEventListener(event: PlaylistEvent, callback: Function): () => void {
371
+ if (!this.eventListeners.has(event)) {
372
+ this.eventListeners.set(event, new Set())
373
+ }
374
+
375
+ this.eventListeners.get(event)!.add(callback)
376
+
377
+ return () => {
378
+ const listeners = this.eventListeners.get(event)
379
+ if (listeners) {
380
+ listeners.delete(callback)
381
+ }
382
+ }
383
+ }
384
+
385
+ /**
386
+ * Get next index based on current configuration
387
+ */
388
+ private getNextIndex(): number {
389
+ if (this.items.length === 0) return -1
390
+
391
+ if (this.config.repeat === 'one') {
392
+ return this.currentIndex
393
+ }
394
+
395
+ if (this.currentIndex < this.items.length - 1) {
396
+ return this.currentIndex + 1
397
+ }
398
+
399
+ if (this.config.repeat === 'all') {
400
+ return 0
401
+ }
402
+
403
+ return -1
404
+ }
405
+
406
+ /**
407
+ * Get previous index
408
+ */
409
+ private getPreviousIndex(): number {
410
+ if (this.items.length === 0) return -1
411
+
412
+ if (this.config.repeat === 'one') {
413
+ return this.currentIndex
414
+ }
415
+
416
+ if (this.currentIndex > 0) {
417
+ return this.currentIndex - 1
418
+ }
419
+
420
+ if (this.config.repeat === 'all') {
421
+ return this.items.length - 1
422
+ }
423
+
424
+ return -1
425
+ }
426
+
427
+ /**
428
+ * Shuffle playlist
429
+ */
430
+ private shufflePlaylist(): void {
431
+ if (this.items.length <= 1) return
432
+
433
+ const currentItem = this.getCurrentItem()
434
+
435
+ // Fisher-Yates shuffle
436
+ for (let i = this.items.length - 1; i > 0; i--) {
437
+ const j = Math.floor(Math.random() * (i + 1))
438
+ ;[this.items[i], this.items[j]] = [this.items[j], this.items[i]]
439
+ }
440
+
441
+ // Move current item to the beginning if it exists
442
+ if (currentItem) {
443
+ const currentItemIndex = this.items.findIndex(item => item.id === currentItem.id)
444
+ if (currentItemIndex > 0) {
445
+ ;[this.items[0], this.items[currentItemIndex]] = [this.items[currentItemIndex], this.items[0]]
446
+ }
447
+ this.currentIndex = 0
448
+ }
449
+ }
450
+
451
+ /**
452
+ * Restore original order
453
+ */
454
+ private unshufflePlaylist(): void {
455
+ const currentItem = this.getCurrentItem()
456
+ this.items = [...this.originalOrder]
457
+
458
+ if (currentItem) {
459
+ this.currentIndex = this.items.findIndex(item => item.id === currentItem.id)
460
+ }
461
+ }
462
+
463
+ /**
464
+ * Add to history
465
+ */
466
+ private addToHistory(index: number): void {
467
+ // Remove any future history items
468
+ this.history = this.history.slice(0, this.historyIndex + 1)
469
+
470
+ // Add new index
471
+ this.history.push(index)
472
+ this.historyIndex = this.history.length - 1
473
+
474
+ // Limit history size
475
+ const maxHistory = 50
476
+ if (this.history.length > maxHistory) {
477
+ this.history = this.history.slice(-maxHistory)
478
+ this.historyIndex = this.history.length - 1
479
+ }
480
+ }
481
+
482
+ /**
483
+ * Clear history
484
+ */
485
+ private clearHistory(): void {
486
+ this.history = []
487
+ this.historyIndex = -1
488
+ }
489
+
490
+ /**
491
+ * Emit events
492
+ */
493
+ private emit(event: PlaylistEvent, data?: PlaylistEventData): void {
494
+ const listeners = this.eventListeners.get(event)
495
+ if (listeners) {
496
+ listeners.forEach(callback => {
497
+ try {
498
+ callback(data)
499
+ } catch (error) {
500
+ console.error(`Error in playlist event listener for ${event}:`, error)
501
+ }
502
+ })
503
+ }
504
+ }
505
+
506
+ /**
507
+ * Export playlist as JSON
508
+ */
509
+ export(): string {
510
+ return JSON.stringify({
511
+ items: this.items,
512
+ currentIndex: this.currentIndex,
513
+ config: this.config
514
+ }, null, 2)
515
+ }
516
+
517
+ /**
518
+ * Import playlist from JSON
519
+ */
520
+ import(jsonData: string): boolean {
521
+ try {
522
+ const data = JSON.parse(jsonData)
523
+
524
+ if (data.items && Array.isArray(data.items)) {
525
+ this.clear()
526
+ this.add(data.items)
527
+
528
+ if (typeof data.currentIndex === 'number') {
529
+ this.setCurrentIndex(data.currentIndex)
530
+ }
531
+
532
+ if (data.config) {
533
+ this.updateConfig(data.config)
534
+ }
535
+
536
+ return true
537
+ }
538
+ } catch (error) {
539
+ console.error('Failed to import playlist:', error)
540
+ }
541
+
542
+ return false
543
+ }
544
+
545
+ /**
546
+ * Create a new playlist from URLs
547
+ */
548
+ static fromUrls(urls: string[], titleExtractor?: (url: string) => string): PlaylistManager {
549
+ const playlist = new PlaylistManager()
550
+ const items: PlaylistItem[] = urls.map((url, index) => ({
551
+ id: `item-${Date.now()}-${index}`,
552
+ title: titleExtractor ? titleExtractor(url) : `Media ${index + 1}`,
553
+ src: url
554
+ }))
555
+
556
+ playlist.add(items)
557
+ return playlist
558
+ }
559
+
560
+ /**
561
+ * Cleanup resources
562
+ */
563
+ destroy(): void {
564
+ this.clear()
565
+ this.eventListeners.clear()
566
+ this.clearHistory()
567
+ }
568
+ }
569
+
570
+ // Utility functions for playlist management
571
+ export const playlistUtils = {
572
+ /**
573
+ * Extract title from URL
574
+ */
575
+ extractTitleFromUrl(url: string): string {
576
+ try {
577
+ const urlObj = new URL(url)
578
+ const pathname = urlObj.pathname
579
+ const filename = pathname.split('/').pop() || 'Media'
580
+
581
+ // Remove file extension
582
+ return filename.split('.')[0] || 'Media'
583
+ } catch {
584
+ return 'Media'
585
+ }
586
+ },
587
+
588
+ /**
589
+ * Generate unique ID
590
+ */
591
+ generateId(): string {
592
+ return `playlist-item-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
593
+ },
594
+
595
+ /**
596
+ * Validate playlist item
597
+ */
598
+ validateItem(item: any): item is PlaylistItem {
599
+ return (
600
+ typeof item === 'object' &&
601
+ typeof item.id === 'string' &&
602
+ typeof item.title === 'string' &&
603
+ typeof item.src === 'string'
604
+ )
605
+ }
606
+ }