vidply 1.2.0 → 1.2.1
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.
- package/README.md +946 -946
- package/dist/dev/{vidply.AudioDescriptionManager-R47Q4YR4.js → vidply.AudioDescriptionManager-6RFVF27Z.js} +275 -19
- package/dist/dev/vidply.AudioDescriptionManager-6RFVF27Z.js.map +7 -0
- package/dist/dev/{vidply.CaptionStyleMenu-7XHRBWUM.js → vidply.CaptionStyleMenu-R2SCQ6VP.js} +4 -4
- package/dist/dev/{vidply.DASHRenderer-PWEMC3BT.js → vidply.DASHRenderer-EOVEW73H.js} +3 -3
- package/dist/dev/vidply.DASHRenderer-EOVEW73H.js.map +7 -0
- package/dist/dev/{vidply.FloatingPlayerManager-PKNNNAS2.js → vidply.FloatingPlayerManager-V5XQWS75.js} +5 -5
- package/dist/dev/vidply.FloatingPlayerManager-V5XQWS75.js.map +7 -0
- package/dist/dev/{vidply.HLSRenderer-E7RQ74HI.js → vidply.HLSRenderer-ZBEFVH5W.js} +4 -4
- package/dist/dev/vidply.HLSRenderer-ZBEFVH5W.js.map +7 -0
- package/dist/dev/vidply.HTML5Renderer-P7ICKYU4.js +12 -0
- package/dist/dev/{vidply.SignLanguageManager-FET5FQZ7.js → vidply.SignLanguageManager-7IAJOVF6.js} +8 -8
- package/dist/dev/vidply.SignLanguageManager-7IAJOVF6.js.map +7 -0
- package/dist/dev/{vidply.SoundCloudRenderer-RVSJZODO.js → vidply.SoundCloudRenderer-OA7QQYGN.js} +3 -3
- package/dist/dev/vidply.SoundCloudRenderer-OA7QQYGN.js.map +7 -0
- package/dist/dev/{vidply.TranscriptManager-5GXOQCLX.js → vidply.TranscriptManager-BPM3T23L.js} +10 -10
- package/dist/dev/vidply.TranscriptManager-BPM3T23L.js.map +7 -0
- package/dist/dev/{vidply.VimeoRenderer-ER2LSBBW.js → vidply.VimeoRenderer-BRHFYPNJ.js} +3 -3
- package/dist/dev/vidply.VimeoRenderer-BRHFYPNJ.js.map +7 -0
- package/dist/dev/{vidply.YouTubeRenderer-NFTOWJAB.js → vidply.YouTubeRenderer-HRTN4EJ5.js} +3 -3
- package/dist/dev/vidply.YouTubeRenderer-HRTN4EJ5.js.map +7 -0
- package/dist/dev/{vidply.chunk-6RQA4ROD.js → vidply.chunk-5IK3IC5V.js} +2 -2
- package/dist/dev/{vidply.chunk-JJ47NO24.js → vidply.chunk-C55OBT5H.js} +2 -2
- package/dist/dev/vidply.chunk-C55OBT5H.js.map +7 -0
- package/dist/dev/{vidply.chunk-DZSDO3B7.js → vidply.chunk-HOYEMJSD.js} +2 -2
- package/dist/dev/{vidply.chunk-VMSQAZM7.js → vidply.chunk-JAKZNGOR.js} +2 -2
- package/dist/dev/{vidply.chunk-64WXMTGL.js → vidply.chunk-LDVFAFGC.js} +5 -5
- package/dist/dev/vidply.chunk-LDVFAFGC.js.map +7 -0
- package/dist/dev/{vidply.chunk-RZWZGDV7.js → vidply.chunk-NSMAZCMB.js} +6 -6
- package/dist/dev/vidply.chunk-NSMAZCMB.js.map +7 -0
- package/dist/dev/{vidply.chunk-KI7C3QVL.js → vidply.chunk-RQG3MA7W.js} +2 -2
- package/dist/dev/vidply.chunk-RQG3MA7W.js.map +7 -0
- package/dist/dev/{vidply.chunk-BNFIUL2Z.js → vidply.chunk-UHMVDBDB.js} +3 -3
- package/dist/dev/vidply.chunk-UHMVDBDB.js.map +7 -0
- package/dist/dev/{vidply.chunk-NQPURMQY.js → vidply.chunk-W5BR65JY.js} +6 -6
- package/dist/dev/vidply.chunk-W5BR65JY.js.map +7 -0
- package/dist/dev/{vidply.chunk-W25HUCHS.js → vidply.chunk-XAK3DPTO.js} +2 -2
- package/dist/dev/vidply.chunk-XAK3DPTO.js.map +7 -0
- package/dist/dev/{vidply.chunk-SK4V2DAU.js → vidply.chunk-YVCYQZJI.js} +3 -3
- package/dist/dev/{vidply.de-4YAT7FFM.js → vidply.de-5W5LBFSJ.js} +2 -2
- package/dist/dev/vidply.de-5W5LBFSJ.js.map +7 -0
- package/dist/dev/{vidply.es-LEWIRUBH.js → vidply.es-SHDAT6L6.js} +2 -2
- package/dist/dev/vidply.es-SHDAT6L6.js.map +7 -0
- package/dist/dev/vidply.esm.js +73 -34
- package/dist/dev/vidply.esm.js.map +2 -2
- package/dist/dev/{vidply.fr-Q4GV2NOE.js → vidply.fr-2JANOMNB.js} +2 -2
- package/dist/dev/vidply.fr-2JANOMNB.js.map +7 -0
- package/dist/dev/{vidply.ja-S6ITA6WU.js → vidply.ja-WWMSTEJH.js} +2 -2
- package/dist/dev/vidply.ja-WWMSTEJH.js.map +7 -0
- package/dist/legacy/vidply.js +345 -32
- package/dist/legacy/vidply.js.map +3 -3
- package/dist/legacy/vidply.min.js +2 -2
- package/dist/legacy/vidply.min.meta.json +66 -47
- package/dist/prod/vidply.AudioDescriptionManager-YTDVDQBO.min.js +6 -0
- package/dist/prod/{vidply.CaptionStyleMenu-M3KVQWB5.min.js → vidply.CaptionStyleMenu-2AFF3HF2.min.js} +2 -2
- package/dist/prod/{vidply.DASHRenderer-DN3S27AV.min.js → vidply.DASHRenderer-URDOTOKS.min.js} +2 -2
- package/dist/prod/{vidply.FloatingPlayerManager-SKAZ4J43.min.js → vidply.FloatingPlayerManager-QUBFK7CQ.min.js} +2 -2
- package/dist/prod/{vidply.HLSRenderer-62HH4YTK.min.js → vidply.HLSRenderer-46KNUVAB.min.js} +2 -2
- package/dist/prod/vidply.HTML5Renderer-7TQDMIGY.min.js +6 -0
- package/dist/prod/{vidply.SignLanguageManager-V3OBTSSF.min.js → vidply.SignLanguageManager-H4C36EVL.min.js} +2 -2
- package/dist/prod/{vidply.SoundCloudRenderer-CXRVZIAJ.min.js → vidply.SoundCloudRenderer-OYSWMW7D.min.js} +2 -2
- package/dist/prod/{vidply.TranscriptManager-SYM3DFUY.min.js → vidply.TranscriptManager-BA3DAS63.min.js} +2 -2
- package/dist/prod/{vidply.VimeoRenderer-72TWRYFQ.min.js → vidply.VimeoRenderer-NF3COTHU.min.js} +2 -2
- package/dist/prod/{vidply.YouTubeRenderer-UMHEV3KD.min.js → vidply.YouTubeRenderer-UCQKYIJ4.min.js} +2 -2
- package/dist/prod/{vidply.chunk-Z3YDSA7O.min.js → vidply.chunk-2HNCP64N.min.js} +1 -1
- package/dist/prod/{vidply.chunk-EKEYJWMG.min.js → vidply.chunk-6ECBIYVI.min.js} +2 -2
- package/dist/prod/{vidply.chunk-YEENJS3Q.min.js → vidply.chunk-7S5AFTST.min.js} +2 -2
- package/dist/prod/{vidply.chunk-P4UXOHWA.min.js → vidply.chunk-EUOA2DJK.min.js} +1 -1
- package/dist/prod/{vidply.chunk-2TB6MUHO.min.js → vidply.chunk-G6KGXNEW.min.js} +1 -1
- package/dist/prod/{vidply.chunk-ABPXVDLZ.min.js → vidply.chunk-JPMAK3NS.min.js} +1 -1
- package/dist/prod/{vidply.chunk-P3PQBY7N.min.js → vidply.chunk-L4O44K63.min.js} +1 -1
- package/dist/prod/{vidply.chunk-WX3MVEAL.min.js → vidply.chunk-M76ABMKB.min.js} +1 -1
- package/dist/prod/{vidply.chunk-TM6AXJVD.min.js → vidply.chunk-WRVDVD33.min.js} +2 -2
- package/dist/prod/{vidply.chunk-RI3X7WHK.min.js → vidply.chunk-X2FQEQYR.min.js} +2 -2
- package/dist/prod/{vidply.chunk-UK7YGOB5.min.js → vidply.chunk-XXUH7ADU.min.js} +2 -2
- package/dist/prod/{vidply.de-BCHXRQ53.min.js → vidply.de-JMCATTKZ.min.js} +1 -1
- package/dist/prod/{vidply.es-PICAKMZD.min.js → vidply.es-YVG37V7Z.min.js} +1 -1
- package/dist/prod/vidply.esm.min.js +2 -2
- package/dist/prod/{vidply.fr-ZILI7CDR.min.js → vidply.fr-OGKGN2R4.min.js} +1 -1
- package/dist/prod/{vidply.ja-5WQ4OK4J.min.js → vidply.ja-Z6DS7QLS.min.js} +1 -1
- package/dist/vidply.css +5796 -5796
- package/dist/vidply.esm.min.meta.json +148 -135
- package/package.json +7 -7
- package/src/controls/CaptionManager.ts +598 -598
- package/src/controls/ControlBar.ts +4159 -4159
- package/src/controls/KeyboardManager.ts +383 -383
- package/src/controls/SettingsDialog.ts +488 -488
- package/src/controls/TranscriptManager.ts +2426 -2426
- package/src/core/AudioDescriptionManager.ts +863 -774
- package/src/core/DescriptionSpeechManager.ts +232 -0
- package/src/core/FloatingPlayerManager.ts +674 -674
- package/src/core/LazyInit.ts +80 -80
- package/src/core/MetadataAlertsManager.ts +514 -514
- package/src/core/Player.ts +2915 -2872
- package/src/core/PosterManager.ts +169 -169
- package/src/core/PseudoFullscreen.ts +179 -179
- package/src/core/ResponsiveManager.ts +231 -231
- package/src/core/ResumeManager.ts +236 -236
- package/src/core/SignLanguageManager.ts +1308 -1308
- package/src/core/ThemeManager.ts +157 -157
- package/src/features/PlaylistManager.ts +1838 -1827
- package/src/i18n/i18n.ts +223 -223
- package/src/i18n/languages/de.ts +217 -217
- package/src/i18n/languages/en.ts +218 -218
- package/src/i18n/languages/es.ts +214 -214
- package/src/i18n/languages/fr.ts +214 -214
- package/src/i18n/languages/ja.ts +214 -214
- package/src/icons/Icons.ts +167 -167
- package/src/index.ts +196 -193
- package/src/renderers/DASHRenderer.ts +1077 -1077
- package/src/renderers/HLSRenderer.ts +944 -944
- package/src/renderers/HTML5Renderer.ts +389 -389
- package/src/renderers/SoundCloudRenderer.ts +437 -437
- package/src/renderers/VimeoRenderer.ts +298 -298
- package/src/renderers/YouTubeRenderer.ts +290 -290
- package/src/styles/vidply.css +5796 -5796
- package/src/types/events.ts +124 -122
- package/src/types/globals.d.ts +313 -313
- package/src/types/options.ts +185 -182
- package/src/types/renderer.ts +58 -58
- package/src/utils/DOMUtils.ts +281 -281
- package/src/utils/DownloadInfo.ts +171 -171
- package/src/utils/DraggablePanel.ts +539 -539
- package/src/utils/DraggablePanelMenu.ts +195 -195
- package/src/utils/DraggableResizable.ts +997 -997
- package/src/utils/EventEmitter.ts +59 -59
- package/src/utils/FormUtils.ts +95 -95
- package/src/utils/MenuFactory.ts +483 -483
- package/src/utils/Sanitize.ts +70 -70
- package/src/utils/SettingsMenuFactory.ts +289 -289
- package/src/utils/StorageManager.ts +287 -287
- package/src/utils/TrackLabelUtils.ts +52 -52
- package/src/utils/UrlSafe.ts +69 -69
- package/src/utils/VideoFrameCapture.ts +114 -114
- package/src/utils/WindowComponents.ts +250 -250
- package/dist/dev/vidply.AudioDescriptionManager-R47Q4YR4.js.map +0 -7
- package/dist/dev/vidply.DASHRenderer-PWEMC3BT.js.map +0 -7
- package/dist/dev/vidply.FloatingPlayerManager-PKNNNAS2.js.map +0 -7
- package/dist/dev/vidply.HLSRenderer-E7RQ74HI.js.map +0 -7
- package/dist/dev/vidply.HTML5Renderer-BU4QJJJY.js +0 -12
- package/dist/dev/vidply.SignLanguageManager-FET5FQZ7.js.map +0 -7
- package/dist/dev/vidply.SoundCloudRenderer-RVSJZODO.js.map +0 -7
- package/dist/dev/vidply.TranscriptManager-5GXOQCLX.js.map +0 -7
- package/dist/dev/vidply.VimeoRenderer-ER2LSBBW.js.map +0 -7
- package/dist/dev/vidply.YouTubeRenderer-NFTOWJAB.js.map +0 -7
- package/dist/dev/vidply.chunk-64WXMTGL.js.map +0 -7
- package/dist/dev/vidply.chunk-BNFIUL2Z.js.map +0 -7
- package/dist/dev/vidply.chunk-JJ47NO24.js.map +0 -7
- package/dist/dev/vidply.chunk-KI7C3QVL.js.map +0 -7
- package/dist/dev/vidply.chunk-NQPURMQY.js.map +0 -7
- package/dist/dev/vidply.chunk-RZWZGDV7.js.map +0 -7
- package/dist/dev/vidply.chunk-W25HUCHS.js.map +0 -7
- package/dist/dev/vidply.de-4YAT7FFM.js.map +0 -7
- package/dist/dev/vidply.es-LEWIRUBH.js.map +0 -7
- package/dist/dev/vidply.fr-Q4GV2NOE.js.map +0 -7
- package/dist/dev/vidply.ja-S6ITA6WU.js.map +0 -7
- package/dist/prod/vidply.AudioDescriptionManager-RHLIJW5C.min.js +0 -6
- package/dist/prod/vidply.HTML5Renderer-IU5T3WEW.min.js +0 -6
- /package/dist/dev/{vidply.CaptionStyleMenu-7XHRBWUM.js.map → vidply.CaptionStyleMenu-R2SCQ6VP.js.map} +0 -0
- /package/dist/dev/{vidply.HTML5Renderer-BU4QJJJY.js.map → vidply.HTML5Renderer-P7ICKYU4.js.map} +0 -0
- /package/dist/dev/{vidply.chunk-6RQA4ROD.js.map → vidply.chunk-5IK3IC5V.js.map} +0 -0
- /package/dist/dev/{vidply.chunk-DZSDO3B7.js.map → vidply.chunk-HOYEMJSD.js.map} +0 -0
- /package/dist/dev/{vidply.chunk-VMSQAZM7.js.map → vidply.chunk-JAKZNGOR.js.map} +0 -0
- /package/dist/dev/{vidply.chunk-SK4V2DAU.js.map → vidply.chunk-YVCYQZJI.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
|
+
|