custom-controls.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  1. // @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
  2. /**
  3. * CustomVideoControls object to handle custom video player functionality
  4. */
  5. const CustomVideoControls = {
  6. video: null,
  7. controls: null,
  8. topControls: null,
  9. playPauseBtn: null,
  10. seekSlider: null,
  11. progressBar: null,
  12. progressFilled: null,
  13. progressBuffered: null,
  14. timeDisplay: null,
  15. muteBtn: null,
  16. volumeSlider: null,
  17. fullscreenBtn: null,
  18. controlsTimeout: null,
  19. mouseTimeout: null,
  20. container: null,
  21. isMouseOverContainer: false,
  22. /**
  23. * Initialize the custom video controls
  24. */
  25. init() {
  26. // Get DOM elements
  27. this.video = document.getElementById('player');
  28. this.controls = document.getElementById('custom-controls');
  29. this.topControls = document.getElementById('controls');
  30. this.playPauseBtn = document.getElementById('play-pause-btn');
  31. this.seekSlider = document.getElementById('seek-slider');
  32. this.progressBar = document.getElementById('progress-bar');
  33. this.progressFilled = document.getElementById('progress-filled');
  34. this.progressBuffered = document.getElementById('progress-buffered');
  35. this.timeDisplay = document.getElementById('time-display');
  36. this.muteBtn = document.getElementById('mute-btn');
  37. this.volumeSlider = document.getElementById('volume-slider');
  38. this.fullscreenBtn = document.getElementById('fullscreen-btn');
  39. if (!this.video || !this.controls) {
  40. console.error('Custom video controls: Required elements not found');
  41. return;
  42. }
  43. // Create a container for video and controls if it doesn't exist
  44. this.createVideoContainer();
  45. // Set up event listeners
  46. this.setupEventListeners();
  47. // Initialize controls state
  48. this.updatePlayPauseButton();
  49. this.updateVolume();
  50. this.updateTimeDisplay();
  51. // Show controls initially
  52. this.showControls();
  53. // Set up mouse movement detection for auto-hiding controls
  54. this.setupAutoHide();
  55. // Set up mouse inactivity detection
  56. this.setupMouseInactivityDetection();
  57. // Set up focus handling for top controls
  58. this.setupTopControlsFocusHandling();
  59. },
  60. /**
  61. * Create a container wrapper for video and controls for proper fullscreen handling
  62. */
  63. createVideoContainer() {
  64. // Container already exists in HTML, just get reference
  65. this.container = document.getElementById('video-container');
  66. if (!this.container) {
  67. console.error('Video container not found in HTML');
  68. return;
  69. }
  70. },
  71. /**
  72. * Set up all event listeners for the custom controls
  73. */
  74. setupEventListeners() {
  75. // Play/Pause button
  76. this.playPauseBtn.addEventListener('click', () => {
  77. this.togglePlayPause();
  78. this.playPauseBtn.blur();
  79. });
  80. // Seek slider
  81. this.seekSlider.addEventListener('input', () => {
  82. this.seek();
  83. });
  84. this.seekSlider.addEventListener('change', () => {
  85. this.seekSlider.blur();
  86. });
  87. // Progress bar click to seek
  88. this.progressBar.addEventListener('click', (e) => {
  89. this.progressBarSeek(e);
  90. e.target.blur();
  91. });
  92. // Mute button
  93. this.muteBtn.addEventListener('click', () => {
  94. this.toggleMute();
  95. this.muteBtn.blur();
  96. });
  97. // Volume slider
  98. this.volumeSlider.addEventListener('input', () => {
  99. this.updateVolume();
  100. });
  101. this.volumeSlider.addEventListener('change', () => {
  102. this.volumeSlider.blur();
  103. });
  104. // Fullscreen button
  105. this.fullscreenBtn.addEventListener('click', () => {
  106. this.toggleFullscreen();
  107. this.fullscreenBtn.blur();
  108. });
  109. // Video click to toggle play/pause
  110. this.video.addEventListener('click', (e) => {
  111. // Prevent triggering if clicking on controls or if it's a right-click
  112. if (e.target === this.video && e.button === 0) {
  113. this.togglePlayPause();
  114. }
  115. });
  116. // Video events
  117. this.video.addEventListener('timeupdate', () => this.updateProgress());
  118. this.video.addEventListener('loadedmetadata', () => this.updateTimeDisplay());
  119. this.video.addEventListener('volumechange', () => this.updateVolumeDisplay());
  120. this.video.addEventListener('play', () => {
  121. this.updatePlayPauseButton();
  122. this.hideControlsWithTimeout(); // Auto-hide controls when playing
  123. this.hideCursorWithTimeout(); // Auto-hide cursor when playing
  124. });
  125. this.video.addEventListener('pause', () => {
  126. this.updatePlayPauseButton();
  127. this.showControls(); // Show controls when paused
  128. this.showCursor(); // Show cursor when paused
  129. this.resetMouseTimeout(); // Clear cursor timeout when paused
  130. });
  131. this.video.addEventListener('seeking', () => {
  132. this.showControls(); // Show controls when seeking
  133. });
  134. this.video.addEventListener('seeked', () => {
  135. this.hideControlsWithTimeout(); // Auto-hide after seeking
  136. this.hideCursorWithTimeout(); // Auto-hide cursor after seeking
  137. });
  138. // Keyboard controls
  139. document.addEventListener('keydown', (e) => this.handleKeyboard(e));
  140. // Controls visibility
  141. this.controls.addEventListener('mouseenter', () => this.showControls());
  142. this.controls.addEventListener('mouseleave', () => this.hideControls());
  143. this.video.addEventListener('mouseenter', () => this.showControls());
  144. this.video.addEventListener('mouseleave', () => this.hideControls());
  145. this.video.addEventListener('mousemove', () => this.showControls());
  146. // Add mouse movement detection for the entire container
  147. if (this.container) {
  148. this.container.addEventListener('mousemove', () => this.handleMouseMove());
  149. this.container.addEventListener('mouseleave', () => this.handleMouseLeave());
  150. this.container.addEventListener('mouseenter', () => this.handleMouseEnter());
  151. }
  152. // Touch events for mobile support
  153. this.video.addEventListener('touchstart', (e) => {
  154. this.showControls();
  155. // Prevent default to avoid interfering with video controls
  156. if (e.touches.length === 1) {
  157. const touch = e.touches[0];
  158. const rect = this.video.getBoundingClientRect();
  159. // Only toggle play/pause if tapping in the center area
  160. if (touch.clientX > rect.left + rect.width * 0.3 &&
  161. touch.clientX < rect.left + rect.width * 0.7 &&
  162. touch.clientY > rect.top + rect.height * 0.3 &&
  163. touch.clientY < rect.top + rect.height * 0.7) {
  164. this.togglePlayPause();
  165. }
  166. }
  167. });
  168. this.controls.addEventListener('touchstart', () => {
  169. this.showControls();
  170. });
  171. // Handle window resize for responsive behavior
  172. window.addEventListener('resize', () => {
  173. this.handleResize();
  174. });
  175. // Fullscreen change events
  176. document.addEventListener('fullscreenchange', () => this.handleFullscreenChange());
  177. document.addEventListener('webkitfullscreenchange', () => this.handleFullscreenChange());
  178. document.addEventListener('mozfullscreenchange', () => this.handleFullscreenChange());
  179. document.addEventListener('MSFullscreenChange', () => this.handleFullscreenChange());
  180. },
  181. /**
  182. * Toggle between play and pause
  183. */
  184. togglePlayPause() {
  185. if (this.video.paused) {
  186. this.video.play();
  187. } else {
  188. this.video.pause();
  189. }
  190. },
  191. /**
  192. * Update the play/pause button display
  193. */
  194. updatePlayPauseButton() {
  195. if (this.video.paused) {
  196. this.playPauseBtn.textContent = '▶';
  197. this.playPauseBtn.setAttribute('aria-label', 'Play');
  198. } else {
  199. this.playPauseBtn.textContent = '❚❚';
  200. this.playPauseBtn.setAttribute('aria-label', 'Pause');
  201. }
  202. },
  203. /**
  204. * Seek to the position specified by the seek slider
  205. */
  206. seek() {
  207. const seekTime = this.seekSlider.value * this.video.duration / 100;
  208. this.video.currentTime = seekTime;
  209. },
  210. /**
  211. * Handle clicking on the progress bar to seek
  212. */
  213. progressBarSeek(e) {
  214. const rect = this.progressBar.getBoundingClientRect();
  215. const pos = (e.clientX - rect.left) / rect.width;
  216. this.video.currentTime = pos * this.video.duration;
  217. },
  218. /**
  219. * Update the progress bar and time display
  220. */
  221. updateProgress() {
  222. const percentage = (this.video.currentTime / this.video.duration) * 100;
  223. this.progressFilled.style.width = `${percentage}%`;
  224. this.seekSlider.value = percentage;
  225. // Update buffer indicator
  226. if (this.video.buffered.length > 0) {
  227. const bufferedEnd = this.video.buffered.end(this.video.buffered.length - 1);
  228. const bufferedPercentage = (bufferedEnd / this.video.duration) * 100;
  229. this.progressBuffered.style.width = `${bufferedPercentage}%`;
  230. }
  231. this.updateTimeDisplay();
  232. },
  233. /**
  234. * Update the time display
  235. */
  236. updateTimeDisplay() {
  237. const currentTime = this.formatTime(this.video.currentTime);
  238. const duration = this.formatTime(this.video.duration);
  239. this.timeDisplay.textContent = `${currentTime} / ${duration}`;
  240. },
  241. /**
  242. * Format time in seconds to MM:SS or HH:MM:SS format
  243. */
  244. formatTime(seconds) {
  245. if (isNaN(seconds)) return '0:00';
  246. const h = Math.floor(seconds / 3600);
  247. const m = Math.floor((seconds % 3600) / 60);
  248. const s = Math.floor(seconds % 60);
  249. if (h > 0) {
  250. return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
  251. } else {
  252. return `${m}:${s.toString().padStart(2, '0')}`;
  253. }
  254. },
  255. /**
  256. * Toggle mute state
  257. */
  258. toggleMute() {
  259. this.video.muted = !this.video.muted;
  260. },
  261. /**
  262. * Update volume based on slider value
  263. */
  264. updateVolume() {
  265. this.video.volume = this.volumeSlider.value;
  266. },
  267. /**
  268. * Update volume display (mute button)
  269. */
  270. updateVolumeDisplay() {
  271. if (this.video.muted || this.video.volume === 0) {
  272. this.muteBtn.textContent = '🔇';
  273. this.muteBtn.setAttribute('aria-label', 'Unmute');
  274. } else if (this.video.volume < 0.5) {
  275. this.muteBtn.textContent = '🔉';
  276. this.muteBtn.setAttribute('aria-label', 'Mute');
  277. } else {
  278. this.muteBtn.textContent = '🔊';
  279. this.muteBtn.setAttribute('aria-label', 'Mute');
  280. }
  281. },
  282. /**
  283. * Toggle fullscreen mode
  284. */
  285. toggleFullscreen() {
  286. if (!document.fullscreenElement) {
  287. // Request fullscreen for the container instead of just the video
  288. if (this.container.requestFullscreen) {
  289. this.container.requestFullscreen();
  290. } else if (this.container.webkitRequestFullscreen) { /* Safari */
  291. this.container.webkitRequestFullscreen();
  292. } else if (this.container.mozRequestFullScreen) { /* Firefox */
  293. this.container.mozRequestFullScreen();
  294. } else if (this.container.msRequestFullscreen) { /* IE11 */
  295. this.container.msRequestFullscreen();
  296. }
  297. } else {
  298. if (document.exitFullscreen) {
  299. document.exitFullscreen();
  300. } else if (document.webkitExitFullscreen) { /* Safari */
  301. document.webkitExitFullscreen();
  302. } else if (document.mozCancelFullScreen) { /* Firefox */
  303. document.mozCancelFullScreen();
  304. } else if (document.msExitFullscreen) { /* IE11 */
  305. document.msExitFullscreen();
  306. }
  307. }
  308. },
  309. /**
  310. * Handle fullscreen change events
  311. */
  312. handleFullscreenChange() {
  313. const isFullscreen = !!(document.fullscreenElement ||
  314. document.webkitFullscreenElement ||
  315. document.mozFullScreenElement ||
  316. document.msFullscreenElement);
  317. if (isFullscreen) {
  318. // Adjust controls for fullscreen
  319. this.controls.style.position = 'absolute';
  320. this.controls.style.bottom = '0';
  321. this.controls.style.left = '0';
  322. this.controls.style.right = '0';
  323. this.controls.style.zIndex = '2147483647'; // Maximum z-index
  324. // Make video fill the container
  325. this.video.style.width = '100%';
  326. this.video.style.height = '100%';
  327. this.video.style.objectFit = 'contain';
  328. } else {
  329. // Reset styles when exiting fullscreen
  330. this.controls.style.position = '';
  331. this.controls.style.bottom = '';
  332. this.controls.style.left = '';
  333. this.controls.style.right = '';
  334. this.controls.style.zIndex = '';
  335. this.video.style.width = '';
  336. this.video.style.height = '';
  337. this.video.style.objectFit = '';
  338. }
  339. // Always show controls when entering/exiting fullscreen
  340. this.showControls();
  341. // Auto-hide controls and cursor after a delay in fullscreen mode if video is playing
  342. if (isFullscreen && !this.video.paused) {
  343. this.hideControlsWithTimeout();
  344. this.hideCursorWithTimeout();
  345. }
  346. },
  347. /**
  348. * Handle keyboard controls
  349. */
  350. handleKeyboard(e) {
  351. // Only handle keys when video is the active element or no input is focused
  352. if (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA') {
  353. return;
  354. }
  355. switch (e.key) {
  356. case ' ':
  357. case 'Spacebar':
  358. e.preventDefault();
  359. this.togglePlayPause();
  360. break;
  361. case 'ArrowLeft':
  362. e.preventDefault();
  363. this.video.currentTime -= 5;
  364. break;
  365. case 'ArrowRight':
  366. e.preventDefault();
  367. this.video.currentTime += 5;
  368. break;
  369. case 'ArrowUp':
  370. e.preventDefault();
  371. this.video.volume = Math.min(1, this.video.volume + 0.1);
  372. this.volumeSlider.value = this.video.volume;
  373. break;
  374. case 'ArrowDown':
  375. e.preventDefault();
  376. this.video.volume = Math.max(0, this.video.volume - 0.1);
  377. this.volumeSlider.value = this.video.volume;
  378. break;
  379. case 'm':
  380. case 'M':
  381. e.preventDefault();
  382. this.toggleMute();
  383. break;
  384. case 'f':
  385. case 'F':
  386. e.preventDefault();
  387. this.toggleFullscreen();
  388. break;
  389. }
  390. },
  391. /**
  392. * Show the controls
  393. */
  394. showControls() {
  395. this.controls.classList.add('show');
  396. if (this.topControls) {
  397. this.topControls.classList.add('show');
  398. }
  399. this.showCursor();
  400. // Only reset timeout if video is playing (to allow auto-hide when idle)
  401. if (!this.video.paused) {
  402. this.resetControlsTimeout();
  403. this.hideControlsWithTimeout();
  404. this.resetMouseTimeout();
  405. this.hideCursorWithTimeout();
  406. }
  407. },
  408. /**
  409. * Hide the controls
  410. */
  411. hideControls() {
  412. if (!this.video.paused) {
  413. this.controls.classList.remove('show');
  414. if (this.topControls) {
  415. this.topControls.classList.remove('show');
  416. }
  417. this.hideCursor();
  418. }
  419. },
  420. /**
  421. * Show the cursor
  422. */
  423. showCursor() {
  424. if (this.container) {
  425. this.container.style.cursor = 'auto';
  426. }
  427. },
  428. /**
  429. * Hide the cursor
  430. */
  431. hideCursor() {
  432. if (this.container && !this.video.paused) {
  433. this.container.style.cursor = 'none';
  434. }
  435. },
  436. /**
  437. * Hide cursor with a timeout
  438. */
  439. hideCursorWithTimeout() {
  440. this.resetMouseTimeout();
  441. this.mouseTimeout = setTimeout(() => {
  442. this.hideCursor();
  443. }, 3000); // 3 seconds
  444. },
  445. /**
  446. * Reset the mouse timeout
  447. */
  448. resetMouseTimeout() {
  449. if (this.mouseTimeout) {
  450. clearTimeout(this.mouseTimeout);
  451. this.mouseTimeout = null;
  452. }
  453. },
  454. /**
  455. * Handle mouse movement inside the container
  456. */
  457. handleMouseMove() {
  458. this.showControls();
  459. if (!this.video.paused) {
  460. this.resetMouseTimeout();
  461. this.hideCursorWithTimeout();
  462. }
  463. },
  464. /**
  465. * Handle mouse entering the container
  466. */
  467. handleMouseEnter() {
  468. this.isMouseOverContainer = true;
  469. this.showControls();
  470. },
  471. /**
  472. * Handle mouse leaving the container
  473. */
  474. handleMouseLeave() {
  475. this.isMouseOverContainer = false;
  476. this.hideControls();
  477. },
  478. /**
  479. * Hide controls with a timeout
  480. */
  481. hideControlsWithTimeout() {
  482. this.resetControlsTimeout();
  483. this.controlsTimeout = setTimeout(() => {
  484. this.hideControls();
  485. }, 3000); // Changed to 3 seconds
  486. },
  487. /**
  488. * Reset the controls timeout
  489. */
  490. resetControlsTimeout() {
  491. if (this.controlsTimeout) {
  492. clearTimeout(this.controlsTimeout);
  493. this.controlsTimeout = null;
  494. }
  495. },
  496. /**
  497. * Set up auto-hide functionality for controls
  498. */
  499. setupAutoHide() {
  500. // Note: These event listeners are already added in setupEventListeners()
  501. // so we don't need to add them again here to prevent duplicate execution
  502. },
  503. /**
  504. * Set up mouse inactivity detection
  505. */
  506. setupMouseInactivityDetection() {
  507. // Initial state - show cursor
  508. this.showCursor();
  509. // Set up initial timeout if video is playing
  510. if (!this.video.paused) {
  511. this.hideCursorWithTimeout();
  512. }
  513. },
  514. /**
  515. * Handle window resize events
  516. */
  517. handleResize() {
  518. // Show controls during resize to ensure they're visible
  519. this.showControls();
  520. // Auto-hide after resize is complete
  521. clearTimeout(this.resizeTimeout);
  522. this.resizeTimeout = setTimeout(() => {
  523. if (!this.video.paused) {
  524. this.hideControlsWithTimeout();
  525. this.hideCursorWithTimeout();
  526. }
  527. }, 1000);
  528. },
  529. /**
  530. * Set up focus handling for top controls
  531. */
  532. setupTopControlsFocusHandling() {
  533. // Get top control buttons and select
  534. const infoBtn = document.querySelector('button[onclick="showInfo()"]');
  535. const shareBtn = document.querySelector('button[onclick="shareVideo()"]');
  536. const downloadBtn = document.querySelector('button[onclick="downloadVideo()"]');
  537. const qualitySelector = document.getElementById('quality-selector');
  538. // Add blur() calls to top control buttons
  539. if (infoBtn) {
  540. infoBtn.addEventListener('click', () => {
  541. infoBtn.blur();
  542. });
  543. }
  544. if (shareBtn) {
  545. shareBtn.addEventListener('click', () => {
  546. shareBtn.blur();
  547. });
  548. }
  549. if (downloadBtn) {
  550. downloadBtn.addEventListener('click', () => {
  551. downloadBtn.blur();
  552. });
  553. }
  554. if (qualitySelector) {
  555. qualitySelector.addEventListener('change', () => {
  556. qualitySelector.blur();
  557. });
  558. }
  559. },
  560. };
  561. // @license-end