Subham9126 commited on
Commit
398a831
·
verified ·
1 Parent(s): 77d1f67

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +270 -572
index.html CHANGED
@@ -3,8 +3,13 @@
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;
@@ -60,6 +65,8 @@
60
  cursor: pointer;
61
  transition: all 0.2s;
62
  background-color: white;
 
 
63
  }
64
  .video-item:hover {
65
  transform: translateY(-5px);
@@ -67,8 +74,8 @@
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;
@@ -77,14 +84,24 @@
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;
@@ -117,7 +134,7 @@
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;
@@ -132,130 +149,66 @@
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;
@@ -266,31 +219,32 @@
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>
@@ -303,479 +257,290 @@
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
@@ -783,93 +548,26 @@
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>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>HF Video Library (Optimized with Plyr)</title>
7
+
8
+ <!-- Plyr CSS -->
9
+ <link rel="stylesheet" href="https://cdn.plyr.io/3.7.8/plyr.css" />
10
+
11
  <style>
12
+ /* --- Keep your existing styles for layout, search, grid, etc. --- */
13
  * {
14
  box-sizing: border-box;
15
  margin: 0;
 
65
  cursor: pointer;
66
  transition: all 0.2s;
67
  background-color: white;
68
+ display: flex; /* Use flexbox for better alignment */
69
+ flex-direction: column;
70
  }
71
  .video-item:hover {
72
  transform: translateY(-5px);
 
74
  }
75
  .video-thumbnail {
76
  width: 100%;
77
+ height: 156px; /* Maintain aspect ratio consistency */
78
+ background-color: #e0e0e0; /* Lighter placeholder */
79
  border-radius: 4px;
80
  display: flex;
81
  align-items: center;
 
84
  overflow: hidden;
85
  position: relative;
86
  }
87
+ .video-thumbnail img { /* Optional: If you add thumbnail images later */
88
  width: 100%;
89
  height: 100%;
90
  object-fit: cover;
91
+ }
92
+ .play-icon { /* Simple text icon */
93
+ font-size: 40px;
94
+ color: rgba(0,0,0,0.6);
95
+ opacity: 0.8;
96
+ transition: opacity 0.2s;
97
+ position: absolute;
98
+ }
99
+ .video-item:hover .play-icon {
100
+ opacity: 1;
101
  }
102
  .video-item-title {
103
  font-weight: 500;
104
+ margin-top: auto; /* Pushes title down if thumbnail varies slightly */
105
  white-space: nowrap;
106
  overflow: hidden;
107
  text-overflow: ellipsis;
 
134
  left: 0;
135
  width: 100%;
136
  height: 100%;
137
+ background-color: rgba(0,0,0,0.85); /* Slightly darker */
138
  display: flex;
139
  align-items: center;
140
  justify-content: center;
 
149
  }
150
  .popup-content {
151
  width: 90%;
152
+ max-width: 1100px; /* Slightly wider for player */
153
+ background-color: #000; /* Black background suits player */
154
  border-radius: 8px;
155
  overflow: hidden;
156
  position: relative;
157
  box-shadow: 0 10px 25px rgba(0,0,0,0.5);
158
+ display: flex; /* Use flexbox for title + player */
159
+ flex-direction: column;
160
  }
161
  .close-popup {
162
  position: absolute;
163
+ top: 8px; /* Adjust for better placement */
164
+ right: 8px; /* Adjust for better placement */
165
+ background-color: rgba(30,30,30,0.7); /* Subtle background */
166
  color: white;
167
  border: none;
168
+ width: 32px; /* Slightly larger */
169
+ height: 32px;
170
  border-radius: 50%;
171
  display: flex;
172
  align-items: center;
173
  justify-content: center;
174
  cursor: pointer;
175
+ z-index: 1001; /* Ensure it's above Plyr */
176
  font-size: 18px;
177
+ line-height: 1; /* Ensure centering */
178
  }
179
+ .close-popup:hover {
180
+ background-color: rgba(50,50,50,0.9);
181
+ }
182
+
 
 
 
 
 
 
 
 
 
183
  .popup-title {
184
  color: white;
185
+ padding: 12px 15px; /* Adjust padding */
186
+ font-size: 1.1em; /* Adjust size */
187
  background-color: #111;
188
+ text-align: center;
189
+ white-space: nowrap;
190
+ overflow: hidden;
191
+ text-overflow: ellipsis;
192
+ order: -1; /* Display title above video */
193
  }
194
+
195
+ /* Remove custom controls, player-wrapper, spinner, indicators */
196
+ /* .controls, .player-wrapper, #loadingSpinner, #bufferingIndicator, #seekPreview { display: none; } */
197
+
198
+ /* Style the video element itself if needed, but Plyr handles most */
199
+ #videoPlayer {
200
+ display: block; /* Ensure it takes block layout */
201
+ width: 100%;
202
+ height: auto; /* Let Plyr manage height based on aspect ratio */
203
+ max-height: 80vh; /* Prevent excessive height on tall screens */
204
+ background-color: #000;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  }
206
+
207
+ /* Plyr Customizations (Optional) */
208
+ :root {
209
+ --plyr-color-main: #007bff; /* Example: Blue theme */
 
 
 
 
 
 
 
 
210
  }
211
+
212
  .no-videos {
213
  padding: 20px;
214
  text-align: center;
 
219
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
220
  }
221
  .popup-content {
222
+ width: 98%; /* Almost full width on mobile */
223
+ max-width: none;
224
+ height: auto;
 
 
 
 
 
 
225
  }
226
+ #videoPlayer {
227
+ max-height: 85vh; /* Adjust max height for mobile */
228
+ }
229
+ .popup-title {
230
+ font-size: 1em;
231
+ }
232
  }
