Subham9126 commited on
Commit
77d1f67
·
verified ·
1 Parent(s): 9b8a30b

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +721 -906
index.html CHANGED
@@ -3,102 +3,297 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Video Library - Optimized Streaming</title>
7
- <!-- Load Plyr.io -->
8
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/plyr/3.7.8/plyr.min.css" />
9
-
10
  <style>
11
- /* Basic Reset & Font */
12
- * { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
13
- body { background-color: #f5f5f5; padding: 20px; max-width: 1200px; margin: 0 auto; }
14
-
15
- /* Layout & Structure */
16
- .container { display: flex; flex-direction: column; gap: 20px; }
17
- .header { background-color: #333; color: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
18
- .search-container { padding: 15px; background-color: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
19
- #search { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 16px; }
20
- .video-list { background-color: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); padding: 15px; }
21
- .video-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 15px; }
22
- .no-videos { padding: 20px; text-align: center; color: #777; }
23
- .pagination { display: flex; justify-content: center; gap: 5px; margin-top: 20px; }
24
- .page-btn { padding: 8px 12px; background-color: white; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; transition: background-color 0.2s; }
25
- .page-btn:hover { background-color: #f0f0f0; }
26
- .page-btn.active { background-color: #333; color: white; border-color: #333; }
27
- .page-btn:disabled { cursor: not-allowed; opacity: 0.6; }
28
-
29
- /* Video Item */
30
- .video-item { padding: 15px; border: 1px solid #eee; border-radius: 8px; cursor: pointer; transition: all 0.2s; background-color: white; display: flex; flex-direction: column; }
31
- .video-item:hover { transform: translateY(-5px); box-shadow: 0 5px 15px rgba(0,0,0,0.1); }
32
- .video-thumbnail { width: 100%; height: 156px; background-color: #e0e0e0; border-radius: 4px; display: flex; align-items: center; justify-content: center; margin-bottom: 10px; overflow: hidden; position: relative; }
33
- .video-thumbnail img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.3s ease; }
34
- .video-item:hover .video-thumbnail img { transform: scale(1.05); }
35
- .video-thumbnail::after { content: ""; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: linear-gradient(to top, rgba(0,0,0,0.3), transparent); opacity: 0; transition: opacity 0.3s; }
36
- .video-item:hover .video-thumbnail::after { opacity: 1; }
37
- .play-icon { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 2; font-size: 45px; color: white; opacity: 0; transition: opacity 0.3s, transform 0.3s; text-shadow: 0 2px 4px rgba(0,0,0,0.5); }
38
- .video-item:hover .play-icon { opacity: 0.9; transform: translate(-50%, -50%) scale(1.1); }
39
- .video-item-title { font-weight: 500; margin-top: auto; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 0.95em; }
40
-
41
- /* Popup Player */
42
- .popup-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.9); display: flex; align-items: center; justify-content: center; z-index: 1000; opacity: 0; visibility: hidden; transition: opacity 0.3s ease, visibility 0s linear 0.3s; }
43
- .popup-overlay.active { opacity: 1; visibility: visible; transition: opacity 0.3s ease, visibility 0s linear 0s; }
44
- .popup-content { width: 90%; max-width: 1100px; background-color: #111; border-radius: 8px; overflow: hidden; position: relative; box-shadow: 0 10px 25px rgba(0,0,0,0.5); display: flex; flex-direction: column; max-height: 90vh; }
45
- .close-popup { position: absolute; top: 12px; right: 12px; background-color: rgba(30,30,30,0.7); color: white; border: none; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 1010; font-size: 18px; transition: background-color 0.2s; line-height: 1; }
46
- .close-popup:hover { background-color: rgba(200,0,0,0.8); }
47
- .popup-header { background-color: #1a1a1a; padding: 12px 15px 12px 15px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #333; }
48
- .popup-title { color: white; font-size: 1.1em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-right: 50px; }
49
- .quality-selector { display: flex; align-items: center; gap: 8px; color: #ccc; font-size: 0.9em; }
50
- .quality-selector select { padding: 4px 8px; background-color: #333; color: white; border: 1px solid #555; border-radius: 4px; font-size: 0.9em; }
51
- .player-wrapper { position: relative; width: 100%; /*padding-top: 56.25%; 16:9 */ background-color: #000; flex-grow: 1; display: flex; align-items: center; justify-content: center; }
52
- /* Make Plyr fill the wrapper */
53
- .plyr { position: absolute; top: 0; left: 0; width: 100% !important; height: 100% !important; }
54
- #playerContainer { width: 100%; height: 100%; display:flex; align-items:center; justify-content:center; }
55
- #playerContainer video { max-width:100%; max-height:100%; object-fit: contain; }
56
-
57
- /* Loading & Debug */
58
- .player-loading { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; color: white; font-size: 1.2em; z-index: 5; flex-direction: column; gap: 10px; pointer-events: none; }
59
- .player-loading .spinner { width: 40px; height: 40px; border: 4px solid rgba(255,255,255,0.3); border-radius: 50%; border-top-color: white; animation: spin 1s ease-in-out infinite; }
60
- @keyframes spin { to { transform: rotate(360deg); } }
61
- .debug-controls { position: absolute; bottom: 70px; left: 10px; z-index: 1005; }
62
- .debug-toggle { background-color: rgba(0,0,0,0.6); color: white; border: 1px solid #555; padding: 5px 10px; border-radius: 4px; cursor: pointer; font-size: 11px; transition: background-color 0.2s; }
63
- .debug-toggle:hover { background-color: rgba(50,50,50,0.8); }
64
- .debug-overlay { position: absolute; bottom: 100px; left: 10px; background-color: rgba(0,0,0,0.75); color: #e0e0e0; padding: 10px; border-radius: 4px; font-size: 11px; max-width: 300px; z-index: 1005; display: none; line-height: 1.5; max-height: 200px; overflow-y: auto; white-space: pre-wrap; }
65
-
66
- /* Responsive */
67
- @media (max-width: 768px) {
68
- .video-grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); }
69
- .popup-content { width: 98%; max-height: 95vh; }
70
- .popup-header { flex-direction: column; align-items: flex-start; gap: 8px; padding-right: 50px; /* Space for close btn */ }
71
- .popup-title { font-size: 1em; }
72
- .quality-selector { font-size: 0.8em; }
73
- .quality-selector select { font-size: 0.8em; }
74
- .debug-overlay { font-size: 10px; max-width: 200px; }
75
- .debug-toggle { font-size: 10px; padding: 4px 8px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  }
77
- @media (max-width: 480px) {
78
- .video-grid { grid-template-columns: 1fr; gap: 10px; } /* Single column */
79
- .video-item { flex-direction: row; gap: 10px; align-items: center; padding: 10px; }
80
- .video-thumbnail { width: 120px; height: 67px; margin-bottom: 0; flex-shrink: 0; }
81
- .video-item-title { white-space: normal; text-overflow: clip; overflow: visible; font-size: 0.9em; }
82
- .play-icon { font-size: 30px; }
83
- body { padding: 10px; }
84
- .container { gap: 15px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  }
86
  </style>
87
  </head>
88
  <body>
89
  <div class="container">
90
  <div class="header">
91
- <h1>Video Library</h1>
92
  </div>
93
-
94
  <div class="search-container">
95
  <input type="text" id="search" placeholder="Search videos...">
96
  </div>
97
-
98
  <div class="video-list">
99
  <div class="video-grid" id="videoGrid">
100
- <!-- Video items will be loaded here -->
101
- <div class="video-thumbnail">Loading videos...</div>
 
102
  </div>
103
  <div class="pagination" id="pagination"></div>
104
  </div>
@@ -107,954 +302,574 @@
107
  <!-- Video Player Popup -->
108
  <div class="popup-overlay" id="playerPopup">
109
  <div class="popup-content">
110
- <button class="close-popup" id="closePopup" title="Close Player (Esc)">✕</button>
111
- <div class="popup-header">
112
- <div class="popup-title" id="currentTitle">Video Title</div>
113
- <div class="quality-selector">
114
- <label for="qualitySelector">Quality:</label>
115
- <select id="qualitySelector">
116
- <option value="auto">Auto</option>
117
- <!-- Quality options added dynamically -->
118
- </select>
119
- </div>
120
- </div>
121
  <div class="player-wrapper">
122
- <div id="playerContainer">
123
- <!-- Video element will be created here -->
124
- </div>
125
- <div class="player-loading" id="playerLoading">
126
- <div class="spinner"></div>
127
- <span>Loading...</span>
128
- </div>
129
- <div class="debug-controls">
130
- <button class="debug-toggle" id="debugToggle">Stats</button>
131
- </div>
132
- <div class="debug-overlay" id="debugOverlay">
133
- Loading metrics...
134
- </div>
 
 
135
  </div>
136
  </div>
137
  </div>
138
 
139
- <!-- Load required scripts -->
140
- <script src="https://cdn.plyr.io/3.7.8/plyr.js"></script>
141
- <script src="https://cdn.dashjs.org/latest/dash.all.min.js"></script> <!-- DASH -->
142
- <script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script> <!-- HLS -->
143
-
144
  <script>
145
- // --- Global Variables ---
146
  let videoData = [];
147
  let filteredVideos = [];
148
  let currentVideo = null;
149
  let currentPage = 1;
150
- const videosPerPage = 12; // Adjusted for better grid layout
151
- let player = null; // Plyr instance
152
- let hls = null; // HLS.js instance
153
- let dash = null; // Dash.js instance
154
- let debugInterval = null;
155
- let isDebugVisible = false;
156
-
157
- // --- DOM Elements ---
158
  const videoGrid = document.getElementById('videoGrid');
159
  const pagination = document.getElementById('pagination');
160
  const searchInput = document.getElementById('search');
161
  const playerPopup = document.getElementById('playerPopup');
 
162
  const currentTitle = document.getElementById('currentTitle');
163
  const closePopup = document.getElementById('closePopup');
164
- const playerContainer = document.getElementById('playerContainer');
165
- const playerLoading = document.getElementById('playerLoading');
166
- const qualitySelector = document.getElementById('qualitySelector');
167
- const debugOverlay = document.getElementById('debugOverlay');
168
- const debugToggle = document.getElementById('debugToggle');
169
-
170
- // --- Core Functions ---
171
-
172
- // Fetch video data from JSON
 
173
  async function fetchVideoData() {
174
- videoGrid.innerHTML = '<div class="video-thumbnail">Loading video index...</div>';
175
  try {
176
- // Add cache busting query param to avoid stale data
177
- const response = await fetch(`database.json?v=${Date.now()}`);
178
  if (!response.ok) {
179
- throw new Error(`HTTP error! status: ${response.status}`);
180
  }
181
  const data = await response.json();
182
-
183
- // Ensure data format is correct
184
- videoData = Array.isArray(data) ? data :
185
- (data && Array.isArray(data.videos) ? data.videos : []);
186
-
187
- if (!videoData.length) {
188
- console.warn("No videos found in database.json or format is incorrect.");
189
- videoGrid.innerHTML = '<div class="no-videos">No videos found. Check database.json.</div>';
190
- return;
191
- }
192
-
193
- // Basic validation of video objects
194
- videoData = videoData.filter(v => v && v.title && v.url);
195
-
196
- // --- IMPORTANT: Add Thumbnail URLs ---
197
- // You NEED to add 'thumbnailUrl' to your database.json entries
198
- // Example: { "title": "My Video", "url": "path/to/manifest.m3u8", "thumbnailUrl": "path/to/thumbnail.jpg" }
199
  videoData.forEach(video => {
200
- if (!video.thumbnailUrl) {
201
- // Fallback placeholder - REPLACE THIS IN PRODUCTION
202
- video.thumbnailUrl = `https://via.placeholder.com/280x156/CCCCCC/000000?text=${encodeURIComponent(video.title.substring(0, 15))}`;
 
 
 
 
 
203
  }
204
  });
205
-
206
  filteredVideos = [...videoData];
207
- currentPage = 1; // Reset page on data load/refresh
208
  renderPagination();
209
  renderVideoGrid();
210
- preloadThumbnailsForPage(currentPage);
211
-
212
  } catch (error) {
213
- console.error('Error fetching or processing video data:', error);
214
  videoGrid.innerHTML = `<div class="no-videos">Error loading videos: ${error.message}</div>`;
215
  }
216
  }
217
 
218
- // Preload thumbnails for the visible page
219
- function preloadThumbnailsForPage(page) {
220
- const startIndex = (page - 1) * videosPerPage;
221
- const endIndex = Math.min(startIndex + videosPerPage, filteredVideos.length);
222
- const videosOnPage = filteredVideos.slice(startIndex, endIndex);
223
-
224
- videosOnPage.forEach(video => {
225
- if (video.thumbnailUrl) {
226
- const img = new Image();
227
- img.src = video.thumbnailUrl;
228
- }
229
- });
230
- }
231
-
232
- // Render video grid for the current page
233
  function renderVideoGrid() {
234
- videoGrid.innerHTML = ''; // Clear previous items
235
  if (filteredVideos.length === 0) {
236
- videoGrid.innerHTML = '<div class="no-videos">No videos match your search.</div>';
 
237
  return;
238
  }
239
 
 
240
  const startIndex = (currentPage - 1) * videosPerPage;
241
  const endIndex = Math.min(startIndex + videosPerPage, filteredVideos.length);
 
242
 
243
- if (startIndex >= filteredVideos.length) {
244
- videoGrid.innerHTML = '<div class="no-videos">No videos on this page.</div>';
245
- return;
246
- }
247
-
248
- const fragment = document.createDocumentFragment();
249
- for (let i = startIndex; i < endIndex; i++) {
250
- const video = filteredVideos[i];
251
- const videoItem = document.createElement('div');
252
- videoItem.className = 'video-item';
253
- videoItem.dataset.index = i; // Store the index in the filtered list
254
- videoItem.innerHTML = `
255
  <div class="video-thumbnail">
256
- <img src="${video.thumbnailUrl || ''}" alt="${video.title}" loading="lazy" onerror="this.style.display='none'; this.parentElement.style.backgroundColor='#ccc';">
257
  <div class="play-icon">▶</div>
258
  </div>
259
  <div class="video-item-title">${video.title}</div>
260
- `;
261
- videoItem.addEventListener('click', () => {
262
- const index = parseInt(videoItem.dataset.index);
263
- if (!isNaN(index) && index >= 0 && index < filteredVideos.length) {
264
- playVideo(filteredVideos[index]);
265
- } else {
266
- console.error("Invalid video index clicked:", videoItem.dataset.index);
267
- }
 
 
268
  });
269
- fragment.appendChild(videoItem);
270
- }
271
- videoGrid.appendChild(fragment);
272
  }
273
 
274
- // Render pagination controls
275
  function renderPagination() {
276
- pagination.innerHTML = ''; // Clear previous pagination
277
  const totalPages = Math.ceil(filteredVideos.length / videosPerPage);
 
 
 
 
 
278
 
279
- if (totalPages <= 1) return; // No pagination needed for 1 or 0 pages
280
-
281
- const maxVisibleButtons = 7; // Max number of page buttons to show
282
- let startPage, endPage;
283
-
284
- if (totalPages <= maxVisibleButtons) {
285
- startPage = 1;
286
- endPage = totalPages;
287
  } else {
288
- const maxPagesBeforeCurrent = Math.floor((maxVisibleButtons - 3) / 2);
289
- const maxPagesAfterCurrent = Math.ceil((maxVisibleButtons - 3) / 2);
290
-
291
- if (currentPage <= maxPagesBeforeCurrent + 1) {
292
- startPage = 1;
293
- endPage = maxVisibleButtons - 1;
294
- } else if (currentPage + maxPagesAfterCurrent >= totalPages) {
295
- startPage = totalPages - (maxVisibleButtons - 2);
296
- endPage = totalPages;
297
- } else {
298
- startPage = currentPage - maxPagesBeforeCurrent;
299
- endPage = currentPage + maxPagesAfterCurrent;
300
  }
301
- }
302
-
303
- const fragment = document.createDocumentFragment();
304
-
305
- // Previous Button
306
- const prevBtn = document.createElement('button');
307
- prevBtn.className = 'page-btn';
308
- prevBtn.textContent = '‹';
309
- prevBtn.disabled = currentPage === 1;
310
- prevBtn.addEventListener('click', () => {
311
- if (currentPage > 1) {
312
- currentPage--;
313
- renderPagination();
314
- renderVideoGrid();
315
- preloadThumbnailsForPage(currentPage);
316
- window.scrollTo(0, videoGrid.offsetTop - 20);
317
  }
318
- });
319
- fragment.appendChild(prevBtn);
320
-
321
- // Page number buttons
322
- if (startPage > 1) {
323
- fragment.appendChild(createPageButton(1));
324
- if (startPage > 2) {
325
- const ellipsis = document.createElement('span');
326
- ellipsis.className = 'page-btn';
327
- ellipsis.textContent = '...';
328
- ellipsis.style.cursor = 'default';
329
- fragment.appendChild(ellipsis);
330
  }
 
 
 
331
  }
332
-
333
- for (let i = startPage; i <= endPage; i++) {
334
- fragment.appendChild(createPageButton(i));
335
- }
336
-
337
- if (endPage < totalPages) {
338
- if (endPage < totalPages - 1) {
339
- const ellipsis = document.createElement('span');
340
- ellipsis.className = 'page-btn';
341
- ellipsis.textContent = '...';
342
- ellipsis.style.cursor = 'default';
343
- fragment.appendChild(ellipsis);
344
  }
345
- fragment.appendChild(createPageButton(totalPages));
346
- }
347
-
348
- // Next Button
349
- const nextBtn = document.createElement('button');
350
- nextBtn.className = 'page-btn';
351
- nextBtn.textContent = '';
352
- nextBtn.disabled = currentPage === totalPages;
353
- nextBtn.addEventListener('click', () => {
354
- if (currentPage < totalPages) {
355
- currentPage++;
356
  renderPagination();
357
  renderVideoGrid();
358
- preloadThumbnailsForPage(currentPage);
359
- window.scrollTo(0, videoGrid.offsetTop - 20);
360
- }
361
- });
362
- fragment.appendChild(nextBtn);
363
-
364
- pagination.appendChild(fragment);
365
- }
366
-
367
- // Helper to create a single pagination button
368
- function createPageButton(pageNumber) {
369
- const button = document.createElement('button');
370
- button.className = 'page-btn';
371
- button.textContent = pageNumber;
372
- button.dataset.page = pageNumber;
373
- if (pageNumber === currentPage) {
374
- button.classList.add('active');
375
- }
376
- button.addEventListener('click', function() {
377
- if (currentPage !== pageNumber) {
378
- currentPage = pageNumber;
379
- renderPagination(); // Re-render pagination to update active state
380
- renderVideoGrid();
381
- preloadThumbnailsForPage(currentPage);
382
- window.scrollTo(0, videoGrid.offsetTop - 20); // Scroll towards top of grid
383
- }
384
  });
385
- return button;
386
  }
387
 
388
-
389
- // --- Player Functions ---
390
-
391
- // Clean up previous player instances (Plyr, HLS, DASH)
392
- function cleanupPlayerInstances() {
393
- console.log("Cleaning up player instances...");
394
- if (debugInterval) {
395
- clearInterval(debugInterval);
396
- debugInterval = null;
397
- }
398
- if (player) {
399
- try {
400
- player.destroy();
401
- } catch (e) { console.error("Error destroying Plyr:", e); }
402
- player = null;
403
- }
404
- if (hls) {
405
- try {
406
- hls.stopLoad();
407
- hls.detachMedia();
408
- hls.destroy();
409
- } catch (e) { console.error("Error destroying HLS:", e); }
410
- hls = null;
411
- }
412
- if (dash) {
413
- try {
414
- dash.reset(); // Resets the MediaPlayer instance
415
- } catch (e) { console.error("Error resetting DASH:", e); }
416
- dash = null; // Assuming create() makes a new context
417
- }
418
- playerContainer.innerHTML = ''; // Clear the container
419
- qualitySelector.innerHTML = '<option value="auto">Auto</option>'; // Reset quality
420
- qualitySelector.value = 'auto';
421
- qualitySelector.disabled = true;
422
- debugOverlay.style.display = 'none';
423
- isDebugVisible = false;
424
- }
425
-
426
- // Play the selected video
427
  function playVideo(video) {
428
- if (!video || !video.url) {
429
- console.error("Invalid video object passed to playVideo:", video);
430
- alert("Cannot play this video: Invalid data.");
431
- return;
432
- }
433
  currentVideo = video;
434
- currentTitle.textContent = video.title || 'Untitled Video';
435
-
436
- playerLoading.style.display = 'flex';
437
- playerPopup.classList.add('active');
438
- document.body.style.overflow = 'hidden'; // Prevent background scroll
439
-
440
- cleanupPlayerInstances(); // Clean up before creating new
441
-
442
- // Create the video element for Plyr
443
- const videoElement = document.createElement('video');
444
- videoElement.id = 'videoPlayerInstance'; // Give it an ID
445
- videoElement.setAttribute('playsinline', ''); // Important for iOS
446
- videoElement.setAttribute('crossorigin', 'anonymous'); // Needed for HLS/DASH usually
447
- // videoElement.controls = true; // Let Plyr handle controls
448
- playerContainer.appendChild(videoElement);
449
-
450
- initializeStreamingPlayer(videoElement, video.url);
451
- }
452
-
453
- // Initialize HLS.js, Dash.js, or standard HTML5 video based on URL
454
- function initializeStreamingPlayer(videoElement, videoUrl) {
455
- const url = videoUrl.trim();
456
- const fileExtension = url.split('.').pop().toLowerCase().split('?')[0]; // Get extension before query params
457
-
458
- console.log(`Initializing player for URL: ${url} (Extension: ${fileExtension})`);
459
-
460
- // --- IMPORTANT URL CHECK ---
461
- // Check if the URL is actually accessible and looks like a media file/manifest
462
- if (!url.startsWith('http://') && !url.startsWith('https://')) {
463
- console.error("Video URL does not start with http:// or https://:", url);
464
- alert(`Error: Invalid video URL format provided: ${url}`);
465
- playerLoading.style.display = 'none';
466
- closePlayerPopup(); // Close the broken player
467
- return;
468
- }
469
-
470
- // Check for Hugging Face direct links - they might need specific handling
471
- // or might not support streaming manifests directly without extra steps.
472
- if (url.includes('huggingface.co') && (fileExtension === 'mp4' || fileExtension === 'webm' || fileExtension === 'mov')) {
473
- console.warn("Detected direct Hugging Face file URL. ABR streaming (HLS/DASH) requires a manifest (.m3u8/.mpd). Seeking performance might be limited.");
474
- // Proceed with standard player, but inform the user about potential limitations.
475
- }
476
-
477
-
478
- // 1. Check for HLS (.m3u8)
479
- if (fileExtension === 'm3u8') {
480
- if (Hls.isSupported()) {
481
- console.log("Using HLS.js");
482
- hls = new Hls({
483
- // Configuration focused on stability and seeking
484
- // Start with auto quality
485
- startLevel: -1,
486
- // Buffer settings: Allow seeking back easily, manage forward buffer
487
- maxBufferLength: 60, // Max seconds of buffer
488
- maxMaxBufferLength: 120, // Max buffer size when seeking forward
489
- backBufferLength: 90, // Keep 90s behind current time for seeking back
490
- // ABR settings: More stable, less aggressive switching
491
- abrEwmaDefaultEstimate: 500000, // Start estimate lower (500kbps)
492
- abrBandWidthFactor: 0.8, // Be slightly conservative
493
- abrBandWidthUpFactor: 0.6, // Don't jump up too eagerly
494
- // Performance
495
- enableWorker: true, // Use web workers if available
496
- // Low Latency (might not be needed unless it's a live stream)
497
- // lowLatencyMode: false,
498
- // Seeking / Frag loading tweaks
499
- fragLoadingMaxRetry: 4,
500
- levelLoadingMaxRetry: 4,
501
- manifestLoadingMaxRetry: 2,
502
- fragLoadingTimeOut: 15000, // 15 seconds timeout for fragments
503
- levelLoadingTimeOut: 10000,
504
- manifestLoadingTimeOut: 10000,
505
- });
506
-
507
- hls.loadSource(url);
508
- hls.attachMedia(videoElement);
509
-
510
- hls.on(Hls.Events.MANIFEST_PARSED, (event, data) => {
511
- console.log("HLS Manifest Parsed:", data.levels);
512
- initPlyr(videoElement); // Init Plyr *after* manifest is ready
513
- populateQualitySelector(hls.levels.map((l, i) => ({ height: l.height, index: i })), 'hls');
514
- });
515
-
516
- hls.on(Hls.Events.LEVEL_SWITCHED, (event, data) => {
517
- console.log(`HLS Level Switched: ${data.level}`);
518
- if (qualitySelector.value !== 'auto') {
519
- // If user manually selected a quality, but ABR switched away
520
- // (e.g., due to bandwidth), reflect the change back to Auto.
521
- // Or keep it? Decide on UX. Let's reset to Auto for clarity.
522
- // const currentLevel = hls.levels[data.level];
523
- // if (qualitySelector.value != currentLevel.height) {
524
- // console.log("ABR override manual selection, resetting selector to Auto");
525
- // qualitySelector.value = 'auto';
526
- // }
527
- }
528
- updateDebugInfo();
529
- });
530
-
531
- hls.on(Hls.Events.ERROR, (event, data) => {
532
- console.error('HLS Error:', data);
533
- handleStreamingError(data, 'HLS');
534
- });
535
-
536
- } else if (videoElement.canPlayType('application/vnd.apple.mpegurl')) {
537
- console.log("Using native HLS support");
538
- videoElement.src = url;
539
- initPlyr(videoElement); // Init Plyr for native HLS
540
- qualitySelector.disabled = true; // Native HLS often doesn't expose quality levels easily
541
- videoElement.addEventListener('loadedmetadata', () => {
542
- playerLoading.style.display = 'none';
543
- });
544
- } else {
545
- console.error("HLS not supported by HLS.js or natively.");
546
- alert("Cannot play this video format (HLS) in your browser.");
547
- playerLoading.style.display = 'none';
548
  }
549
- }
550
- // 2. Check for DASH (.mpd)
551
- else if (fileExtension === 'mpd') {
552
- console.log("Using Dash.js");
553
- try {
554
- dash = dashjs.MediaPlayer().create();
555
- // Config for stability and seeking
556
- dash.updateSettings({
557
- 'streaming': {
558
- 'abr': {
559
- // 'ABRStrategy': 'abrL2A', // Throughput based
560
- 'useDefaultABRRules': true,
561
- 'ABRStrategy': 'abrThroughput', // Often better for VOD seeking
562
- 'bandwidthSafetyFactor': 0.8,
563
- 'useDeadTimeLatency': false, // Good for VOD
564
- },
565
- 'buffer': {
566
- 'stableBufferTime': 30, // Target buffer
567
- 'bufferTimeAtTopQuality': 45, // Allow more buffer at highest quality
568
- 'bufferTimeAtTopQualityLongForm': 60,
569
- 'longFormContentDurationThreshold': 600, // Content > 10 mins
570
- 'bufferToKeep': 20, // Keep less behind playhead than HLS default
571
- 'bufferPruningInterval': 15,
572
- 'fastSwitchEnabled': true, // Enable faster quality switching
573
- },
574
- 'scheduling': {
575
- 'scheduleWhilePaused': true, // Keep buffering when paused
576
- }
577
- }
578
- });
579
-
580
- dash.initialize(videoElement, url, true); // Autoplay = true
581
-
582
- dash.on(dashjs.MediaPlayer.events.STREAM_INITIALIZED, () => {
583
- console.log("DASH Stream Initialized");
584
- initPlyr(videoElement);
585
- const qualities = dash.getBitrateInfoListFor('video');
586
- console.log("DASH Qualities:", qualities);
587
- populateQualitySelector(qualities.map(q => ({ height: q.height, index: q.qualityIndex })), 'dash');
588
- });
589
-
590
- dash.on(dashjs.MediaPlayer.events.QUALITY_CHANGE_RENDERED, (e) => {
591
- console.log(`DASH Quality Changed: ${e.mediaType} to ${e.newQuality} (${dash.getBitrateInfoListFor(e.mediaType)[e.newQuality]?.height}p)`);
592
- updateDebugInfo();
593
- });
594
-
595
- dash.on(dashjs.MediaPlayer.events.ERROR, (e) => {
596
- console.error('DASH Error:', e.error.message, e);
597
- handleStreamingError(e.error, 'DASH'); // Pass the error object itself
598
- });
599
-
600
- } catch (error) {
601
- console.error("Failed to initialize Dash.js:", error);
602
- alert(`Error initializing DASH player: ${error.message}`);
603
- playerLoading.style.display = 'none';
604
- // Optionally fallback to standard player if possible?
605
- }
606
- }
607
- // 3. Fallback to standard HTML5 video (mp4, webm, etc.)
608
- else {
609
- console.log(`Using standard HTML5 video player for extension: ${fileExtension}`);
610
- if (videoElement.canPlayType(`video/${fileExtension}`) || fileExtension === 'mp4' || fileExtension === 'webm' || fileExtension === 'ogv') {
611
- videoElement.src = url;
612
- initPlyr(videoElement); // Initialize Plyr directly
613
- qualitySelector.disabled = true; // No quality options for standard video
614
- // Use loadedmetadata event for standard video as 'ready' might fire too early
615
- videoElement.addEventListener('loadedmetadata', () => {
616
- playerLoading.style.display = 'none';
617
- });
618
- videoElement.addEventListener('error', (e) => {
619
- console.error("HTML5 Video Error:", videoElement.error);
620
- alert(`Error playing video: ${videoElement.error?.message || 'Unknown error'}`);
621
- playerLoading.style.display = 'none';
622
- });
623
- } else {
624
- console.error(`Unsupported video format: ${fileExtension}`);
625
- alert(`Your browser does not support the video format: ${fileExtension}`);
626
- playerLoading.style.display = 'none';
627
- }
628
- }
629
- }
630
-
631
- // Handle errors from HLS.js or Dash.js
632
- function handleStreamingError(errorData, sourceType) {
633
- let shouldClose = false;
634
- let message = `A ${sourceType} error occurred.`;
635
-
636
- if (sourceType === 'HLS') {
637
- if (errorData.fatal) {
638
- message += ` Fatal error: ${errorData.type} - ${errorData.details}`;
639
- console.error(message);
640
- switch (errorData.type) {
641
- case Hls.ErrorTypes.NETWORK_ERROR:
642
- console.warn("HLS Network Error. Attempting recovery...");
643
- if (hls) hls.startLoad(); // Try to recover network errors
644
- else shouldClose = true;
645
- break;
646
- case Hls.ErrorTypes.MEDIA_ERROR:
647
- console.warn("HLS Media Error. Attempting recovery...");
648
- if (hls) hls.recoverMediaError();
649
- else shouldClose = true;
650
- break;
651
- default:
652
- console.error("Unrecoverable HLS error.");
653
- shouldClose = true; // Close on other fatal errors
654
- break;
655
- }
656
- } else {
657
- // Non-fatal errors, just log them
658
- message += ` Non-fatal: ${errorData.type} - ${errorData.details}`;
659
- console.warn(message);
660
  }
661
- } else if (sourceType === 'DASH') {
662
- message = `DASH Error: ${errorData.code} - ${errorData.message}`;
663
- console.error(message, errorData);
664
- // Dash.js errors are often less recoverable by simple API calls
665
- shouldClose = true; // Assume most DASH errors require closing player for now
666
  }
667
-
668
- if (shouldClose) {
669
- alert(`Could not play video due to a streaming error:\n${message}\nPlease try again later or select a different video.`);
670
- closePlayerPopup();
671
  } else {
672
- // For recoverable errors, maybe show a temporary warning?
673
- playerLoading.innerHTML = `<div class="spinner"></div><span>Error occurred, trying to recover...</span>`;
674
- playerLoading.style.display = 'flex';
675
- // Hide after a few seconds if playback resumes
676
- setTimeout(() => {
677
- if(player && !player.paused) playerLoading.style.display = 'none';
678
- }, 3000);
679
  }
 
 
 
 
 
 
680
  }
681
 
682
- // Populate the quality selector dropdown
683
- function populateQualitySelector(levels, type) {
684
- qualitySelector.innerHTML = '<option value="auto">Auto</option>'; // Start fresh
685
- qualitySelector.disabled = false;
686
-
687
- if (levels && levels.length > 0) {
688
- // Sort by height descending
689
- levels.sort((a, b) => (b.height || 0) - (a.height || 0));
690
-
691
- // Use a Set to avoid duplicate heights (common in DASH)
692
- const uniqueHeights = new Set();
693
- levels.forEach(level => {
694
- if (level.height && !uniqueHeights.has(level.height)) {
695
- uniqueHeights.add(level.height);
696
- const option = document.createElement('option');
697
- option.value = level.index; // Store index for setting quality
698
- option.dataset.height = level.height; // Store height for display/logic
699
- option.textContent = `${level.height}p`;
700
- qualitySelector.appendChild(option);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
701
  }
702
  });
 
 
 
 
703
 
704
- // Ensure 'auto' is selected initially
705
- qualitySelector.value = 'auto';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
706
 
707
- } else {
708
- console.warn("No quality levels found to populate selector.");
709
- qualitySelector.disabled = true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
710
  }
711
  }
712
 
713
- // Initialize Plyr player instance
714
- function initPlyr(videoElement) {
715
- if (player) {
716
- console.warn("Plyr already initialized. Destroying previous instance.");
717
- try { player.destroy(); } catch(e){}
 
 
 
 
718
  }
719
- console.log("Initializing Plyr...");
 
 
720
 
721
- const plyrOptions = {
722
- // Controls: Customize as needed
723
- controls: [
724
- 'play-large', 'restart', 'play', 'progress', 'current-time', 'duration',
725
- 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen'
726
- ],
727
- settings: ['captions', 'quality', 'speed', 'loop'],
728
- // Quality handling delegated to our custom selector + HLS/DASH
729
- quality: {
730
- default: 'auto', // Corresponds to our value
731
- options: [], // We populate this via populateQualitySelector
732
- forced: true, // Use our logic
733
- onChange: (newQuality) => {
734
- // This might be called by Plyr's internal settings menu if enabled
735
- // Let's sync our selector if Plyr changes it
736
- console.log("Plyr quality onChange triggered:", newQuality);
737
- const selectedHeight = parseInt(newQuality); // Plyr usually passes height
738
- let foundOption = false;
739
- for(let option of qualitySelector.options) {
740
- if (option.dataset.height == selectedHeight) {
741
- qualitySelector.value = option.value;
742
- foundOption = true;
743
- break;
744
- }
745
- }
746
- if (!foundOption && newQuality === 'auto') {
747
- qualitySelector.value = 'auto';
748
- }
749
- // Trigger our own change handler
750
- handleQualityChange();
751
- },
752
- },
753
- speed: { selected: 1, options: [0.5, 0.75, 1, 1.25, 1.5, 2] },
754
- keyboard: { focused: true, global: true }, // Allow spacebar etc.
755
- tooltips: { controls: true, seek: true },
756
- seekTime: 10, // Seek interval
757
- // Ensure autoplay works after user interaction
758
- autoplay: true, // Attempt autoplay
759
- // Volume persistence
760
- storage: { enabled: true, key: 'plyr_volume' },
761
- // Fix for aspect ratio issues sometimes
762
- ratio: '16:9', // Or detect dynamically if needed
763
- };
764
 
 
765
  try {
766
- player = new Plyr(videoElement, plyrOptions);
767
-
768
- // Plyr Event Listeners
769
- player.on('ready', event => {
770
- console.log('Plyr Ready');
771
- playerLoading.style.display = 'none';
772
- // Attempt to play, might be blocked by browser
773
- player.play().catch(error => console.warn('Autoplay prevented:', error));
774
- initDebugOverlay(); // Setup debug toggle listener
775
- });
776
-
777
- player.on('playing', event => {
778
- playerLoading.style.display = 'none';
779
- updateDebugInfo(); // Start updating debug info if visible
780
- });
781
-
782
- player.on('waiting', event => {
783
- playerLoading.style.display = 'flex'; // Show loading on buffer stall
784
- });
785
-
786
- player.on('seeking', event => {
787
- playerLoading.style.display = 'flex'; // Show loading immediately on seek start
788
- });
789
-
790
- player.on('seeked', event => {
791
- // Hide loading quickly after seek *if* video is ready to play
792
- // Check if paused - if seeking while paused, don't hide loading
793
- if (!player.paused) {
794
- playerLoading.style.display = 'none';
795
- }
796
- });
797
-
798
- player.on('qualitychange', event => {
799
- console.log("Plyr qualitychange event (internal):", event.detail.quality);
800
- updateDebugInfo();
801
- });
802
-
803
- player.on('error', event => {
804
- console.error("Plyr player error:", event.detail.error);
805
- // Don't necessarily close popup on generic plyr error, let HLS/DASH handle stream errors
806
- });
807
-
808
- player.on('controlsshown', () => updateDebugInfo()); // Update when controls appear
809
-
810
-
811
  } catch (error) {
812
- console.error("Failed to initialize Plyr:", error);
813
- alert(`Error initializing video player UI: ${error.message}`);
814
- playerLoading.style.display = 'none';
815
  }
 
816
  }
817
 
818
- // Handle change event from our custom quality selector
819
- function handleQualityChange() {
820
- if (!qualitySelector.value) return;
821
-
822
- const selectedValue = qualitySelector.value; // This is 'auto' or the level/quality index
823
-
824
- console.log(`Quality selector changed to: ${selectedValue}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
825
 
826
- if (selectedValue === 'auto') {
827
- // Enable ABR
828
- if (hls) {
829
- console.log("Setting HLS to Auto quality (level -1)");
830
- hls.currentLevel = -1;
831
- }
832
- if (dash) {
833
- console.log("Setting DASH to Auto quality");
834
- dash.updateSettings({ 'streaming': { 'abr': { 'initialBitrate': { 'video': -1 }, 'autoSwitchBitrate': { 'video': true } } } });
835
- }
836
  } else {
837
- // Disable ABR and set specific level/quality index
838
- const qualityIndex = parseInt(selectedValue);
839
- if (!isNaN(qualityIndex)) {
840
- if (hls) {
841
- console.log(`Setting HLS quality index: ${qualityIndex}`);
842
- hls.currentLevel = qualityIndex;
843
- }
844
- if (dash) {
845
- console.log(`Setting DASH quality index: ${qualityIndex}`);
846
- dash.updateSettings({ 'streaming': { 'abr': { 'autoSwitchBitrate': { 'video': false } } } });
847
- // Setting quality might need a slight delay after disabling ABR
848
- setTimeout(() => dash.setQualityFor('video', qualityIndex, true), 50); // true = immediate switch
849
- }
850
- } else {
851
- console.warn("Invalid quality index selected:", selectedValue);
852
- }
853
  }
854
- updateDebugInfo(); // Update debug info after change
855
  }
856
 
857
- // Close the player popup
858
- function closePlayerPopup() {
859
- playerPopup.classList.remove('active');
860
- document.body.style.overflow = ''; // Restore background scroll
861
- cleanupPlayerInstances(); // Crucial to stop streams and free resources
862
- currentVideo = null;
863
- currentTitle.textContent = '';
 
 
 
864
  }
865
 
866
- // --- Debugging Functions ---
867
-
868
- function initDebugOverlay() {
869
- debugToggle.removeEventListener('click', toggleDebugDisplay); // Remove previous listener if any
870
- debugToggle.addEventListener('click', toggleDebugDisplay);
871
- // Reset state
872
- debugOverlay.style.display = 'none';
873
- isDebugVisible = false;
874
- if (debugInterval) clearInterval(debugInterval);
875
  }
876
 
877
- function toggleDebugDisplay() {
878
- isDebugVisible = !isDebugVisible;
879
- if (isDebugVisible) {
880
- debugOverlay.style.display = 'block';
881
- updateDebugInfo(); // Initial update
882
- if (!debugInterval) {
883
- debugInterval = setInterval(updateDebugInfo, 1500); // Update ~ every 1.5 seconds
 
884
  }
885
  } else {
886
- debugOverlay.style.display = 'none';
887
- if (debugInterval) {
888
- clearInterval(debugInterval);
889
- debugInterval = null;
 
 
890
  }
891
  }
892
  }
893
 
894
- function updateDebugInfo() {
895
- if (!isDebugVisible || !player || !player.media) return;
896
-
897
- const video = player.media;
898
- let info = `Player State: ${player.playing ? 'Playing' : (player.paused ? 'Paused' : 'Idle')}\n`;
899
- info += `Resolution: ${video.videoWidth}x${video.videoHeight}\n`;
900
- info += `Time: ${formatTime(video.currentTime)} / ${formatTime(video.duration)}\n`;
901
- info += `Volume: ${Math.round(player.volume * 100)}% ${player.muted ? '(Muted)' : ''}\n`;
902
- info += `Playback Rate: ${player.speed}x\n`;
903
- info += `Buffered: ${getBufferedRanges(video)}\n`;
904
- info += `Network State: ${getNetworkState(video.networkState)}\n`;
905
- info += `Ready State: ${getReadyState(video.readyState)}\n`;
906
-
907
- if (hls) {
908
- info += "\n--- HLS.js Info ---\n";
909
- const currentLevel = hls.levels[hls.currentLevel];
910
- const autoLevel = hls.autoLevelEnabled;
911
- info += `Mode: HLS.js ${autoLevel ? '(Auto)' : '(Manual)'}\n`;
912
- if (currentLevel) {
913
- info += `Current Level: ${hls.currentLevel} (${currentLevel.width}x${currentLevel.height} @ ${formatBitrate(currentLevel.bitrate)})\n`;
914
- } else {
915
- info += `Current Level: ${hls.currentLevel} (Auto/Initial)\n`;
916
- }
917
- info += `Target Level: ${hls.nextLevel} ${hls.nextAutoLevel ? '(Auto)' : ''}\n`;
918
- info += `Load Level: ${hls.loadLevel}\n`;
919
- info += `Buffer Length: ${hls.mainForwardBufferInfo.len?.toFixed(2) || '?'}s\n`;
920
- info += `Live Latency: ${hls.liveSyncPosition?.toFixed(2) || 'N/A'}s\n`;
921
- const stats = hls.stats;
922
- if (stats) {
923
- info += `Dropped Frames: ${stats.totalDroppedFrames || 0}\n`;
924
- info += `BW Estimate: ${formatBitrate(stats.bwEstimate)}\n`;
925
- }
926
- }
927
-
928
- if (dash) {
929
- info += "\n--- Dash.js Info ---\n";
930
- const streamInfo = dash.getActiveStream()?.getStreamInfo();
931
- const videoQuality = dash.getQualityFor('video');
932
- const isAuto = dash.getSettings()?.streaming?.abr?.autoSwitchBitrate?.video ?? true;
933
- info += `Mode: Dash.js ${isAuto ? '(Auto)' : '(Manual)'}\n`;
934
- if (streamInfo) {
935
- info += `Stream ID: ${streamInfo.id}\n`;
936
- info += `Duration: ${formatTime(streamInfo.duration)}\n`;
937
- }
938
- const bitrateList = dash.getBitrateInfoListFor('video');
939
- if (bitrateList && bitrateList[videoQuality]) {
940
- const currentQuality = bitrateList[videoQuality];
941
- info += `Current Quality: ${videoQuality} (${currentQuality.width}x${currentQuality.height} @ ${formatBitrate(currentQuality.bitrate)})\n`;
942
- } else {
943
- info += `Current Quality: ${videoQuality} (Unknown)\n`;
944
- }
945
- info += `Buffer Length: ${dash.getBufferLength('video')?.toFixed(2) || '?'}s\n`;
946
- const dashMetrics = dash.getMetricsFor('video');
947
- if (dashMetrics?.DroppedFrames) {
948
- info += `Dropped Frames: ${dashMetrics.DroppedFrames.framesDropped || 0}\n`;
949
- }
950
- if(dashMetrics?.BufferLevel) {
951
- const bufferLevels = dashMetrics.BufferLevel;
952
- if(bufferLevels.length > 0) {
953
- info += `Buffer Level (Last): ${bufferLevels[bufferLevels.length - 1].level.toFixed(2)}s\n`;
954
- }
955
- }
956
- if (dashMetrics?.RequestsQueue) {
957
- info += `Request Queue: ${dashMetrics.RequestsQueue.requestsQueue?.length || 0}\n`;
958
- }
959
- }
960
-
961
- debugOverlay.textContent = info;
962
- }
963
-
964
- // Helper to format time
965
- function formatTime(seconds) {
966
- if (isNaN(seconds) || seconds === Infinity) return '0:00 / ∞';
967
- const date = new Date(0);
968
- date.setSeconds(seconds);
969
- return date.toISOString().substr(11, 8); // HH:MM:SS format
970
- }
971
-
972
- // Helper to format bitrate
973
- function formatBitrate(bits) {
974
- if (isNaN(bits) || bits === 0) return '0 bps';
975
- if (bits < 1000) return `${bits} bps`;
976
- if (bits < 1000000) return `${(bits / 1000).toFixed(1)} kbps`;
977
- return `${(bits / 1000000).toFixed(2)} Mbps`;
978
- }
979
-
980
- // Helper for buffered ranges
981
- function getBufferedRanges(video) {
982
- try {
983
- const buffered = video.buffered;
984
- if (!buffered || buffered.length === 0) return '[]';
985
- let ranges = '';
986
- for (let i = 0; i < buffered.length; i++) {
987
- ranges += `[${formatTime(buffered.start(i))} - ${formatTime(buffered.end(i))}] `;
988
- }
989
- return ranges.trim();
990
- } catch (e) {
991
- return '[Error accessing buffer]';
992
- }
993
  }
994
 
995
- // Helper for network state
996
- function getNetworkState(state) {
997
- switch (state) {
998
- case 0: return 'NETWORK_EMPTY';
999
- case 1: return 'NETWORK_IDLE';
1000
- case 2: return 'NETWORK_LOADING';
1001
- case 3: return 'NETWORK_NO_SOURCE';
1002
- default: return 'UNKNOWN';
1003
  }
 
 
 
1004
  }
1005
- // Helper for ready state
1006
- function getReadyState(state) {
1007
- switch (state) {
1008
- case 0: return 'HAVE_NOTHING';
1009
- case 1: return 'HAVE_METADATA';
1010
- case 2: return 'HAVE_CURRENT_DATA';
1011
- case 3: return 'HAVE_FUTURE_DATA';
1012
- case 4: return 'HAVE_ENOUGH_DATA';
1013
- default: return 'UNKNOWN';
1014
- }
1015
- }
1016
-
1017
-
1018
- // --- Event Listeners ---
1019
 
1020
  // Search functionality
1021
  searchInput.addEventListener('input', function() {
1022
- const searchTerm = this.value.toLowerCase().trim();
1023
- filteredVideos = videoData.filter(video =>
1024
  video.title.toLowerCase().includes(searchTerm)
1025
  );
1026
  currentPage = 1; // Reset to first page on search
1027
  renderPagination();
1028
  renderVideoGrid();
1029
- preloadThumbnailsForPage(currentPage);
1030
  });
1031
 
1032
- // Close popup button
1033
  closePopup.addEventListener('click', closePlayerPopup);
1034
 
1035
- // Close popup on ESC key
1036
- document.addEventListener('keydown', function(e) {
1037
- if (e.key === 'Escape' && playerPopup.classList.contains('active')) {
1038
  closePlayerPopup();
1039
  }
1040
- // Let Plyr handle other keyboard shortcuts like space, arrows etc.
1041
  });
1042
 
1043
- // Close popup when clicking outside the content area
1044
- playerPopup.addEventListener('click', function(e) {
1045
- if (e.target === playerPopup) { // Check if the click was directly on the overlay
1046
- closePlayerPopup();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1047
  }
1048
  });
1049
 
1050
- // Handle quality selector changes
1051
- qualitySelector.addEventListener('change', handleQualityChange);
1052
-
1053
-
1054
- // --- Initialization ---
1055
- document.addEventListener('DOMContentLoaded', fetchVideoData);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1056
 
 
 
1057
  </script>
1058
-
1059
  </body>
1060
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>HF Video Library</title>
 
 
 
7
  <style>
8
+ * {
9
+ box-sizing: border-box;
10
+ margin: 0;
11
+ padding: 0;
12
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
13
+ }
14
+ body {
15
+ background-color: #f5f5f5;
16
+ padding: 20px;
17
+ max-width: 1200px;
18
+ margin: 0 auto;
19
+ }
20
+ .container {
21
+ display: flex;
22
+ flex-direction: column;
23
+ gap: 20px;
24
+ }
25
+ .header {
26
+ background-color: #333;
27
+ color: white;
28
+ padding: 20px;
29
+ border-radius: 8px;
30
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
31
+ }
32
+ .search-container {
33
+ padding: 15px;
34
+ background-color: white;
35
+ border-radius: 8px;
36
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
37
+ }
38
+ #search {
39
+ width: 100%;
40
+ padding: 10px;
41
+ border: 1px solid #ddd;
42
+ border-radius: 4px;
43
+ font-size: 16px;
44
+ }
45
+ .video-list {
46
+ background-color: white;
47
+ border-radius: 8px;
48
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
49
+ padding: 15px;
50
+ }
51
+ .video-grid {
52
+ display: grid;
53
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
54
+ gap: 15px;
55
+ }
56
+ .video-item {
57
+ padding: 15px;
58
+ border: 1px solid #eee;
59
+ border-radius: 8px;
60
+ cursor: pointer;
61
+ transition: all 0.2s;
62
+ background-color: white;
63
+ }
64
+ .video-item:hover {
65
+ transform: translateY(-5px);
66
+ box-shadow: 0 5px 15px rgba(0,0,0,0.1);
67
+ }
68
+ .video-thumbnail {
69
+ width: 100%;
70
+ height: 156px;
71
+ background-color: #333;
72
+ border-radius: 4px;
73
+ display: flex;
74
+ align-items: center;
75
+ justify-content: center;
76
+ margin-bottom: 10px;
77
+ overflow: hidden;
78
+ position: relative;
79
+ }
80
+ .video-thumbnail img {
81
+ width: 100%;
82
+ height: 100%;
83
+ object-fit: cover;
84
+ }
85
+ .video-item-title {
86
+ font-weight: 500;
87
+ margin-top: 10px;
88
+ white-space: nowrap;
89
+ overflow: hidden;
90
+ text-overflow: ellipsis;
91
+ }
92
+ .pagination {
93
+ display: flex;
94
+ justify-content: center;
95
+ gap: 5px;
96
+ margin-top: 20px;
97
+ }
98
+ .page-btn {
99
+ padding: 8px 12px;
100
+ background-color: white;
101
+ border: 1px solid #ddd;
102
+ border-radius: 4px;
103
+ cursor: pointer;
104
+ transition: background-color 0.2s;
105
+ }
106
+ .page-btn:hover {
107
+ background-color: #f0f0f0;
108
+ }
109
+ .page-btn.active {
110
+ background-color: #333;
111
+ color: white;
112
+ border-color: #333;
113
+ }
114
+ .popup-overlay {
115
+ position: fixed;
116
+ top: 0;
117
+ left: 0;
118
+ width: 100%;
119
+ height: 100%;
120
+ background-color: rgba(0,0,0,0.8);
121
+ display: flex;
122
+ align-items: center;
123
+ justify-content: center;
124
+ z-index: 1000;
125
+ opacity: 0;
126
+ visibility: hidden;
127
+ transition: opacity 0.3s, visibility 0.3s;
128
+ }
129
+ .popup-overlay.active {
130
+ opacity: 1;
131
+ visibility: visible;
132
+ }
133
+ .popup-content {
134
+ width: 90%;
135
+ max-width: 1024px;
136
+ background-color: #222;
137
+ border-radius: 8px;
138
+ overflow: hidden;
139
+ position: relative;
140
+ box-shadow: 0 10px 25px rgba(0,0,0,0.5);
141
+ }
142
+ .close-popup {
143
+ position: absolute;
144
+ top: 10px;
145
+ right: 10px;
146
+ background-color: rgba(0,0,0,0.5);
147
+ color: white;
148
+ border: none;
149
+ width: 30px;
150
+ height: 30px;
151
+ border-radius: 50%;
152
+ display: flex;
153
+ align-items: center;
154
+ justify-content: center;
155
+ cursor: pointer;
156
+ z-index: 10;
157
+ font-size: 18px;
158
+ }
159
+ .player-wrapper {
160
+ position: relative;
161
+ width: 100%;
162
+ padding-top: 56.25%; /* 16:9 Aspect Ratio */
163
+ }
164
+ #videoPlayer {
165
+ position: absolute;
166
+ top: 0;
167
+ left: 0;
168
+ width: 100%;
169
+ height: 100%;
170
+ background-color: #000;
171
+ }
172
+ .popup-title {
173
+ color: white;
174
+ padding: 15px;
175
+ font-size: 1.2em;
176
+ background-color: #111;
177
+ }
178
+ .controls {
179
+ display: flex;
180
+ gap: 10px;
181
+ padding: 15px;
182
+ background-color: #111;
183
+ flex-wrap: wrap;
184
+ }
185
+ .btn {
186
+ padding: 8px 16px;
187
+ background-color: #333;
188
+ color: white;
189
+ border: none;
190
+ border-radius: 4px;
191
+ cursor: pointer;
192
+ transition: background-color 0.2s;
193
  }
194
+ .btn:hover {
195
+ background-color: #555;
196
+ }
197
+ .btn.active {
198
+ background-color: #007bff;
199
+ }
200
+ .instructions {
201
+ color: #ccc;
202
+ padding: 10px 15px;
203
+ background-color: #111;
204
+ font-size: 0.9em;
205
+ }
206
+ .play-icon {
207
+ font-size: 50px;
208
+ color: white;
209
+ opacity: 0.8;
210
+ transition: opacity 0.2s;
211
+ position: absolute;
212
+ }
213
+ .video-item:hover .play-icon {
214
+ opacity: 1;
215
+ }
216
+ .loading-spinner {
217
+ position: absolute;
218
+ top: 50%;
219
+ left: 50%;
220
+ transform: translate(-50%, -50%);
221
+ width: 40px;
222
+ height: 40px;
223
+ border: 4px solid rgba(255, 255, 255, 0.3);
224
+ border-radius: 50%;
225
+ border-top: 4px solid #fff;
226
+ animation: spin 1s linear infinite;
227
+ z-index: 10;
228
+ display: none;
229
+ }
230
+ @keyframes spin {
231
+ 0% { transform: translate(-50%, -50%) rotate(0deg); }
232
+ 100% { transform: translate(-50%, -50%) rotate(360deg); }
233
+ }
234
+ .buffering-indicator {
235
+ position: absolute;
236
+ bottom: 60px;
237
+ left: 10px;
238
+ background-color: rgba(0,0,0,0.5);
239
+ color: white;
240
+ padding: 5px 10px;
241
+ border-radius: 4px;
242
+ font-size: 12px;
243
+ z-index: 10;
244
+ display: none;
245
+ }
246
+ .seek-preview {
247
+ position: absolute;
248
+ bottom: 100px;
249
+ left: 50%;
250
+ transform: translateX(-50%);
251
+ background-color: rgba(0,0,0,0.7);
252
+ color: white;
253
+ padding: 5px 10px;
254
+ border-radius: 4px;
255
+ font-size: 14px;
256
+ z-index: 10;
257
+ display: none;
258
+ }
259
+ .no-videos {
260
+ padding: 20px;
261
+ text-align: center;
262
+ color: #777;
263
+ }
264
+ @media (max-width: 768px) {
265
+ .video-grid {
266
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
267
+ }
268
+ .popup-content {
269
+ width: 95%;
270
+ }
271
+ .controls {
272
+ gap: 5px;
273
+ padding: 10px;
274
+ }
275
+ .btn {
276
+ padding: 6px 12px;
277
+ font-size: 0.9em;
278
+ }
279
  }
280
  </style>
281
  </head>
282
  <body>
283
  <div class="container">
284
  <div class="header">
285
+ <h1>HF Video Library</h1>
286
  </div>
287
+
288
  <div class="search-container">
289
  <input type="text" id="search" placeholder="Search videos...">
290
  </div>
291
+
292
  <div class="video-list">
293
  <div class="video-grid" id="videoGrid">
294
+ <div class="video-item">
295
+ <div class="video-thumbnail">Loading...</div>
296
+ </div>
297
  </div>
298
  <div class="pagination" id="pagination"></div>
299
  </div>
 
302
  <!-- Video Player Popup -->
303
  <div class="popup-overlay" id="playerPopup">
304
  <div class="popup-content">
305
+ <button class="close-popup" id="closePopup">✕</button>
306
+ <div class="buffering-indicator" id="bufferingIndicator">Buffering...</div>
307
+ <div class="seek-preview" id="seekPreview"></div>
308
+ <div class="popup-title" id="currentTitle">Video Title</div>
 
 
 
 
 
 
 
309
  <div class="player-wrapper">
310
+ <div class="loading-spinner" id="loadingSpinner"></div>
311
+ <video id="videoPlayer" preload="auto" playsinline>
312
+ Your browser does not support the video tag.
313
+ </video>
314
+ </div>
315
+ <div class="controls">
316
+ <button class="btn" id="backwardBtn">⏪ 10s</button>
317
+ <button class="btn" id="playPauseBtn">⏯️ Play/Pause</button>
318
+ <button class="btn" id="forwardBtn">⏩ 10s</button>
319
+ <button class="btn" id="fullscreenBtn">⛶ Fullscreen</button>
320
+ <button class="btn" id="muteBtn">🔊 Mute/Unmute</button>
321
+ </div>
322
+ <div class="instructions">
323
+ <p><strong>Keyboard Controls:</strong> Space (Play/Pause) • ← (Back 10s) • → (Forward 10s) • F (Fullscreen) • M (Mute/Unmute)</p>
324
+ <p><strong>Double-tap:</strong> Left side (Back 10s) • Center (Play/Pause) • Right side (Forward 10s)</p>
325
  </div>
326
  </div>
327
  </div>
328
 
 
 
 
 
 
329
  <script>
330
+ // Global variables
331
  let videoData = [];
332
  let filteredVideos = [];
333
  let currentVideo = null;
334
  let currentPage = 1;
335
+ const videosPerPage = 10;
336
+ let isSeeking = false;
337
+ let preloadedSegments = new Set();
338
+
339
+ // DOM Elements
 
 
 
340
  const videoGrid = document.getElementById('videoGrid');
341
  const pagination = document.getElementById('pagination');
342
  const searchInput = document.getElementById('search');
343
  const playerPopup = document.getElementById('playerPopup');
344
+ const videoPlayer = document.getElementById('videoPlayer');
345
  const currentTitle = document.getElementById('currentTitle');
346
  const closePopup = document.getElementById('closePopup');
347
+ const playPauseBtn = document.getElementById('playPauseBtn');
348
+ const backwardBtn = document.getElementById('backwardBtn');
349
+ const forwardBtn = document.getElementById('forwardBtn');
350
+ const fullscreenBtn = document.getElementById('fullscreenBtn');
351
+ const muteBtn = document.getElementById('muteBtn');
352
+ const loadingSpinner = document.getElementById('loadingSpinner');
353
+ const bufferingIndicator = document.getElementById('bufferingIndicator');
354
+ const seekPreview = document.getElementById('seekPreview');
355
+
356
+ // Function to fetch video data
357
  async function fetchVideoData() {
 
358
  try {
359
+ const response = await fetch('database.json');
 
360
  if (!response.ok) {
361
+ throw new Error('Failed to fetch video data');
362
  }
363
  const data = await response.json();
364
+
365
+ // Check if data has videos array or is itself an array
366
+ videoData = Array.isArray(data) ? data :
367
+ (data.videos && Array.isArray(data.videos) ? data.videos : []);
368
+
369
+ // Optimize HF URLs
 
 
 
 
 
 
 
 
 
 
 
370
  videoData.forEach(video => {
371
+ if (video.url.includes('huggingface.co')) {
372
+ // Force no-transform for HF URLs
373
+ if (!video.url.includes('?')) {
374
+ video.url += '?';
375
+ }
376
+ if (!video.url.includes('download=true')) {
377
+ video.url += (video.url.endsWith('?') ? '' : '&') + 'download=true';
378
+ }
379
  }
380
  });
381
+
382
  filteredVideos = [...videoData];
 
383
  renderPagination();
384
  renderVideoGrid();
 
 
385
  } catch (error) {
386
+ console.error('Error fetching video data:', error);
387
  videoGrid.innerHTML = `<div class="no-videos">Error loading videos: ${error.message}</div>`;
388
  }
389
  }
390
 
391
+ // Function to render video grid
 
 
 
 
 
 
 
 
 
 
 
 
 
 
392
  function renderVideoGrid() {
 
393
  if (filteredVideos.length === 0) {
394
+ videoGrid.innerHTML = '<div class="no-videos">No videos found</div>';
395
+ pagination.innerHTML = '';
396
  return;
397
  }
398
 
399
+ // Calculate start and end index for current page
400
  const startIndex = (currentPage - 1) * videosPerPage;
401
  const endIndex = Math.min(startIndex + videosPerPage, filteredVideos.length);
402
+ const currentVideos = filteredVideos.slice(startIndex, endIndex);
403
 
404
+ videoGrid.innerHTML = currentVideos.map((video, index) => `
405
+ <div class="video-item" data-index="${startIndex + index}">
 
 
 
 
 
 
 
 
 
 
406
  <div class="video-thumbnail">
 
407
  <div class="play-icon">▶</div>
408
  </div>
409
  <div class="video-item-title">${video.title}</div>
410
+ </div>
411
+ `).join('');
412
+
413
+ // Add click event listeners to video items
414
+ document.querySelectorAll('.video-item').forEach(item => {
415
+ item.addEventListener('click', function() {
416
+ const index = parseInt(this.getAttribute('data-index'));
417
+ if (!isNaN(index) && index >= 0 && index < filteredVideos.length) {
418
+ playVideo(filteredVideos[index]);
419
+ }
420
  });
421
+ });
 
 
422
  }
423
 
424
+ // Function to render pagination
425
  function renderPagination() {
 
426
  const totalPages = Math.ceil(filteredVideos.length / videosPerPage);
427
+
428
+ if (totalPages <= 1) {
429
+ pagination.innerHTML = '';
430
+ return;
431
+ }
432
 
433
+ // Determine which page buttons to show
434
+ let buttons = [];
435
+ if (totalPages <= 7) {
436
+ // Show all pages if 7 or fewer
437
+ for (let i = 1; i <= totalPages; i++) {
438
+ buttons.push(i);
439
+ }
 
440
  } else {
441
+ // Always include first page
442
+ buttons.push(1);
443
+
444
+ // Add ellipsis if current page is more than 3
445
+ if (currentPage > 3) {
446
+ buttons.push('...');
 
 
 
 
 
 
447
  }
448
+
449
+ // Add pages around current page
450
+ const startPage = Math.max(2, currentPage - 1);
451
+ const endPage = Math.min(totalPages - 1, currentPage + 1);
452
+ for (let i = startPage; i <= endPage; i++) {
453
+ buttons.push(i);
 
 
 
 
 
 
 
 
 
 
454
  }
455
+
456
+ // Add ellipsis if current page is less than totalPages - 2
457
+ if (currentPage < totalPages - 2) {
458
+ buttons.push('...');
 
 
 
 
 
 
 
 
459
  }
460
+
461
+ // Always include last page
462
+ buttons.push(totalPages);
463
  }
464
+
465
+ // Create pagination HTML
466
+ pagination.innerHTML = buttons.map(button => {
467
+ if (button === '...') {
468
+ return '<span class="page-btn">...</span>';
 
 
 
 
 
 
 
469
  }
470
+ return `<button class="page-btn ${button === currentPage ? 'active' : ''}" data-page="${button}">${button}</button>`;
471
+ }).join('');
472
+
473
+ // Add click event listeners to page buttons
474
+ document.querySelectorAll('.page-btn[data-page]').forEach(button => {
475
+ button.addEventListener('click', function() {
476
+ currentPage = parseInt(this.getAttribute('data-page'));
 
 
 
 
477
  renderPagination();
478
  renderVideoGrid();
479
+ window.scrollTo(0, 0);
480
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
481
  });
 
482
  }
483
 
484
+ // Function to play video with ultra-fast seeking
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
485
  function playVideo(video) {
 
 
 
 
 
486
  currentVideo = video;
487
+ currentTitle.textContent = video.title;
488
+
489
+ // Reset state
490
+ isSeeking = false;
491
+ preloadedSegments.clear();
492
+
493
+ // Show loading spinner
494
+ loadingSpinner.style.display = 'block';
495
+ bufferingIndicator.style.display = 'none';
496
+
497
+ // Optimize HF URL
498
+ let videoUrl = video.url;
499
+ if (videoUrl.includes('huggingface.co')) {
500
+ if (!videoUrl.includes('?')) {
501
+ videoUrl += '?';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
502
  }
503
+ if (!videoUrl.includes('download=true')) {
504
+ videoUrl += (videoUrl.endsWith('?') ? '' : '&') + 'download=true';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
505
  }
 
 
 
 
 
506
  }
507
+
508
+ // Use MediaSource for advanced buffering control if supported
509
+ if (window.MediaSource && MediaSource.isTypeSupported('video/mp4')) {
510
+ setupMediaSourcePlayer(videoUrl);
511
  } else {
512
+ setupRegularPlayer(videoUrl);
 
 
 
 
 
 
513
  }
514
+
515
+ // Show popup
516
+ playerPopup.classList.add('active');
517
+
518
+ // Disable scrolling on body when popup is active
519
+ document.body.style.overflow = 'hidden';
520
  }
521
 
522
+ function setupMediaSourcePlayer(url) {
523
+ const mediaSource = new MediaSource();
524
+ videoPlayer.src = URL.createObjectURL(mediaSource);
525
+
526
+ mediaSource.addEventListener('sourceopen', function() {
527
+ const sourceBuffer = mediaSource.addSourceBuffer('video/mp4');
528
+
529
+ // Start loading the initial segment
530
+ fetchSegment(url, 0, 1024 * 1024 * 2) // First 2MB
531
+ .then(segment => {
532
+ sourceBuffer.addEventListener('updateend', function() {
533
+ if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
534
+ mediaSource.endOfStream();
535
+ videoPlayer.play().catch(e => console.error('Play error:', e));
536
+ loadingSpinner.style.display = 'none';
537
+
538
+ // Start aggressive preloading
539
+ preloadNextSegments(videoPlayer.duration, url);
540
+ }
541
+ }, { once: true });
542
+
543
+ sourceBuffer.appendBuffer(segment);
544
+ })
545
+ .catch(error => {
546
+ console.error('Error loading initial segment:', error);
547
+ // Fallback to regular player
548
+ setupRegularPlayer(url);
549
+ });
550
+
551
+ // Handle seeking
552
+ videoPlayer.addEventListener('seeking', function() {
553
+ if (!isSeeking && mediaSource.readyState === 'open') {
554
+ isSeeking = true;
555
+ const targetTime = videoPlayer.currentTime;
556
+ seekPreview.textContent = formatTime(targetTime);
557
+ seekPreview.style.display = 'block';
558
+
559
+ // Load segment around seek position
560
+ const segmentStart = Math.max(0, targetTime - 2);
561
+ const segmentEnd = Math.min(videoPlayer.duration, targetTime + 2);
562
+
563
+ fetchByteRange(url, segmentStart, segmentEnd)
564
+ .then(segment => {
565
+ sourceBuffer.appendBuffer(segment);
566
+ seekPreview.style.display = 'none';
567
+ isSeeking = false;
568
+ })
569
+ .catch(error => {
570
+ console.error('Seek error:', error);
571
+ seekPreview.style.display = 'none';
572
+ isSeeking = false;
573
+ });
574
  }
575
  });
576
+ });
577
+
578
+ setupPlayerEventListeners();
579
+ }
580
 
581
+ function setupRegularPlayer(url) {
582
+ videoPlayer.src = url;
583
+ videoPlayer.load();
584
+
585
+ // Set up event listeners for immediate playback
586
+ videoPlayer.addEventListener('canplaythrough', function() {
587
+ loadingSpinner.style.display = 'none';
588
+ videoPlayer.play().catch(e => console.error('Play error:', e));
589
+
590
+ // Start aggressive preloading
591
+ preloadNextSegments(videoPlayer.duration, url);
592
+ }, { once: true });
593
+
594
+ videoPlayer.addEventListener('seeking', function() {
595
+ const targetTime = videoPlayer.currentTime;
596
+ seekPreview.textContent = formatTime(targetTime);
597
+ seekPreview.style.display = 'block';
598
+
599
+ setTimeout(() => {
600
+ seekPreview.style.display = 'none';
601
+ }, 1000);
602
+ });
603
+
604
+ setupPlayerEventListeners();
605
+ }
606
 
607
+ function preloadNextSegments(duration, url) {
608
+ if (!duration || duration === Infinity) return;
609
+
610
+ const currentTime = videoPlayer.currentTime;
611
+ const segmentSize = 1024 * 1024 * 4; // 4MB segments
612
+
613
+ // Preload next 3 segments ahead
614
+ for (let i = 1; i <= 3; i++) {
615
+ const segmentStart = currentTime + (i * 5);
616
+ if (segmentStart >= duration) break;
617
+
618
+ const segmentEnd = Math.min(duration, segmentStart + 5);
619
+ const segmentKey = `${Math.floor(segmentStart)}-${Math.floor(segmentEnd)}`;
620
+
621
+ if (!preloadedSegments.has(segmentKey)) {
622
+ preloadedSegments.add(segmentKey);
623
+
624
+ // Just trigger the download, we don't need the data for regular playback
625
+ fetchByteRange(url, segmentStart, segmentEnd)
626
+ .catch(error => console.error('Preload error:', error));
627
+ }
628
  }
629
  }
630
 
631
+ async function fetchSegment(url, start, end) {
632
+ const response = await fetch(url, {
633
+ headers: {
634
+ 'Range': `bytes=${start}-${end}`
635
+ }
636
+ });
637
+
638
+ if (!response.ok) {
639
+ throw new Error(`HTTP error! status: ${response.status}`);
640
  }
641
+
642
+ return response.arrayBuffer();
643
+ }
644
 
645
+ async function fetchByteRange(url, startTime, endTime) {
646
+ // Estimate byte range based on time (approximation)
647
+ const duration = videoPlayer.duration || 60; // Fallback to 60s if unknown
648
+ const fileSize = await getFileSize(url);
649
+ const startByte = Math.floor((startTime / duration) * fileSize);
650
+ const endByte = Math.floor((endTime / duration) * fileSize);
651
+
652
+ return fetchSegment(url, startByte, endByte);
653
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
654
 
655
+ async function getFileSize(url) {
656
  try {
657
+ const response = await fetch(url, { method: 'HEAD' });
658
+ if (response.ok) {
659
+ const contentLength = response.headers.get('Content-Length');
660
+ return parseInt(contentLength) || 100 * 1024 * 1024; // Fallback to 100MB
661
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
662
  } catch (error) {
663
+ console.error('Error getting file size:', error);
 
 
664
  }
665
+ return 100 * 1024 * 1024; // Default fallback
666
  }
667
 
668
+ function setupPlayerEventListeners() {
669
+ // Play/Pause button
670
+ playPauseBtn.addEventListener('click', togglePlayPause);
671
+
672
+ // Backward/Forward buttons
673
+ backwardBtn.addEventListener('click', () => seekRelative(-10));
674
+ forwardBtn.addEventListener('click', () => seekRelative(10));
675
+
676
+ // Fullscreen button
677
+ fullscreenBtn.addEventListener('click', toggleFullscreen);
678
+
679
+ // Mute button
680
+ muteBtn.addEventListener('click', toggleMute);
681
+
682
+ // Player events
683
+ videoPlayer.addEventListener('play', () => {
684
+ playPauseBtn.textContent = '⏸️ Pause';
685
+ });
686
+
687
+ videoPlayer.addEventListener('pause', () => {
688
+ playPauseBtn.textContent = '⏯️ Play';
689
+ });
690
+
691
+ videoPlayer.addEventListener('waiting', () => {
692
+ bufferingIndicator.style.display = 'block';
693
+ });
694
+
695
+ videoPlayer.addEventListener('playing', () => {
696
+ bufferingIndicator.style.display = 'none';
697
+ });
698
+
699
+ videoPlayer.addEventListener('timeupdate', () => {
700
+ // Continuously preload ahead
701
+ preloadNextSegments(videoPlayer.duration, videoPlayer.src);
702
+ });
703
+
704
+ videoPlayer.addEventListener('error', (e) => {
705
+ console.error('Video error:', videoPlayer.error);
706
+ loadingSpinner.style.display = 'none';
707
+ bufferingIndicator.style.display = 'none';
708
+ });
709
+ }
710
 
711
+ function togglePlayPause() {
712
+ if (videoPlayer.paused) {
713
+ videoPlayer.play().catch(error => {
714
+ console.error('Error playing video:', error);
715
+ });
 
 
 
 
 
716
  } else {
717
+ videoPlayer.pause();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
718
  }
 
719
  }
720
 
721
+ function seekRelative(seconds) {
722
+ const newTime = Math.max(0, Math.min(videoPlayer.duration, videoPlayer.currentTime + seconds));
723
+ videoPlayer.currentTime = newTime;
724
+
725
+ // Show seek preview
726
+ seekPreview.textContent = formatTime(newTime);
727
+ seekPreview.style.display = 'block';
728
+ setTimeout(() => {
729
+ seekPreview.style.display = 'none';
730
+ }, 1000);
731
  }
732
 
733
+ function formatTime(seconds) {
734
+ const mins = Math.floor(seconds / 60);
735
+ const secs = Math.floor(seconds % 60);
736
+ return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
 
 
 
 
 
737
  }
738
 
739
+ function toggleFullscreen() {
740
+ if (!document.fullscreenElement) {
741
+ if (playerPopup.requestFullscreen) {
742
+ playerPopup.requestFullscreen();
743
+ } else if (playerPopup.webkitRequestFullscreen) {
744
+ playerPopup.webkitRequestFullscreen();
745
+ } else if (playerPopup.msRequestFullscreen) {
746
+ playerPopup.msRequestFullscreen();
747
  }
748
  } else {
749
+ if (document.exitFullscreen) {
750
+ document.exitFullscreen();
751
+ } else if (document.webkitExitFullscreen) {
752
+ document.webkitExitFullscreen();
753
+ } else if (document.msExitFullscreen) {
754
+ document.msExitFullscreen();
755
  }
756
  }
757
  }
758
 
759
+ function toggleMute() {
760
+ videoPlayer.muted = !videoPlayer.muted;
761
+ muteBtn.textContent = videoPlayer.muted ? '🔇 Unmute' : '🔊 Mute';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
762
  }
763
 
764
+ // Close popup function
765
+ function closePlayerPopup() {
766
+ videoPlayer.pause();
767
+ if (videoPlayer.src.startsWith('blob:')) {
768
+ URL.revokeObjectURL(videoPlayer.src);
 
 
 
769
  }
770
+ videoPlayer.src = '';
771
+ playerPopup.classList.remove('active');
772
+ document.body.style.overflow = '';
773
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
774
 
775
  // Search functionality
776
  searchInput.addEventListener('input', function() {
777
+ const searchTerm = this.value.toLowerCase();
778
+ filteredVideos = videoData.filter(video =>
779
  video.title.toLowerCase().includes(searchTerm)
780
  );
781
  currentPage = 1; // Reset to first page on search
782
  renderPagination();
783
  renderVideoGrid();
 
784
  });
785
 
786
+ // Close popup when clicking close button
787
  closePopup.addEventListener('click', closePlayerPopup);
788
 
789
+ // Close popup when clicking outside content
790
+ playerPopup.addEventListener('click', function(e) {
791
+ if (e.target === playerPopup) {
792
  closePlayerPopup();
793
  }
 
794
  });
795
 
796
+ // Keyboard controls (only active when popup is visible)
797
+ document.addEventListener('keydown', function(e) {
798
+ if (!playerPopup.classList.contains('active')) return;
799
+
800
+ switch(e.key) {
801
+ case ' ': // Space
802
+ togglePlayPause();
803
+ e.preventDefault();
804
+ break;
805
+ case 'ArrowRight':
806
+ seekRelative(10);
807
+ e.preventDefault();
808
+ break;
809
+ case 'ArrowLeft':
810
+ seekRelative(-10);
811
+ e.preventDefault();
812
+ break;
813
+ case 'f':
814
+ case 'F':
815
+ toggleFullscreen();
816
+ e.preventDefault();
817
+ break;
818
+ case 'm':
819
+ case 'M':
820
+ toggleMute();
821
+ e.preventDefault();
822
+ break;
823
+ case 'Escape':
824
+ closePlayerPopup();
825
+ e.preventDefault();
826
+ break;
827
  }
828
  });
829
 
830
+ // Double tap controls for mobile/touch devices
831
+ let lastTap = 0;
832
+ let tapTimeout;
833
+
834
+ videoPlayer.addEventListener('click', function(e) {
835
+ const currentTime = new Date().getTime();
836
+ const tapLength = currentTime - lastTap;
837
+ clearTimeout(tapTimeout);
838
+
839
+ if (tapLength < 500 && tapLength > 0) {
840
+ // Double tap detected
841
+ const playerRect = videoPlayer.getBoundingClientRect();
842
+ const clickX = e.clientX - playerRect.left;
843
+ const playerWidth = playerRect.width;
844
+
845
+ // Determine click position (left, center, or right third)
846
+ if (clickX < playerWidth / 3) {
847
+ // Left side - backward
848
+ seekRelative(-10);
849
+ } else if (clickX > (playerWidth * 2) / 3) {
850
+ // Right side - forward
851
+ seekRelative(10);
852
+ } else {
853
+ // Center - play/pause
854
+ togglePlayPause();
855
+ }
856
+ e.preventDefault();
857
+ } else {
858
+ // Single tap - wait to see if it becomes a double tap
859
+ tapTimeout = setTimeout(function() {
860
+ // Single tap action (toggle controls visibility)
861
+ if (videoPlayer.hasAttribute('controls')) {
862
+ videoPlayer.removeAttribute('controls');
863
+ } else {
864
+ videoPlayer.setAttribute('controls', '');
865
+ }
866
+ }, 300);
867
+ }
868
+ lastTap = currentTime;
869
+ });
870
 
871
+ // Initialize
872
+ fetchVideoData();
873
  </script>
 
874
  </body>
875
  </html>