custom-controls.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  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. container: null,
  20. /**
  21. * Initialize the custom video controls
  22. */
  23. init() {
  24. // Get DOM elements
  25. this.video = document.getElementById('player');
  26. this.controls = document.getElementById('custom-controls');
  27. this.topControls = document.getElementById('controls');
  28. this.playPauseBtn = document.getElementById('play-pause-btn');
  29. this.seekSlider = document.getElementById('seek-slider');
  30. this.progressBar = document.getElementById('progress-bar');
  31. this.progressFilled = document.getElementById('progress-filled');
  32. this.progressBuffered = document.getElementById('progress-buffered');
  33. this.timeDisplay = document.getElementById('time-display');
  34. this.muteBtn = document.getElementById('mute-btn');
  35. this.volumeSlider = document.getElementById('volume-slider');
  36. this.fullscreenBtn = document.getElementById('fullscreen-btn');
  37. if (!this.video || !this.controls) {
  38. console.error('Custom video controls: Required elements not found');
  39. return;
  40. }
  41. // Create a container for video and controls if it doesn't exist
  42. this.createVideoContainer();
  43. // Set up event listeners
  44. this.setupEventListeners();
  45. // Initialize controls state
  46. this.updatePlayPauseButton();
  47. this.updateVolume();
  48. this.updateTimeDisplay();
  49. // Show controls initially
  50. this.showControls();
  51. // Set up mouse movement detection for auto-hiding controls
  52. this.setupAutoHide();
  53. },
  54. /**
  55. * Create a container wrapper for video and controls for proper fullscreen handling
  56. */
  57. createVideoContainer() {
  58. // Container already exists in HTML, just get reference
  59. this.container = document.getElementById('video-container');
  60. if (!this.container) {
  61. console.error('Video container not found in HTML');
  62. return;
  63. }
  64. },
  65. /**
  66. * Set up all event listeners for the custom controls
  67. */
  68. setupEventListeners() {
  69. // Play/Pause button
  70. this.playPauseBtn.addEventListener('click', () => this.togglePlayPause());
  71. // Seek slider
  72. this.seekSlider.addEventListener('input', () => this.seek());
  73. // Progress bar click to seek
  74. this.progressBar.addEventListener('click', (e) => this.progressBarSeek(e));
  75. // Mute button
  76. this.muteBtn.addEventListener('click', () => this.toggleMute());
  77. // Volume slider
  78. this.volumeSlider.addEventListener('input', () => this.updateVolume());
  79. // Fullscreen button
  80. this.fullscreenBtn.addEventListener('click', () => this.toggleFullscreen());
  81. // Video click to toggle play/pause
  82. this.video.addEventListener('click', (e) => {
  83. // Prevent triggering if clicking on controls or if it's a right-click
  84. if (e.target === this.video && e.button === 0) {
  85. this.togglePlayPause();
  86. }
  87. });
  88. // Video events
  89. this.video.addEventListener('timeupdate', () => this.updateProgress());
  90. this.video.addEventListener('loadedmetadata', () => this.updateTimeDisplay());
  91. this.video.addEventListener('volumechange', () => this.updateVolumeDisplay());
  92. this.video.addEventListener('play', () => {
  93. this.updatePlayPauseButton();
  94. this.hideControlsWithTimeout(); // Auto-hide controls when playing
  95. });
  96. this.video.addEventListener('pause', () => {
  97. this.updatePlayPauseButton();
  98. this.showControls(); // Show controls when paused
  99. });
  100. this.video.addEventListener('seeking', () => {
  101. this.showControls(); // Show controls when seeking
  102. });
  103. this.video.addEventListener('seeked', () => {
  104. this.hideControlsWithTimeout(); // Auto-hide after seeking
  105. });
  106. // Keyboard controls
  107. document.addEventListener('keydown', (e) => this.handleKeyboard(e));
  108. // Controls visibility
  109. this.controls.addEventListener('mouseenter', () => this.showControls());
  110. this.controls.addEventListener('mouseleave', () => this.hideControls());
  111. this.video.addEventListener('mouseenter', () => this.showControls());
  112. this.video.addEventListener('mouseleave', () => this.hideControls());
  113. this.video.addEventListener('mousemove', () => this.showControls());
  114. // Add mouse movement detection for the entire container
  115. if (this.container) {
  116. this.container.addEventListener('mousemove', () => this.showControls());
  117. this.container.addEventListener('mouseleave', () => this.hideControls());
  118. this.container.addEventListener('mouseenter', () => this.showControls());
  119. }
  120. // Touch events for mobile support
  121. this.video.addEventListener('touchstart', (e) => {
  122. this.showControls();
  123. // Prevent default to avoid interfering with video controls
  124. if (e.touches.length === 1) {
  125. const touch = e.touches[0];
  126. const rect = this.video.getBoundingClientRect();
  127. // Only toggle play/pause if tapping in the center area
  128. if (touch.clientX > rect.left + rect.width * 0.3 &&
  129. touch.clientX < rect.left + rect.width * 0.7 &&
  130. touch.clientY > rect.top + rect.height * 0.3 &&
  131. touch.clientY < rect.top + rect.height * 0.7) {
  132. this.togglePlayPause();
  133. }
  134. }
  135. });
  136. this.controls.addEventListener('touchstart', () => {
  137. this.showControls();
  138. });
  139. // Handle window resize for responsive behavior
  140. window.addEventListener('resize', () => {
  141. this.handleResize();
  142. });
  143. // Fullscreen change events
  144. document.addEventListener('fullscreenchange', () => this.handleFullscreenChange());
  145. document.addEventListener('webkitfullscreenchange', () => this.handleFullscreenChange());
  146. document.addEventListener('mozfullscreenchange', () => this.handleFullscreenChange());
  147. document.addEventListener('MSFullscreenChange', () => this.handleFullscreenChange());
  148. },
  149. /**
  150. * Toggle between play and pause
  151. */
  152. togglePlayPause() {
  153. if (this.video.paused) {
  154. this.video.play();
  155. } else {
  156. this.video.pause();
  157. }
  158. },
  159. /**
  160. * Update the play/pause button display
  161. */
  162. updatePlayPauseButton() {
  163. if (this.video.paused) {
  164. this.playPauseBtn.textContent = '▶';
  165. this.playPauseBtn.setAttribute('aria-label', 'Play');
  166. } else {
  167. this.playPauseBtn.textContent = '❚❚';
  168. this.playPauseBtn.setAttribute('aria-label', 'Pause');
  169. }
  170. },
  171. /**
  172. * Seek to the position specified by the seek slider
  173. */
  174. seek() {
  175. const seekTime = this.seekSlider.value * this.video.duration / 100;
  176. this.video.currentTime = seekTime;
  177. },
  178. /**
  179. * Handle clicking on the progress bar to seek
  180. */
  181. progressBarSeek(e) {
  182. const rect = this.progressBar.getBoundingClientRect();
  183. const pos = (e.clientX - rect.left) / rect.width;
  184. this.video.currentTime = pos * this.video.duration;
  185. },
  186. /**
  187. * Update the progress bar and time display
  188. */
  189. updateProgress() {
  190. const percentage = (this.video.currentTime / this.video.duration) * 100;
  191. this.progressFilled.style.width = `${percentage}%`;
  192. this.seekSlider.value = percentage;
  193. // Update buffer indicator
  194. if (this.video.buffered.length > 0) {
  195. const bufferedEnd = this.video.buffered.end(this.video.buffered.length - 1);
  196. const bufferedPercentage = (bufferedEnd / this.video.duration) * 100;
  197. this.progressBuffered.style.width = `${bufferedPercentage}%`;
  198. }
  199. this.updateTimeDisplay();
  200. },
  201. /**
  202. * Update the time display
  203. */
  204. updateTimeDisplay() {
  205. const currentTime = this.formatTime(this.video.currentTime);
  206. const duration = this.formatTime(this.video.duration);
  207. this.timeDisplay.textContent = `${currentTime} / ${duration}`;
  208. },
  209. /**
  210. * Format time in seconds to MM:SS or HH:MM:SS format
  211. */
  212. formatTime(seconds) {
  213. if (isNaN(seconds)) return '0:00';
  214. const h = Math.floor(seconds / 3600);
  215. const m = Math.floor((seconds % 3600) / 60);
  216. const s = Math.floor(seconds % 60);
  217. if (h > 0) {
  218. return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
  219. } else {
  220. return `${m}:${s.toString().padStart(2, '0')}`;
  221. }
  222. },
  223. /**
  224. * Toggle mute state
  225. */
  226. toggleMute() {
  227. this.video.muted = !this.video.muted;
  228. },
  229. /**
  230. * Update volume based on slider value
  231. */
  232. updateVolume() {
  233. this.video.volume = this.volumeSlider.value;
  234. },
  235. /**
  236. * Update volume display (mute button)
  237. */
  238. updateVolumeDisplay() {
  239. if (this.video.muted || this.video.volume === 0) {
  240. this.muteBtn.textContent = '🔇';
  241. this.muteBtn.setAttribute('aria-label', 'Unmute');
  242. } else if (this.video.volume < 0.5) {
  243. this.muteBtn.textContent = '🔉';
  244. this.muteBtn.setAttribute('aria-label', 'Mute');
  245. } else {
  246. this.muteBtn.textContent = '🔊';
  247. this.muteBtn.setAttribute('aria-label', 'Mute');
  248. }
  249. },
  250. /**
  251. * Toggle fullscreen mode
  252. */
  253. toggleFullscreen() {
  254. if (!document.fullscreenElement) {
  255. // Request fullscreen for the container instead of just the video
  256. if (this.container.requestFullscreen) {
  257. this.container.requestFullscreen();
  258. } else if (this.container.webkitRequestFullscreen) { /* Safari */
  259. this.container.webkitRequestFullscreen();
  260. } else if (this.container.mozRequestFullScreen) { /* Firefox */
  261. this.container.mozRequestFullScreen();
  262. } else if (this.container.msRequestFullscreen) { /* IE11 */
  263. this.container.msRequestFullscreen();
  264. }
  265. } else {
  266. if (document.exitFullscreen) {
  267. document.exitFullscreen();
  268. } else if (document.webkitExitFullscreen) { /* Safari */
  269. document.webkitExitFullscreen();
  270. } else if (document.mozCancelFullScreen) { /* Firefox */
  271. document.mozCancelFullScreen();
  272. } else if (document.msExitFullscreen) { /* IE11 */
  273. document.msExitFullscreen();
  274. }
  275. }
  276. },
  277. /**
  278. * Handle fullscreen change events
  279. */
  280. handleFullscreenChange() {
  281. const isFullscreen = !!(document.fullscreenElement ||
  282. document.webkitFullscreenElement ||
  283. document.mozFullScreenElement ||
  284. document.msFullscreenElement);
  285. if (isFullscreen) {
  286. // Adjust controls for fullscreen
  287. this.controls.style.position = 'absolute';
  288. this.controls.style.bottom = '0';
  289. this.controls.style.left = '0';
  290. this.controls.style.right = '0';
  291. this.controls.style.zIndex = '2147483647'; // Maximum z-index
  292. // Make video fill the container
  293. this.video.style.width = '100%';
  294. this.video.style.height = '100%';
  295. this.video.style.objectFit = 'contain';
  296. } else {
  297. // Reset styles when exiting fullscreen
  298. this.controls.style.position = '';
  299. this.controls.style.bottom = '';
  300. this.controls.style.left = '';
  301. this.controls.style.right = '';
  302. this.controls.style.zIndex = '';
  303. this.video.style.width = '';
  304. this.video.style.height = '';
  305. this.video.style.objectFit = '';
  306. }
  307. // Always show controls when entering/exiting fullscreen
  308. this.showControls();
  309. // Auto-hide controls after a delay in fullscreen mode if video is playing
  310. if (isFullscreen && !this.video.paused) {
  311. this.hideControlsWithTimeout();
  312. }
  313. },
  314. /**
  315. * Handle keyboard controls
  316. */
  317. handleKeyboard(e) {
  318. // Only handle keys when video is the active element or no input is focused
  319. if (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA') {
  320. return;
  321. }
  322. switch (e.key) {
  323. case ' ':
  324. case 'Spacebar':
  325. e.preventDefault();
  326. this.togglePlayPause();
  327. break;
  328. case 'ArrowLeft':
  329. e.preventDefault();
  330. this.video.currentTime -= 5;
  331. break;
  332. case 'ArrowRight':
  333. e.preventDefault();
  334. this.video.currentTime += 5;
  335. break;
  336. case 'ArrowUp':
  337. e.preventDefault();
  338. this.video.volume = Math.min(1, this.video.volume + 0.1);
  339. this.volumeSlider.value = this.video.volume;
  340. break;
  341. case 'ArrowDown':
  342. e.preventDefault();
  343. this.video.volume = Math.max(0, this.video.volume - 0.1);
  344. this.volumeSlider.value = this.video.volume;
  345. break;
  346. case 'm':
  347. case 'M':
  348. e.preventDefault();
  349. this.toggleMute();
  350. break;
  351. case 'f':
  352. case 'F':
  353. e.preventDefault();
  354. this.toggleFullscreen();
  355. break;
  356. }
  357. },
  358. /**
  359. * Show the controls
  360. */
  361. showControls() {
  362. this.controls.classList.add('show');
  363. if (this.topControls) {
  364. this.topControls.classList.add('show');
  365. }
  366. // Only reset timeout if video is playing (to allow auto-hide when idle)
  367. if (!this.video.paused) {
  368. this.resetControlsTimeout();
  369. this.hideControlsWithTimeout();
  370. }
  371. },
  372. /**
  373. * Hide the controls
  374. */
  375. hideControls() {
  376. if (!this.video.paused) {
  377. this.controls.classList.remove('show');
  378. if (this.topControls) {
  379. this.topControls.classList.remove('show');
  380. }
  381. }
  382. },
  383. /**
  384. * Hide controls with a timeout
  385. */
  386. hideControlsWithTimeout() {
  387. this.resetControlsTimeout();
  388. this.controlsTimeout = setTimeout(() => {
  389. this.hideControls();
  390. }, 3000); // Changed to 3 seconds
  391. },
  392. /**
  393. * Reset the controls timeout
  394. */
  395. resetControlsTimeout() {
  396. if (this.controlsTimeout) {
  397. clearTimeout(this.controlsTimeout);
  398. this.controlsTimeout = null;
  399. }
  400. },
  401. /**
  402. * Set up auto-hide functionality for controls
  403. */
  404. setupAutoHide() {
  405. // Note: These event listeners are already added in setupEventListeners()
  406. // so we don't need to add them again here to prevent duplicate execution
  407. },
  408. /**
  409. * Handle window resize events
  410. */
  411. handleResize() {
  412. // Show controls during resize to ensure they're visible
  413. this.showControls();
  414. // Auto-hide after resize is complete
  415. clearTimeout(this.resizeTimeout);
  416. this.resizeTimeout = setTimeout(() => {
  417. if (!this.video.paused) {
  418. this.hideControlsWithTimeout();
  419. }
  420. }, 1000);
  421. },
  422. };
  423. // @license-end