233
  </style>
234
  </head>
235
  <body>
236
  <div class="container">
237
  <div class="header">
238
+ <h1>HF Video Library <small>(Plyr Enhanced)</small></h1>
239
  </div>
240
+
241
  <div class="search-container">
242
  <input type="text" id="search" placeholder="Search videos...">
243
  </div>
244
+
245
  <div class="video-list">
246
  <div class="video-grid" id="videoGrid">
247
+ <!-- Video items will be loaded here -->
248
  <div class="video-item">
249
  <div class="video-thumbnail">Loading...</div>
250
  </div>
 
257
  <div class="popup-overlay" id="playerPopup">
258
  <div class="popup-content">
259
  <button class="close-popup" id="closePopup">✕</button>
 
 
260
  <div class="popup-title" id="currentTitle">Video Title</div>
261
+ <!-- The video element Plyr will enhance -->
262
+ <video id="videoPlayer" playsinline controls preload="metadata">
263
+ <!-- preload="metadata" is often a good balance -->
264
+ Your browser does not support the video tag.
265
+ </video>
266
+ <!-- Custom controls and instructions are removed - Plyr provides them -->
 
 
 
 
 
 
 
 
 
 
 
267
  </div>
268
  </div>
269
 
270
+ <!-- Plyr JS -->
271
+ <script src="https://cdn.plyr.io/3.7.8/plyr.js"></script>
272
+
273
  <script>
274
  // Global variables
275
  let videoData = [];
276
  let filteredVideos = [];
277
  let currentVideo = null;
278
  let currentPage = 1;
279
+ const videosPerPage = 12; // Adjusted for potentially smaller cards
280
+ let player = null; // To hold the Plyr instance
281
+
 
282
  // DOM Elements
283
  const videoGrid = document.getElementById('videoGrid');
284
  const pagination = document.getElementById('pagination');
285
  const searchInput = document.getElementById('search');
286
  const playerPopup = document.getElementById('playerPopup');
287
+ const videoPlayerElement = document.getElementById('videoPlayer'); // Get the element
288
  const currentTitle = document.getElementById('currentTitle');
