vidply 1.0.4 → 1.0.6
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/LICENSE +22 -22
- package/README.md +593 -517
- package/dist/vidply.css +1807 -1807
- package/dist/vidply.esm.js +268 -182
- package/dist/vidply.esm.js.map +3 -3
- package/dist/vidply.esm.min.js +6 -6
- package/dist/vidply.esm.min.meta.json +40 -34
- package/dist/vidply.js +268 -182
- package/dist/vidply.js.map +3 -3
- package/dist/vidply.min.js +6 -6
- package/dist/vidply.min.meta.json +40 -34
- package/package.json +57 -57
- package/src/controls/CaptionManager.js +248 -248
- package/src/controls/ControlBar.js +4 -4
- package/src/controls/KeyboardManager.js +233 -233
- package/src/controls/SettingsDialog.js +417 -417
- package/src/controls/TranscriptManager.js +728 -728
- package/src/core/Player.js +1186 -1134
- package/src/i18n/i18n.js +66 -66
- package/src/i18n/translations.js +561 -511
- package/src/icons/Icons.js +183 -183
- package/src/index.js +95 -95
- package/src/renderers/HLSRenderer.js +302 -302
- package/src/renderers/HTML5Renderer.js +298 -298
- package/src/renderers/VimeoRenderer.js +257 -257
- package/src/renderers/YouTubeRenderer.js +274 -274
- package/src/styles/vidply.css +1807 -1807
- package/src/utils/DOMUtils.js +154 -154
- package/src/utils/EventEmitter.js +53 -53
- package/src/utils/TimeUtils.js +87 -82
|
@@ -1,298 +1,298 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HTML5 Media Renderer
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export class HTML5Renderer {
|
|
6
|
-
constructor(player) {
|
|
7
|
-
this.player = player;
|
|
8
|
-
this.media = player.element;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
async init() {
|
|
12
|
-
// Hide native controls
|
|
13
|
-
this.media.controls = false;
|
|
14
|
-
this.media.removeAttribute('controls');
|
|
15
|
-
|
|
16
|
-
this.attachEvents();
|
|
17
|
-
|
|
18
|
-
// Set preload
|
|
19
|
-
this.media.preload = this.player.options.preload;
|
|
20
|
-
|
|
21
|
-
// Load media
|
|
22
|
-
this.media.load();
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
attachEvents() {
|
|
26
|
-
// Playback events
|
|
27
|
-
this.media.addEventListener('loadedmetadata', () => {
|
|
28
|
-
this.player.state.duration = this.media.duration;
|
|
29
|
-
this.player.emit('loadedmetadata');
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
this.media.addEventListener('play', () => {
|
|
33
|
-
this.player.state.playing = true;
|
|
34
|
-
this.player.state.paused = false;
|
|
35
|
-
this.player.state.ended = false;
|
|
36
|
-
this.player.emit('play');
|
|
37
|
-
|
|
38
|
-
if (this.player.options.onPlay) {
|
|
39
|
-
this.player.options.onPlay.call(this.player);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Pause other players if enabled
|
|
43
|
-
if (this.player.options.pauseOthersOnPlay) {
|
|
44
|
-
this.pauseOtherPlayers();
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
this.media.addEventListener('pause', () => {
|
|
49
|
-
this.player.state.playing = false;
|
|
50
|
-
this.player.state.paused = true;
|
|
51
|
-
this.player.emit('pause');
|
|
52
|
-
|
|
53
|
-
if (this.player.options.onPause) {
|
|
54
|
-
this.player.options.onPause.call(this.player);
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
this.media.addEventListener('ended', () => {
|
|
59
|
-
this.player.state.playing = false;
|
|
60
|
-
this.player.state.paused = true;
|
|
61
|
-
this.player.state.ended = true;
|
|
62
|
-
this.player.emit('ended');
|
|
63
|
-
|
|
64
|
-
if (this.player.options.onEnded) {
|
|
65
|
-
this.player.options.onEnded.call(this.player);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Handle loop
|
|
69
|
-
if (this.player.options.loop) {
|
|
70
|
-
this.player.seek(0);
|
|
71
|
-
this.player.play();
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
this.media.addEventListener('timeupdate', () => {
|
|
76
|
-
this.player.state.currentTime = this.media.currentTime;
|
|
77
|
-
this.player.emit('timeupdate', this.media.currentTime);
|
|
78
|
-
|
|
79
|
-
if (this.player.options.onTimeUpdate) {
|
|
80
|
-
this.player.options.onTimeUpdate.call(this.player, this.media.currentTime);
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
this.media.addEventListener('volumechange', () => {
|
|
85
|
-
this.player.state.volume = this.media.volume;
|
|
86
|
-
this.player.state.muted = this.media.muted;
|
|
87
|
-
this.player.emit('volumechange', this.media.volume);
|
|
88
|
-
|
|
89
|
-
if (this.player.options.onVolumeChange) {
|
|
90
|
-
this.player.options.onVolumeChange.call(this.player, this.media.volume);
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
this.media.addEventListener('seeking', () => {
|
|
95
|
-
this.player.state.seeking = true;
|
|
96
|
-
this.player.emit('seeking');
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
this.media.addEventListener('seeked', () => {
|
|
100
|
-
this.player.state.seeking = false;
|
|
101
|
-
this.player.emit('seeked');
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
this.media.addEventListener('waiting', () => {
|
|
105
|
-
this.player.state.buffering = true;
|
|
106
|
-
this.player.emit('waiting');
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
this.media.addEventListener('canplay', () => {
|
|
110
|
-
this.player.state.buffering = false;
|
|
111
|
-
this.player.emit('canplay');
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
this.media.addEventListener('progress', () => {
|
|
115
|
-
if (this.media.buffered.length > 0) {
|
|
116
|
-
const buffered = this.media.buffered.end(this.media.buffered.length - 1);
|
|
117
|
-
this.player.emit('progress', buffered);
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
this.media.addEventListener('error', (e) => {
|
|
122
|
-
this.player.handleError(this.media.error);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
this.media.addEventListener('ratechange', () => {
|
|
126
|
-
this.player.state.playbackSpeed = this.media.playbackRate;
|
|
127
|
-
this.player.emit('ratechange', this.media.playbackRate);
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
pauseOtherPlayers() {
|
|
132
|
-
// Pause other VidPly instances
|
|
133
|
-
const allPlayers = document.querySelectorAll('.vidply-player');
|
|
134
|
-
allPlayers.forEach(playerEl => {
|
|
135
|
-
if (playerEl !== this.player.container) {
|
|
136
|
-
const video = playerEl.querySelector('video, audio');
|
|
137
|
-
if (video && !video.paused) {
|
|
138
|
-
video.pause();
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
play() {
|
|
145
|
-
const promise = this.media.play();
|
|
146
|
-
|
|
147
|
-
if (promise !== undefined) {
|
|
148
|
-
promise.catch(error => {
|
|
149
|
-
this.player.log('Play failed:', error, 'warn');
|
|
150
|
-
|
|
151
|
-
// If autoplay failed, try muted autoplay
|
|
152
|
-
if (this.player.options.autoplay && !this.player.state.muted) {
|
|
153
|
-
this.player.log('Retrying play with muted audio', 'info');
|
|
154
|
-
this.media.muted = true;
|
|
155
|
-
this.media.play().catch(err => {
|
|
156
|
-
this.player.handleError(err);
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
pause() {
|
|
164
|
-
this.media.pause();
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
seek(time) {
|
|
168
|
-
this.media.currentTime = time;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
setVolume(volume) {
|
|
172
|
-
this.media.volume = volume;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
setMuted(muted) {
|
|
176
|
-
this.media.muted = muted;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
setPlaybackSpeed(speed) {
|
|
180
|
-
this.media.playbackRate = speed;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Get available quality levels from source elements
|
|
185
|
-
* @returns {Array} Array of quality objects with index, height, width, and src
|
|
186
|
-
*/
|
|
187
|
-
getQualities() {
|
|
188
|
-
const sources = Array.from(this.media.querySelectorAll('source'));
|
|
189
|
-
|
|
190
|
-
if (sources.length <= 1) {
|
|
191
|
-
return [];
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return sources.map((source, index) => {
|
|
195
|
-
// Try to extract quality from data attributes or label
|
|
196
|
-
const label = source.getAttribute('data-quality') || source.getAttribute('label') || '';
|
|
197
|
-
const height = source.getAttribute('data-height') || this.extractHeightFromLabel(label);
|
|
198
|
-
const width = source.getAttribute('data-width') || '';
|
|
199
|
-
|
|
200
|
-
return {
|
|
201
|
-
index,
|
|
202
|
-
height: height ? parseInt(height) : 0,
|
|
203
|
-
width: width ? parseInt(width) : 0,
|
|
204
|
-
src: source.src,
|
|
205
|
-
type: source.type,
|
|
206
|
-
name: label || (height ? `${height}p` : `Quality ${index + 1}`)
|
|
207
|
-
};
|
|
208
|
-
}).filter(q => q.height > 0); // Only return qualities with valid height
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Extract height from quality label (e.g., "1080p" -> 1080)
|
|
213
|
-
* @param {string} label
|
|
214
|
-
* @returns {number}
|
|
215
|
-
*/
|
|
216
|
-
extractHeightFromLabel(label) {
|
|
217
|
-
const match = label.match(/(\d+)p/i);
|
|
218
|
-
return match ? parseInt(match[1]) : 0;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Switch to a specific quality level
|
|
223
|
-
* @param {number} qualityIndex - Index of the quality level (-1 for auto, not applicable for HTML5)
|
|
224
|
-
*/
|
|
225
|
-
switchQuality(qualityIndex) {
|
|
226
|
-
const qualities = this.getQualities();
|
|
227
|
-
|
|
228
|
-
if (qualityIndex < 0 || qualityIndex >= qualities.length) {
|
|
229
|
-
this.player.log('Invalid quality index', 'warn');
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const quality = qualities[qualityIndex];
|
|
234
|
-
const currentTime = this.media.currentTime;
|
|
235
|
-
const wasPlaying = !this.media.paused;
|
|
236
|
-
|
|
237
|
-
// Store the current source for comparison
|
|
238
|
-
const currentSrc = this.media.currentSrc;
|
|
239
|
-
|
|
240
|
-
// Don't switch if already at this quality
|
|
241
|
-
if (currentSrc === quality.src) {
|
|
242
|
-
this.player.log('Already at this quality level', 'info');
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
this.player.log(`Switching to quality: ${quality.name}`, 'info');
|
|
247
|
-
|
|
248
|
-
// Update the src
|
|
249
|
-
this.media.src = quality.src;
|
|
250
|
-
|
|
251
|
-
// Wait for the new source to load, then restore playback state
|
|
252
|
-
const onLoadedMetadata = () => {
|
|
253
|
-
this.media.removeEventListener('loadedmetadata', onLoadedMetadata);
|
|
254
|
-
|
|
255
|
-
// Restore playback position
|
|
256
|
-
this.media.currentTime = currentTime;
|
|
257
|
-
|
|
258
|
-
// Resume playback if it was playing
|
|
259
|
-
if (wasPlaying) {
|
|
260
|
-
this.media.play().catch(err => {
|
|
261
|
-
this.player.log('Failed to resume playback after quality switch', 'warn');
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Emit quality change event
|
|
266
|
-
this.player.emit('qualitychange', { quality: quality.name, index: qualityIndex });
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
this.media.addEventListener('loadedmetadata', onLoadedMetadata);
|
|
270
|
-
this.media.load();
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* Get current quality index
|
|
275
|
-
* @returns {number}
|
|
276
|
-
*/
|
|
277
|
-
getCurrentQuality() {
|
|
278
|
-
const qualities = this.getQualities();
|
|
279
|
-
const currentSrc = this.media.currentSrc;
|
|
280
|
-
|
|
281
|
-
for (let i = 0; i < qualities.length; i++) {
|
|
282
|
-
if (qualities[i].src === currentSrc) {
|
|
283
|
-
return i;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
return 0; // Default to first quality if not found
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
destroy() {
|
|
291
|
-
// Remove event listeners
|
|
292
|
-
this.media.removeEventListener('loadedmetadata', () => {});
|
|
293
|
-
this.media.removeEventListener('play', () => {});
|
|
294
|
-
this.media.removeEventListener('pause', () => {});
|
|
295
|
-
// ... (other listeners would be removed in a real implementation)
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
1
|
+
/**
|
|
2
|
+
* HTML5 Media Renderer
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export class HTML5Renderer {
|
|
6
|
+
constructor(player) {
|
|
7
|
+
this.player = player;
|
|
8
|
+
this.media = player.element;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async init() {
|
|
12
|
+
// Hide native controls
|
|
13
|
+
this.media.controls = false;
|
|
14
|
+
this.media.removeAttribute('controls');
|
|
15
|
+
|
|
16
|
+
this.attachEvents();
|
|
17
|
+
|
|
18
|
+
// Set preload
|
|
19
|
+
this.media.preload = this.player.options.preload;
|
|
20
|
+
|
|
21
|
+
// Load media
|
|
22
|
+
this.media.load();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
attachEvents() {
|
|
26
|
+
// Playback events
|
|
27
|
+
this.media.addEventListener('loadedmetadata', () => {
|
|
28
|
+
this.player.state.duration = this.media.duration;
|
|
29
|
+
this.player.emit('loadedmetadata');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
this.media.addEventListener('play', () => {
|
|
33
|
+
this.player.state.playing = true;
|
|
34
|
+
this.player.state.paused = false;
|
|
35
|
+
this.player.state.ended = false;
|
|
36
|
+
this.player.emit('play');
|
|
37
|
+
|
|
38
|
+
if (this.player.options.onPlay) {
|
|
39
|
+
this.player.options.onPlay.call(this.player);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Pause other players if enabled
|
|
43
|
+
if (this.player.options.pauseOthersOnPlay) {
|
|
44
|
+
this.pauseOtherPlayers();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
this.media.addEventListener('pause', () => {
|
|
49
|
+
this.player.state.playing = false;
|
|
50
|
+
this.player.state.paused = true;
|
|
51
|
+
this.player.emit('pause');
|
|
52
|
+
|
|
53
|
+
if (this.player.options.onPause) {
|
|
54
|
+
this.player.options.onPause.call(this.player);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
this.media.addEventListener('ended', () => {
|
|
59
|
+
this.player.state.playing = false;
|
|
60
|
+
this.player.state.paused = true;
|
|
61
|
+
this.player.state.ended = true;
|
|
62
|
+
this.player.emit('ended');
|
|
63
|
+
|
|
64
|
+
if (this.player.options.onEnded) {
|
|
65
|
+
this.player.options.onEnded.call(this.player);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Handle loop
|
|
69
|
+
if (this.player.options.loop) {
|
|
70
|
+
this.player.seek(0);
|
|
71
|
+
this.player.play();
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
this.media.addEventListener('timeupdate', () => {
|
|
76
|
+
this.player.state.currentTime = this.media.currentTime;
|
|
77
|
+
this.player.emit('timeupdate', this.media.currentTime);
|
|
78
|
+
|
|
79
|
+
if (this.player.options.onTimeUpdate) {
|
|
80
|
+
this.player.options.onTimeUpdate.call(this.player, this.media.currentTime);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
this.media.addEventListener('volumechange', () => {
|
|
85
|
+
this.player.state.volume = this.media.volume;
|
|
86
|
+
this.player.state.muted = this.media.muted;
|
|
87
|
+
this.player.emit('volumechange', this.media.volume);
|
|
88
|
+
|
|
89
|
+
if (this.player.options.onVolumeChange) {
|
|
90
|
+
this.player.options.onVolumeChange.call(this.player, this.media.volume);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
this.media.addEventListener('seeking', () => {
|
|
95
|
+
this.player.state.seeking = true;
|
|
96
|
+
this.player.emit('seeking');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
this.media.addEventListener('seeked', () => {
|
|
100
|
+
this.player.state.seeking = false;
|
|
101
|
+
this.player.emit('seeked');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
this.media.addEventListener('waiting', () => {
|
|
105
|
+
this.player.state.buffering = true;
|
|
106
|
+
this.player.emit('waiting');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
this.media.addEventListener('canplay', () => {
|
|
110
|
+
this.player.state.buffering = false;
|
|
111
|
+
this.player.emit('canplay');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
this.media.addEventListener('progress', () => {
|
|
115
|
+
if (this.media.buffered.length > 0) {
|
|
116
|
+
const buffered = this.media.buffered.end(this.media.buffered.length - 1);
|
|
117
|
+
this.player.emit('progress', buffered);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
this.media.addEventListener('error', (e) => {
|
|
122
|
+
this.player.handleError(this.media.error);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
this.media.addEventListener('ratechange', () => {
|
|
126
|
+
this.player.state.playbackSpeed = this.media.playbackRate;
|
|
127
|
+
this.player.emit('ratechange', this.media.playbackRate);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
pauseOtherPlayers() {
|
|
132
|
+
// Pause other VidPly instances
|
|
133
|
+
const allPlayers = document.querySelectorAll('.vidply-player');
|
|
134
|
+
allPlayers.forEach(playerEl => {
|
|
135
|
+
if (playerEl !== this.player.container) {
|
|
136
|
+
const video = playerEl.querySelector('video, audio');
|
|
137
|
+
if (video && !video.paused) {
|
|
138
|
+
video.pause();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
play() {
|
|
145
|
+
const promise = this.media.play();
|
|
146
|
+
|
|
147
|
+
if (promise !== undefined) {
|
|
148
|
+
promise.catch(error => {
|
|
149
|
+
this.player.log('Play failed:', error, 'warn');
|
|
150
|
+
|
|
151
|
+
// If autoplay failed, try muted autoplay
|
|
152
|
+
if (this.player.options.autoplay && !this.player.state.muted) {
|
|
153
|
+
this.player.log('Retrying play with muted audio', 'info');
|
|
154
|
+
this.media.muted = true;
|
|
155
|
+
this.media.play().catch(err => {
|
|
156
|
+
this.player.handleError(err);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
pause() {
|
|
164
|
+
this.media.pause();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
seek(time) {
|
|
168
|
+
this.media.currentTime = time;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
setVolume(volume) {
|
|
172
|
+
this.media.volume = volume;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
setMuted(muted) {
|
|
176
|
+
this.media.muted = muted;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
setPlaybackSpeed(speed) {
|
|
180
|
+
this.media.playbackRate = speed;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get available quality levels from source elements
|
|
185
|
+
* @returns {Array} Array of quality objects with index, height, width, and src
|
|
186
|
+
*/
|
|
187
|
+
getQualities() {
|
|
188
|
+
const sources = Array.from(this.media.querySelectorAll('source'));
|
|
189
|
+
|
|
190
|
+
if (sources.length <= 1) {
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return sources.map((source, index) => {
|
|
195
|
+
// Try to extract quality from data attributes or label
|
|
196
|
+
const label = source.getAttribute('data-quality') || source.getAttribute('label') || '';
|
|
197
|
+
const height = source.getAttribute('data-height') || this.extractHeightFromLabel(label);
|
|
198
|
+
const width = source.getAttribute('data-width') || '';
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
index,
|
|
202
|
+
height: height ? parseInt(height) : 0,
|
|
203
|
+
width: width ? parseInt(width) : 0,
|
|
204
|
+
src: source.src,
|
|
205
|
+
type: source.type,
|
|
206
|
+
name: label || (height ? `${height}p` : `Quality ${index + 1}`)
|
|
207
|
+
};
|
|
208
|
+
}).filter(q => q.height > 0); // Only return qualities with valid height
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Extract height from quality label (e.g., "1080p" -> 1080)
|
|
213
|
+
* @param {string} label
|
|
214
|
+
* @returns {number}
|
|
215
|
+
*/
|
|
216
|
+
extractHeightFromLabel(label) {
|
|
217
|
+
const match = label.match(/(\d+)p/i);
|
|
218
|
+
return match ? parseInt(match[1]) : 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Switch to a specific quality level
|
|
223
|
+
* @param {number} qualityIndex - Index of the quality level (-1 for auto, not applicable for HTML5)
|
|
224
|
+
*/
|
|
225
|
+
switchQuality(qualityIndex) {
|
|
226
|
+
const qualities = this.getQualities();
|
|
227
|
+
|
|
228
|
+
if (qualityIndex < 0 || qualityIndex >= qualities.length) {
|
|
229
|
+
this.player.log('Invalid quality index', 'warn');
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const quality = qualities[qualityIndex];
|
|
234
|
+
const currentTime = this.media.currentTime;
|
|
235
|
+
const wasPlaying = !this.media.paused;
|
|
236
|
+
|
|
237
|
+
// Store the current source for comparison
|
|
238
|
+
const currentSrc = this.media.currentSrc;
|
|
239
|
+
|
|
240
|
+
// Don't switch if already at this quality
|
|
241
|
+
if (currentSrc === quality.src) {
|
|
242
|
+
this.player.log('Already at this quality level', 'info');
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
this.player.log(`Switching to quality: ${quality.name}`, 'info');
|
|
247
|
+
|
|
248
|
+
// Update the src
|
|
249
|
+
this.media.src = quality.src;
|
|
250
|
+
|
|
251
|
+
// Wait for the new source to load, then restore playback state
|
|
252
|
+
const onLoadedMetadata = () => {
|
|
253
|
+
this.media.removeEventListener('loadedmetadata', onLoadedMetadata);
|
|
254
|
+
|
|
255
|
+
// Restore playback position
|
|
256
|
+
this.media.currentTime = currentTime;
|
|
257
|
+
|
|
258
|
+
// Resume playback if it was playing
|
|
259
|
+
if (wasPlaying) {
|
|
260
|
+
this.media.play().catch(err => {
|
|
261
|
+
this.player.log('Failed to resume playback after quality switch', 'warn');
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Emit quality change event
|
|
266
|
+
this.player.emit('qualitychange', { quality: quality.name, index: qualityIndex });
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
this.media.addEventListener('loadedmetadata', onLoadedMetadata);
|
|
270
|
+
this.media.load();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Get current quality index
|
|
275
|
+
* @returns {number}
|
|
276
|
+
*/
|
|
277
|
+
getCurrentQuality() {
|
|
278
|
+
const qualities = this.getQualities();
|
|
279
|
+
const currentSrc = this.media.currentSrc;
|
|
280
|
+
|
|
281
|
+
for (let i = 0; i < qualities.length; i++) {
|
|
282
|
+
if (qualities[i].src === currentSrc) {
|
|
283
|
+
return i;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return 0; // Default to first quality if not found
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
destroy() {
|
|
291
|
+
// Remove event listeners
|
|
292
|
+
this.media.removeEventListener('loadedmetadata', () => {});
|
|
293
|
+
this.media.removeEventListener('play', () => {});
|
|
294
|
+
this.media.removeEventListener('pause', () => {});
|
|
295
|
+
// ... (other listeners would be removed in a real implementation)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|