289
  const closePopup = document.getElementById('closePopup');
290
+
291
+ // --- Fetching, Rendering Grid, Pagination (Keep mostly the same) ---
292
+
 
 
 
 
 
 
 
293
  async function fetchVideoData() {
294
  try {
295
+ // Show initial loading state
296
+ videoGrid.innerHTML = '<div class="no-videos">Loading video list...</div>';
297
+ pagination.innerHTML = '';
298
+
299
+ const response = await fetch('database.json'); // Ensure this path is correct
300
  if (!response.ok) {
301
+ throw new Error(`Failed to fetch video data (Status: ${response.status})`);
302
  }
303
  const data = await response.json();
304
+
305
+ videoData = Array.isArray(data) ? data :
 
306
  (data.videos && Array.isArray(data.videos) ? data.videos : []);
307
+
308
+ // Optimize HF URLs (Keep this logic)
309
  videoData.forEach(video => {
310
+ if (video.url && video.url.includes('huggingface.co')) {
311
+ let url = new URL(video.url);
312
+ // Ensure download=true is present without duplicating query params
313
+ if(url.searchParams.get('download') !== 'true') {
314
+ url.searchParams.set('download', 'true');
 
 
315
  }
316
+ video.url = url.toString();
317
+ }
318
+ // Basic validation
319
+ if (!video.title || !video.url) {
320
+ console.warn("Skipping video with missing title or URL:", video);
321
  }
322
  });
323
+
324
+ // Filter out invalid entries before proceeding
325
+ videoData = videoData.filter(video => video.title && video.url);
326
+
327
+
328
+ if (videoData.length === 0) {
329
+ videoGrid.innerHTML = '<div class="no-videos">No valid videos found in database.json</div>';
330
+ return;
331
+ }
332
+
333
  filteredVideos = [...videoData];
334
+ currentPage = 1; // Reset to page 1
335
  renderPagination();
336
  renderVideoGrid();
337
  } catch (error) {
338
+ console.error('Error fetching or processing video data:', error);
339
+ videoGrid.innerHTML = `<div class="no-videos">Error loading videos: ${error.message}. Check console.</div>`;
340
  }
341
  }
342
 
 
343
  function renderVideoGrid() {
344
+ videoGrid.innerHTML = ''; // Clear previous items
345
+
346
  if (filteredVideos.length === 0) {
347
+ videoGrid.innerHTML = '<div class="no-videos">No videos match your search.</div>';
348
+ // Don't clear pagination if there are videos overall, just no search results
349
+ if (videoData.length > 0 && searchInput.value) {
350
+ renderPagination(); // Keep pagination visible
351
+ } else {
352
+ pagination.innerHTML = ''; // Hide if no videos at all
353
+ }
354
  return;
355
  }
356
 
 
357
  const startIndex = (currentPage - 1) * videosPerPage;
358
  const endIndex = Math.min(startIndex + videosPerPage, filteredVideos.length);
359
  const currentVideos = filteredVideos.slice(startIndex, endIndex);
360
 
361
+ const fragment = document.createDocumentFragment();
362
+ currentVideos.forEach((video, index) => {
363
+ const videoItem = document.createElement('div');
364
+ videoItem.className = 'video-item';
365
+ videoItem.setAttribute('data-index', startIndex + index);
366
+ videoItem.innerHTML = `
367
  <div class="video-thumbnail">
368
  <div class="play-icon">▶</div>
369
+ ${video.thumbnail ? `<img src="${video.thumbnail}" alt="${video.title} thumbnail" loading="lazy">` : ''}
370
  </div>
371
+ <div class="video-item-title" title="${video.title}">${video.title}</div>
372
+ `;
373
+ videoItem.addEventListener('click', function() {
374
+ const dataIndex = parseInt(this.getAttribute('data-index'));
375
+ if (!isNaN(dataIndex) && dataIndex >= 0 && dataIndex < filteredVideos.length) {
376
+ playVideo(filteredVideos[dataIndex]);
377
+ } else {
378
+ console.error("Invalid video index clicked:", dataIndex);
 
 
379
  }
380
  });
381
+ fragment.appendChild(videoItem);
382
  });
383
+
384
+ videoGrid.appendChild(fragment);
385
  }
386
 
 
387
  function renderPagination() {
388
+ const totalPages = Math.ceil(filteredVideos.length / videosPerPage);
389
+ pagination.innerHTML = ''; // Clear existing buttons
390
+
391
  if (totalPages <= 1) {
392
+ return; // No pagination needed for 0 or 1 page
 
393
  }
394
 
 
395
  let buttons = [];
396
+ const maxVisibleButtons = 7; // Max buttons like 1 ... 5 6 7 ... 10
397
+
398
+ if (totalPages <= maxVisibleButtons) {
399
  for (let i = 1; i <= totalPages; i++) {
400
  buttons.push(i);
401
  }
402
  } else {
403
+ buttons.push(1); // Always show first page
404
+ let startPage, endPage;
405
+
406
+ if (currentPage <= Math.floor(maxVisibleButtons / 2) + 1) {
407
+ // Near the beginning
408
+ startPage = 2;
409
+ endPage = maxVisibleButtons - 2; // 1 + (max-2 pages) + ... + last
410
+ for (let i = startPage; i <= endPage; i++) buttons.push(i);
411
+ buttons.push('...');
412
+ } else if (currentPage >= totalPages - Math.floor(maxVisibleButtons / 2)) {
413
+ // Near the end
414
+ startPage = totalPages - (maxVisibleButtons - 3); // first + ... + (max-2 pages) + last
 
 
 
 
 
415
  buttons.push('...');
416
+ for (let i = startPage; i < totalPages; i++) buttons.push(i);
417
+ } else {
418
+ // In the middle
419
+ buttons.push('...');
420
+ const sideButtons = Math.floor((maxVisibleButtons - 3) / 2); // Buttons around current
421
+ startPage = currentPage - sideButtons;
422
+ endPage = currentPage + sideButtons;
423
+ for (let i = startPage; i <= endPage; i++) buttons.push(i);
424
+ buttons.push('...');
425
  }
426
+ buttons.push(totalPages); // Always show last page
 
 
427
  }
428
+
429
+ // Create pagination HTML
430
  pagination.innerHTML = buttons.map(button => {
431
  if (button === '...') {
432
+ return '<span class="page-btn disabled">...</span>'; // Non-clickable ellipsis
433
  }
434
  return `<button class="page-btn ${button === currentPage ? 'active' : ''}" data-page="${button}">${button}</button>`;
435
  }).join('');
436
+
437
  // Add click event listeners to page buttons
438
  document.querySelectorAll('.page-btn[data-page]').forEach(button => {
439
  button.addEventListener('click', function() {
440
+ const newPage = parseInt(this.getAttribute('data-page'));
441
+ if (newPage !== currentPage) {
442
+ currentPage = newPage;
443
+ renderPagination(); // Update buttons appearance
444
+ renderVideoGrid(); // Render new page content
445
+ window.scrollTo({ top: videoGrid.offsetTop - 20, behavior: 'smooth' }); // Scroll near top of grid
446
+ }
447
  });
448
  });
449
  }
450
 
451
+
452
+ // --- Player Logic (Using Plyr) ---
453
+
454
  function playVideo(video) {
455
+ if (!video || !video.url) {
456
+ console.error("Cannot play video: Invalid video data provided.");
457
+ return;
458
+ }
459
  currentVideo = video;
460
  currentTitle.textContent = video.title;
461
+ currentTitle.setAttribute('title', video.title); // Add tooltip for long titles
462
+
463
+ // Set the source for the video element
464
+ videoPlayerElement.src = video.url;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
465
 
466
+ // Destroy previous Plyr instance if it exists
467
+ if (player) {
468
+ try {
469
+ player.destroy();
470
+ } catch (e) {
471
+ console.warn("Error destroying previous Plyr instance:", e);
472
  }
473
+ player = null;
 
 
 
474
  }
 
 
 
475
 
476
+ // Initialize Plyr
 
 
 
 
 
 
 
 
 
 
477
  try {
478
+ player = new Plyr(videoPlayerElement, {
479
+ // Plyr options (optional)
480
+ title: video.title,
481
+ controls: ['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen'],
482
+ settings: ['captions', 'quality', 'speed', 'loop'],
483
+ // quality: {
484
+ // // If you were using HLS/DASH, you'd configure quality options here
485
+ // default: 720, // Example default quality
486
+ // options: [1080, 720, 480]
487
+ // },
488
+ // tooltips: { controls: true, seek: true } // Enable tooltips
489
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
490
 
491
+ // Add event listener for when player is ready
492
+ player.on('ready', () => {
493
+ // Autoplay (optional, might be blocked by browsers)
494
+ // player.play().catch(e => console.log("Autoplay prevented:", e));
495
  });
 
 
 
 
496
 
497
+ // Add event listener for errors
498
+ player.on('error', event => {
499
+ console.error('Plyr error:', event.detail.plyr.source, event);
500
+ // Maybe display a user-friendly error message
501
+ });
502
+
503
+ // Show the popup
504
+ playerPopup.classList.add('active');
505
+ document.body.style.overflow = 'hidden'; // Prevent background scroll
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
506
 
507
+ } catch (error) {
508
+ console.error("Failed to initialize Plyr:", error);
509
+ // Fallback or error message
510
+ alert(`Error initializing video player for "${video.title}". Check console for details.`);
511
+ // Still try to show the basic video element if Plyr fails?
512
+ videoPlayerElement.load(); // Try loading without Plyr
513
+ playerPopup.classList.add('active');
514
+ document.body.style.overflow = 'hidden';
515
+ }
516
  }
517
 
 
518
  function closePlayerPopup() {
519
+ if (player) {
520
+ try {
521
+ player.pause(); // Pause before destroying
522
+ player.destroy();
523
+ } catch (e) {
524
+ console.warn("Error destroying Plyr instance on close:", e)
525
+ }
526
+ player = null;
527
+ }
528
+ // Fallback if player wasn't initialized or destroyed properly
529
+ videoPlayerElement.pause();
530
+ videoPlayerElement.removeAttribute('src'); // Remove src to stop potential loading
531
+ videoPlayerElement.load(); // Reset the media element
532
+
533
  playerPopup.classList.remove('active');
534
+ document.body.style.overflow = ''; // Restore scroll
535
+ currentVideo = null;
536
  }
537
 
538
+
539
+ // --- Event Listeners (Search, Popup Close) ---
540
+
541
  searchInput.addEventListener('input', function() {
542
+ const searchTerm = this.value.toLowerCase().trim();
543
+ filteredVideos = videoData.filter(video =>
544
  video.title.toLowerCase().includes(searchTerm)
545
  );
546
  currentPage = 1; // Reset to first page on search
 
548
  renderVideoGrid();
549
  });
550
 
 
551
  closePopup.addEventListener('click', closePlayerPopup);
552
 
 
553
  playerPopup.addEventListener('click', function(e) {
554
+ // Close only if clicking the overlay itself, not the content inside
555
  if (e.target === playerPopup) {
556
  closePlayerPopup();
557
  }
558
  });
559
 
560
+ // Close popup with Escape key (Plyr might handle this, but good fallback)
561
  document.addEventListener('keydown', function(e) {
562
+ if (e.key === 'Escape' && playerPopup.classList.contains('active')) {
563
+ closePlayerPopup();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
564
  }
565
  });
566
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
567
 
568
+ // --- Initial Load ---
569
  fetchVideoData();
570
+
571
  </script>
572
  </body>
573
  </html>