Last active
July 27, 2026 17:47
-
-
Save lwcorp/b85b82126f373fbf435f9edecb6f1a0c to your computer and use it in GitHub Desktop.
Bookmarklet - Adds full video features to all video players including blocked ones
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| javascript:(function(){var pct=String.fromCharCode(37);const CONFIG={enablePlayPause:true,enableSpeed:true,enableReset:true,enableLoop:true,enablePip:true,enableCast:true,enableMute:true,enableVolumeSlider:true,enableTime:true,enableScreenshot:true,enableFullscreen:true,enableClose:true,enableCloseAll:true,PLAYBACK_SPEEDS:[0.5,0.75,1,1.25,1.5,1.75,2]};if(window.trustedTypes&&window.trustedTypes.createPolicy&&!window.trustedTypes.defaultPolicy){try{window.trustedTypes.createPolicy('default',{createHTML:function(s){return s;},createScriptURL:function(s){return s;},createScript:function(s){return s;}});}catch(e){}}document.querySelectorAll('.media-above-vol-wrapper').forEach(function(el){el.remove();});const existingStyle=document.getElementById('media-above-style');if(existingStyle)existingStyle.remove();var rawCss=".media-above-vol-wrapper{display:flex;justify-content:center !important;width:100"+pct+";margin-bottom:6px;z-index:99999;position:relative;direction:ltr !important}.media-above-vol{display:inline-flex;flex-wrap:wrap;align-items:center;gap:8px;background:rgba(30,31,32,0.95);padding:6px 14px;border-radius:20px;border:1px solid #555;box-shadow:0 4px 12px rgba(0,0,0,0.3);font-family:system-ui,-apple-system,sans-serif;user-select:none;direction:ltr !important}.media-above-btn{cursor:pointer;font-size:15px;line-height:1;color:#fff;padding:2px;display:flex;align-items:center;justify-content:center;transition:transform 0.1s ease,color 0.15s ease,opacity 0.15s ease}.media-above-btn:hover{transform:scale(1.15)}.media-above-btn[data-active=\"true\"]{color:#8ab4f8;text-shadow:0 0 8px rgba(138,180,248,0.5)}.media-above-slider{width:65px;cursor:pointer;accent-color:#8ab4f8}.media-above-label{font-size:12px;font-weight:500;color:#fff;min-width:36px;text-align:right;font-variant-numeric:tabular-nums}.media-above-time-container{display:inline-flex;align-items:center;font-size:12px;color:#ccc;font-variant-numeric:tabular-nums;gap:4px}.media-above-time-input{color:inherit !important;padding:inherit !important;margin:inherit !important;background-color:inherit !important;width:50px !important;border:1.5px solid #8ab4f8;font-family:inherit !important;font-size:12px !important;font-weight:500 !important;text-align:center !important;border-radius:6px !important;outline:none !important;cursor:text !important;box-shadow:0 0 4px rgba(138,180,248,0.25) !important;transition:all 0.15s ease !important}.media-above-time-input:hover{box-shadow:0 0 8px rgba(138,180,248,0.5)}.media-above-time-input:focus{border-color:#fff;box-shadow:0 0 10px rgba(138,180,248,0.8)}.media-above-select{background:rgba(0,0,0,0.5);color:#fff;border:1px solid #666;border-radius:6px;font-size:11px;padding:2px 4px;cursor:pointer;outline:none}.media-above-divider{width:1px;height:14px;background:rgba(255,255,255,0.2);margin:0 2px}.media-above-close{cursor:pointer;font-size:18px;font-weight:bold;color:#aaa;margin-left:2px;padding:0 2px;line-height:1;transition:color 0.15s ease}.media-above-close:hover{color:#fff}.media-above-close-all{cursor:pointer;font-size:20px;font-weight:900;color:#ff6b6b;margin-left:4px;padding:0 4px;line-height:1;transition:transform 0.15s ease,color 0.15s ease}";const style=document.createElement('style');style.id='media-above-style';style.textContent=rawCss;document.head.appendChild(style);const formatTime=(secs)=>{if(!secs||isNaN(secs))return'0:00';const h=Math.floor(secs / 3600);const m=Math.floor((secs-(h*3600))/ 60);const s=Math.floor(secs-Math.floor(secs / 60)*60);const sStr=s<10?'0'+s:s;if(h>0){const mStr=m<10?'0'+m:m;return h+':'+mStr+':'+sStr;}return m+':'+sStr;};const parseTimeInput=(str)=>{const parts=str.trim().split(':').map(Number);if(parts.some(isNaN))return null;if(parts.length===3)return parts[0]*3600+parts[1]*60+parts[2];if(parts.length===2)return parts[0]*60+parts[1];if(parts.length===1)return parts[0];return null;};const createDivider=()=>{const d=document.createElement('span');d.className='media-above-divider';return d;};document.querySelectorAll('video').forEach((video)=>{const container=video.closest('generated-video')||video.closest('video-player')||video.parentElement;const wrapper=document.createElement('div');wrapper.className='media-above-vol-wrapper';const ctrlBox=document.createElement('div');ctrlBox.className='media-above-vol';['click','mousedown','touchstart','dblclick'].forEach(evt=>{ctrlBox.addEventListener(evt,(e)=>e.stopPropagation());});const elementsToAppend=[];if(CONFIG.enablePlayPause){const ppBtn=document.createElement('span');ppBtn.className='media-above-btn';const PLAY_ICON='▶';const PAUSE_ICON='<span style="font-size: 14px; letter-spacing: 2px; font-weight: 800;">❚❚</span>';ppBtn.innerHTML=video.paused?PLAY_ICON:PAUSE_ICON;ppBtn.title='Play/Pause';ppBtn.addEventListener('click',()=>{video.paused?video.play():video.pause();});video.addEventListener('play',()=>ppBtn.innerHTML=PAUSE_ICON);video.addEventListener('pause',()=>ppBtn.innerHTML=PLAY_ICON);elementsToAppend.push(ppBtn);}if(CONFIG.enableSpeed){const speedSelect=document.createElement('select');speedSelect.className='media-above-select';speedSelect.title='Playback Speed';CONFIG.PLAYBACK_SPEEDS.forEach(rate=>{const opt=document.createElement('option');opt.value=rate;opt.innerText=rate+'x';if(video.playbackRate===rate||(rate===1&&!video.playbackRate))opt.selected=true;speedSelect.appendChild(opt);});speedSelect.addEventListener('change',(e)=>{video.playbackRate=parseFloat(e.target.value);});elementsToAppend.push(speedSelect);}if(CONFIG.enableReset){const resetBtn=document.createElement('span');resetBtn.className='media-above-btn';resetBtn.innerHTML='<span style="font-size: 11px; font-weight: 800; letter-spacing: 0.5px;">RESET</span>';resetBtn.title='Resets Speed, Volume, Time, Loop, PiP, and Cast';resetBtn.addEventListener('click',()=>{video.playbackRate=1;const speedSelect=ctrlBox.querySelector('.media-above-select');if(speedSelect)speedSelect.value='1';video.muted=false;video.volume=1;video.currentTime=0;video.loop=false;const allBtns=ctrlBox.querySelectorAll('.media-above-btn');allBtns.forEach(function(b){if(b.title==='Toggle Loop')b.dataset.active='false';});if(document.pictureInPictureElement){document.exitPictureInPicture().catch(()=>{});}if(video.remote){try{video.disableRemotePlayback=true;setTimeout(()=>{video.disableRemotePlayback=false;},100);}catch(e){}}});elementsToAppend.push(resetBtn);}if(CONFIG.enableLoop){const loopBtn=document.createElement('span');loopBtn.className='media-above-btn';loopBtn.innerHTML='↻';loopBtn.title='Toggle Loop';loopBtn.dataset.active=video.loop.toString();loopBtn.addEventListener('click',()=>{video.loop=!video.loop;loopBtn.dataset.active=video.loop.toString();});elementsToAppend.push(loopBtn);}if(CONFIG.enablePip&&document.pictureInPictureEnabled){const pipBtn=document.createElement('span');pipBtn.className='media-above-btn';pipBtn.innerHTML='▦';pipBtn.title='Picture-in-Picture';pipBtn.addEventListener('click',()=>{video.disablePictureInPicture=false;try{video.removeAttribute('disablePictureInPicture');}catch(e){}if(document.pictureInPictureElement===video){document.exitPictureInPicture().catch(()=>{});}else{video.requestPictureInPicture().catch((err)=>{alert('PiP Blocked by Website:\n\nThis website strictly prevents Picture-in-Picture natively. This usually happens because:\n1. The video is DRM-encrypted (Netflix, Hulu, etc.)\n2. The player uses a custom Canvas render rather than standard HTML5\n3. Aggressive anti-PiP scripts are running.\n\nWorkaround: Try using your browser\'s native Media Control hub (the music note icon next to your extensions) or a dedicated browser extension.');});}});elementsToAppend.push(pipBtn);}if(CONFIG.enableCast){const castBtn=document.createElement('span');castBtn.className='media-above-btn';castBtn.innerHTML='📺';castBtn.title='Cast / Remote Playback';castBtn.addEventListener('click',(e)=>{e.preventDefault();e.stopPropagation();if(navigator.userActivation&&!navigator.userActivation.isActive){alert('Browser Security Warning:\n\nThe Cast prompt was blocked because the browser did not register a direct user gesture. Please try clicking inside the video player once before clicking Cast.');return;}if(video.remote){try{video.disableRemotePlayback=false;}catch(err){}video.remote.prompt().then(()=>{}).catch(err=>{if(err.name==='NotFoundError'){alert('No compatible Cast or AirPlay devices found on your local network.');}else if(err.name==='NotSupportedError'||err.name==='InvalidStateError'){alert('Stream Incompatible:\n\nThis video stream (e.g., Blob URL, MSE, or CORS-protected CDN) cannot be cast natively by the browser.\n\nWorkaround: Use Chrome or Edge\'s 3-dot menu -> "Cast..." to mirror your entire tab instead!');}else if(err.name==='NotAllowedError'){alert('Cast Prompt Dismissed or Blocked:\n\nThe browser blocked the request or the device picker was closed.\n\nIf this persists on this website, use your browser\'s 3-dot menu -> "Cast..." to mirror the tab directly.');}else{alert('Cast failed ('+err.name+'): '+err.message+'\n\nTip: Use your browser\'s 3-dot menu -> "Cast..." to mirror this tab.');}});}else{alert('Your browser does not support the native Web Remote Playback API.\n\nTip: Use Chrome or Edge\'s 3-dot menu -> "Cast..." to mirror this tab!');}});elementsToAppend.push(castBtn);}let lastVol=video.volume||1;if(CONFIG.enableMute||CONFIG.enableVolumeSlider){elementsToAppend.push(createDivider());const speakerBtn=document.createElement('span');const slider=document.createElement('input');const label=document.createElement('span');if(CONFIG.enableMute){speakerBtn.className='media-above-btn';speakerBtn.innerHTML=video.muted||video.volume===0?'🔇':'🔊';speakerBtn.title='Mute/Unmute';speakerBtn.addEventListener('click',()=>{if(video.muted||video.volume===0){video.muted=false;video.volume=lastVol||0.5;}else{lastVol=video.volume;video.muted=true;video.volume=0;}});elementsToAppend.push(speakerBtn);}if(CONFIG.enableVolumeSlider){slider.type='range';slider.className='media-above-slider';slider.min='0';slider.max='1';slider.step='0.05';slider.value=video.muted?'0':(video.volume||'1');label.className='media-above-label';label.innerText=Math.round((video.muted?0:video.volume)*100)+pct;slider.addEventListener('input',(e)=>{const vol=parseFloat(e.target.value);if(vol>0)lastVol=vol;video.muted=(vol===0);video.volume=vol;});elementsToAppend.push(slider,label);}video.addEventListener('volumechange',()=>{if(CONFIG.enableMute){speakerBtn.innerHTML=video.muted||video.volume===0?'🔇':'🔊';}if(CONFIG.enableVolumeSlider){slider.value=video.muted?'0':video.volume;label.innerText=Math.round((video.muted?0:video.volume)*100)+pct;}});}if(CONFIG.enableTime){elementsToAppend.push(createDivider());const timeContainer=document.createElement('span');timeContainer.className='media-above-time-container';const timeInput=document.createElement('input');timeInput.type='text';timeInput.className='media-above-time-input';timeInput.title='Click to edit timestamp (e.g. 1:15 or 75) and press Enter to jump';const durationSpan=document.createElement('span');let isEditingTime=false;const updateTimeDisplay=()=>{if(!isEditingTime){timeInput.value=formatTime(video.currentTime);}durationSpan.innerText='/ '+formatTime(video.duration);};timeInput.addEventListener('focus',()=>{isEditingTime=true;timeInput.select();});const applyTypedTime=()=>{isEditingTime=false;const targetSeconds=parseTimeInput(timeInput.value);if(targetSeconds!==null&&targetSeconds<=video.duration){video.currentTime=targetSeconds;}updateTimeDisplay();};timeInput.addEventListener('blur',applyTypedTime);timeInput.addEventListener('keydown',(e)=>{if(e.key==='Enter'){timeInput.blur();}else if(e.key==='Escape'){isEditingTime=false;timeInput.blur();}});updateTimeDisplay();video.addEventListener('timeupdate',updateTimeDisplay);video.addEventListener('loadedmetadata',updateTimeDisplay);timeContainer.append(timeInput,durationSpan);elementsToAppend.push(timeContainer);}if(CONFIG.enableScreenshot){elementsToAppend.push(createDivider());const snapBtn=document.createElement('span');snapBtn.className='media-above-btn';snapBtn.innerHTML='📷';snapBtn.title='Take Screenshot';snapBtn.addEventListener('click',()=>{try{const canvas=document.createElement('canvas');canvas.width=video.videoWidth||video.clientWidth;canvas.height=video.videoHeight||video.clientHeight;canvas.getContext('2d').drawImage(video,0,0,canvas.width,canvas.height);const link=document.createElement('a');link.download='screenshot-'+Math.floor(video.currentTime)+'s.png';link.href=canvas.toDataURL('image/png');link.click();}catch(err){alert('Screenshot failed: Video stream is protected by CORS.');}});elementsToAppend.push(snapBtn);}if(CONFIG.enableFullscreen){const fsBtn=document.createElement('span');fsBtn.className='media-above-btn';fsBtn.innerHTML='⛶';fsBtn.title='Toggle Fullscreen';fsBtn.addEventListener('click',()=>{const target=video.closest('video-player')||video;if(document.fullscreenElement){if(document.exitFullscreen)document.exitFullscreen();}else{if(target.requestFullscreen)target.requestFullscreen();else if(target.webkitRequestFullscreen)target.webkitRequestFullscreen();}});elementsToAppend.push(fsBtn);}if(CONFIG.enableClose||CONFIG.enableCloseAll){elementsToAppend.push(createDivider());if(CONFIG.enableClose){const closeBtn=document.createElement('span');closeBtn.className='media-above-close';closeBtn.innerHTML='×';closeBtn.title='Close This Control';closeBtn.addEventListener('click',()=>wrapper.remove());elementsToAppend.push(closeBtn);}if(CONFIG.enableCloseAll){const closeAllBtn=document.createElement('span');closeAllBtn.className='media-above-close-all';closeAllBtn.innerHTML='✖';closeAllBtn.title='Close ALL Controls';closeAllBtn.addEventListener('click',()=>{document.querySelectorAll('.media-above-vol-wrapper').forEach(el=>el.remove());const s=document.getElementById('media-above-style');if(s)s.remove();});elementsToAppend.push(closeAllBtn);}}elementsToAppend.forEach(el=>ctrlBox.appendChild(el));wrapper.appendChild(ctrlBox);if(container&&container.parentNode){container.parentNode.insertBefore(wrapper,container);}});})(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| (function () { | |
| var pct = String.fromCharCode(37); | |
| const CONFIG = { | |
| enablePlayPause: true, | |
| enableSpeed: true, | |
| enableReset: true, | |
| enableLoop: true, | |
| enablePip: true, | |
| enableCast: true, | |
| enableMute: true, | |
| enableVolumeSlider: true, | |
| enableTime: true, | |
| enableScreenshot: true, | |
| enableFullscreen: true, | |
| enableClose: true, | |
| enableCloseAll: true, | |
| PLAYBACK_SPEEDS: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] | |
| }; | |
| if (window.trustedTypes && window.trustedTypes.createPolicy && !window.trustedTypes.defaultPolicy) { | |
| try { | |
| window.trustedTypes.createPolicy('default', { | |
| createHTML: function(s) { return s; }, | |
| createScriptURL: function(s) { return s; }, | |
| createScript: function(s) { return s; } | |
| }); | |
| } catch (e) {} | |
| } | |
| document.querySelectorAll('.media-above-vol-wrapper').forEach(function(el) { el.remove(); }); | |
| const existingStyle = document.getElementById('media-above-style'); | |
| if (existingStyle) existingStyle.remove(); | |
| var rawCss = ".media-above-vol-wrapper{display:flex;justify-content:center !important;width:100" + pct + ";margin-bottom:6px;z-index:99999;position:relative;direction:ltr !important}.media-above-vol{display:inline-flex;flex-wrap:wrap;align-items:center;gap:8px;background:rgba(30,31,32,0.95);padding:6px 14px;border-radius:20px;border:1px solid #555;box-shadow:0 4px 12px rgba(0,0,0,0.3);font-family:system-ui,-apple-system,sans-serif;user-select:none;direction:ltr !important}.media-above-btn{cursor:pointer;font-size:15px;line-height:1;color:#fff;padding:2px;display:flex;align-items:center;justify-content:center;transition:transform 0.1s ease,color 0.15s ease,opacity 0.15s ease}.media-above-btn:hover{transform:scale(1.15)}.media-above-btn[data-active=\"true\"]{color:#8ab4f8;text-shadow:0 0 8px rgba(138,180,248,0.5)}.media-above-slider{width:65px;cursor:pointer;accent-color:#8ab4f8}.media-above-label{font-size:12px;font-weight:500;color:#fff;min-width:36px;text-align:right;font-variant-numeric:tabular-nums}.media-above-time-container{display:inline-flex;align-items:center;font-size:12px;color:#ccc;font-variant-numeric:tabular-nums;gap:4px}.media-above-time-input{color:inherit !important;padding:inherit !important;margin:inherit !important;background-color:inherit !important;width:50px !important;border:1.5px solid #8ab4f8;font-family:inherit !important;font-size:12px !important;font-weight:500 !important;text-align:center !important;border-radius:6px !important;outline:none !important;cursor:text !important;box-shadow:0 0 4px rgba(138,180,248,0.25) !important;transition:all 0.15s ease !important}.media-above-time-input:hover{box-shadow:0 0 8px rgba(138,180,248,0.5)}.media-above-time-input:focus{border-color:#fff;box-shadow:0 0 10px rgba(138,180,248,0.8)}.media-above-select{background:rgba(0,0,0,0.5);color:#fff;border:1px solid #666;border-radius:6px;font-size:11px;padding:2px 4px;cursor:pointer;outline:none}.media-above-divider{width:1px;height:14px;background:rgba(255,255,255,0.2);margin:0 2px}.media-above-close{cursor:pointer;font-size:18px;font-weight:bold;color:#aaa;margin-left:2px;padding:0 2px;line-height:1;transition:color 0.15s ease}.media-above-close:hover{color:#fff}.media-above-close-all{cursor:pointer;font-size:20px;font-weight:900;color:#ff6b6b;margin-left:4px;padding:0 4px;line-height:1;transition:transform 0.15s ease,color 0.15s ease}"; | |
| const style = document.createElement('style'); | |
| style.id = 'media-above-style'; | |
| style.textContent = rawCss; | |
| document.head.appendChild(style); | |
| const formatTime = (secs) => { | |
| if (!secs || isNaN(secs)) return '0:00'; | |
| const h = Math.floor(secs / 3600); | |
| const m = Math.floor((secs - (h * 3600)) / 60); | |
| const s = Math.floor(secs - Math.floor(secs / 60) * 60); | |
| const sStr = s < 10 ? '0' + s : s; | |
| if (h > 0) { | |
| const mStr = m < 10 ? '0' + m : m; | |
| return h + ':' + mStr + ':' + sStr; | |
| } | |
| return m + ':' + sStr; | |
| }; | |
| const parseTimeInput = (str) => { | |
| const parts = str.trim().split(':').map(Number); | |
| if (parts.some(isNaN)) return null; | |
| if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2]; | |
| if (parts.length === 2) return parts[0] * 60 + parts[1]; | |
| if (parts.length === 1) return parts[0]; | |
| return null; | |
| }; | |
| const createDivider = () => { | |
| const d = document.createElement('span'); | |
| d.className = 'media-above-divider'; | |
| return d; | |
| }; | |
| document.querySelectorAll('video').forEach((video) => { | |
| const container = video.closest('generated-video') || video.closest('video-player') || video.parentElement; | |
| const wrapper = document.createElement('div'); | |
| wrapper.className = 'media-above-vol-wrapper'; | |
| const ctrlBox = document.createElement('div'); | |
| ctrlBox.className = 'media-above-vol'; | |
| ['click', 'mousedown', 'touchstart', 'dblclick'].forEach(evt => { | |
| ctrlBox.addEventListener(evt, (e) => e.stopPropagation()); | |
| }); | |
| const elementsToAppend = []; | |
| if (CONFIG.enablePlayPause) { | |
| const ppBtn = document.createElement('span'); | |
| ppBtn.className = 'media-above-btn'; | |
| const PLAY_ICON = '▶'; | |
| const PAUSE_ICON = '<span style="font-size: 14px; letter-spacing: 2px; font-weight: 800;">❚❚</span>'; | |
| ppBtn.innerHTML = video.paused ? PLAY_ICON : PAUSE_ICON; | |
| ppBtn.title = 'Play/Pause'; | |
| ppBtn.addEventListener('click', () => { | |
| video.paused ? video.play() : video.pause(); | |
| }); | |
| video.addEventListener('play', () => ppBtn.innerHTML = PAUSE_ICON); | |
| video.addEventListener('pause', () => ppBtn.innerHTML = PLAY_ICON); | |
| elementsToAppend.push(ppBtn); | |
| } | |
| if (CONFIG.enableSpeed) { | |
| const speedSelect = document.createElement('select'); | |
| speedSelect.className = 'media-above-select'; | |
| speedSelect.title = 'Playback Speed'; | |
| CONFIG.PLAYBACK_SPEEDS.forEach(rate => { | |
| const opt = document.createElement('option'); | |
| opt.value = rate; | |
| opt.innerText = rate + 'x'; | |
| if (video.playbackRate === rate || (rate === 1 && !video.playbackRate)) opt.selected = true; | |
| speedSelect.appendChild(opt); | |
| }); | |
| speedSelect.addEventListener('change', (e) => { | |
| video.playbackRate = parseFloat(e.target.value); | |
| }); | |
| elementsToAppend.push(speedSelect); | |
| } | |
| if (CONFIG.enableReset) { | |
| const resetBtn = document.createElement('span'); | |
| resetBtn.className = 'media-above-btn'; | |
| resetBtn.innerHTML = '<span style="font-size: 11px; font-weight: 800; letter-spacing: 0.5px;">RESET</span>'; | |
| resetBtn.title = 'Resets Speed, Volume, Time, Loop, PiP, and Cast'; | |
| resetBtn.addEventListener('click', () => { | |
| video.playbackRate = 1; | |
| const speedSelect = ctrlBox.querySelector('.media-above-select'); | |
| if (speedSelect) speedSelect.value = '1'; | |
| video.muted = false; | |
| video.volume = 1; | |
| video.currentTime = 0; | |
| video.loop = false; | |
| const allBtns = ctrlBox.querySelectorAll('.media-above-btn'); | |
| allBtns.forEach(function(b) { | |
| if (b.title === 'Toggle Loop') b.dataset.active = 'false'; | |
| }); | |
| if (document.pictureInPictureElement) { | |
| document.exitPictureInPicture().catch(() => {}); | |
| } | |
| if (video.remote) { | |
| try { | |
| video.disableRemotePlayback = true; | |
| setTimeout(() => { video.disableRemotePlayback = false; }, 100); | |
| } catch (e) {} | |
| } | |
| }); | |
| elementsToAppend.push(resetBtn); | |
| } | |
| if (CONFIG.enableLoop) { | |
| const loopBtn = document.createElement('span'); | |
| loopBtn.className = 'media-above-btn'; | |
| loopBtn.innerHTML = '↻'; | |
| loopBtn.title = 'Toggle Loop'; | |
| loopBtn.dataset.active = video.loop.toString(); | |
| loopBtn.addEventListener('click', () => { | |
| video.loop = !video.loop; | |
| loopBtn.dataset.active = video.loop.toString(); | |
| }); | |
| elementsToAppend.push(loopBtn); | |
| } | |
| if (CONFIG.enablePip && document.pictureInPictureEnabled) { | |
| const pipBtn = document.createElement('span'); | |
| pipBtn.className = 'media-above-btn'; | |
| pipBtn.innerHTML = '▦'; | |
| pipBtn.title = 'Picture-in-Picture'; | |
| pipBtn.addEventListener('click', () => { | |
| video.disablePictureInPicture = false; | |
| try { video.removeAttribute('disablePictureInPicture'); } catch(e) {} | |
| if (document.pictureInPictureElement === video) { | |
| document.exitPictureInPicture().catch(() => {}); | |
| } else { | |
| video.requestPictureInPicture().catch((err) => { | |
| alert('PiP Blocked by Website:\n\nThis website strictly prevents Picture-in-Picture natively. This usually happens because:\n1. The video is DRM-encrypted (Netflix, Hulu, etc.)\n2. The player uses a custom Canvas render rather than standard HTML5\n3. Aggressive anti-PiP scripts are running.\n\nWorkaround: Try using your browser\'s native Media Control hub (the music note icon next to your extensions) or a dedicated browser extension.'); | |
| }); | |
| } | |
| }); | |
| elementsToAppend.push(pipBtn); | |
| } | |
| if (CONFIG.enableCast) { | |
| const castBtn = document.createElement('span'); | |
| castBtn.className = 'media-above-btn'; | |
| castBtn.innerHTML = '📺'; | |
| castBtn.title = 'Cast / Remote Playback'; | |
| castBtn.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| if (navigator.userActivation && !navigator.userActivation.isActive) { | |
| alert('Browser Security Warning:\n\nThe Cast prompt was blocked because the browser did not register a direct user gesture. Please try clicking inside the video player once before clicking Cast.'); | |
| return; | |
| } | |
| if (video.remote) { | |
| try { | |
| video.disableRemotePlayback = false; | |
| } catch (err) {} | |
| video.remote.prompt() | |
| .then(() => {}) | |
| .catch(err => { | |
| if (err.name === 'NotFoundError') { | |
| alert('No compatible Cast or AirPlay devices found on your local network.'); | |
| } else if (err.name === 'NotSupportedError' || err.name === 'InvalidStateError') { | |
| alert('Stream Incompatible:\n\nThis video stream (e.g., Blob URL, MSE, or CORS-protected CDN) cannot be cast natively by the browser.\n\nWorkaround: Use Chrome or Edge\'s 3-dot menu -> "Cast..." to mirror your entire tab instead!'); | |
| } else if (err.name === 'NotAllowedError') { | |
| alert('Cast Prompt Dismissed or Blocked:\n\nThe browser blocked the request or the device picker was closed.\n\nIf this persists on this website, use your browser\'s 3-dot menu -> "Cast..." to mirror the tab directly.'); | |
| } else { | |
| alert('Cast failed (' + err.name + '): ' + err.message + '\n\nTip: Use your browser\'s 3-dot menu -> "Cast..." to mirror this tab.'); | |
| } | |
| }); | |
| } else { | |
| alert('Your browser does not support the native Web Remote Playback API.\n\nTip: Use Chrome or Edge\'s 3-dot menu -> "Cast..." to mirror this tab!'); | |
| } | |
| }); | |
| elementsToAppend.push(castBtn); | |
| } | |
| let lastVol = video.volume || 1; | |
| if (CONFIG.enableMute || CONFIG.enableVolumeSlider) { | |
| elementsToAppend.push(createDivider()); | |
| const speakerBtn = document.createElement('span'); | |
| const slider = document.createElement('input'); | |
| const label = document.createElement('span'); | |
| if (CONFIG.enableMute) { | |
| speakerBtn.className = 'media-above-btn'; | |
| speakerBtn.innerHTML = video.muted || video.volume === 0 ? '🔇' : '🔊'; | |
| speakerBtn.title = 'Mute/Unmute'; | |
| speakerBtn.addEventListener('click', () => { | |
| if (video.muted || video.volume === 0) { | |
| video.muted = false; | |
| video.volume = lastVol || 0.5; | |
| } else { | |
| lastVol = video.volume; | |
| video.muted = true; | |
| video.volume = 0; | |
| } | |
| }); | |
| elementsToAppend.push(speakerBtn); | |
| } | |
| if (CONFIG.enableVolumeSlider) { | |
| slider.type = 'range'; | |
| slider.className = 'media-above-slider'; | |
| slider.min = '0'; | |
| slider.max = '1'; | |
| slider.step = '0.05'; | |
| slider.value = video.muted ? '0' : (video.volume || '1'); | |
| label.className = 'media-above-label'; | |
| label.innerText = Math.round((video.muted ? 0 : video.volume) * 100) + pct; | |
| slider.addEventListener('input', (e) => { | |
| const vol = parseFloat(e.target.value); | |
| if (vol > 0) lastVol = vol; | |
| video.muted = (vol === 0); | |
| video.volume = vol; | |
| }); | |
| elementsToAppend.push(slider, label); | |
| } | |
| video.addEventListener('volumechange', () => { | |
| if (CONFIG.enableMute) { | |
| speakerBtn.innerHTML = video.muted || video.volume === 0 ? '🔇' : '🔊'; | |
| } | |
| if (CONFIG.enableVolumeSlider) { | |
| slider.value = video.muted ? '0' : video.volume; | |
| label.innerText = Math.round((video.muted ? 0 : video.volume) * 100) + pct; | |
| } | |
| }); | |
| } | |
| if (CONFIG.enableTime) { | |
| elementsToAppend.push(createDivider()); | |
| const timeContainer = document.createElement('span'); | |
| timeContainer.className = 'media-above-time-container'; | |
| const timeInput = document.createElement('input'); | |
| timeInput.type = 'text'; | |
| timeInput.className = 'media-above-time-input'; | |
| timeInput.title = 'Click to edit timestamp (e.g. 1:15 or 75) and press Enter to jump'; | |
| const durationSpan = document.createElement('span'); | |
| let isEditingTime = false; | |
| const updateTimeDisplay = () => { | |
| if (!isEditingTime) { | |
| timeInput.value = formatTime(video.currentTime); | |
| } | |
| durationSpan.innerText = '/ ' + formatTime(video.duration); | |
| }; | |
| timeInput.addEventListener('focus', () => { | |
| isEditingTime = true; | |
| timeInput.select(); | |
| }); | |
| const applyTypedTime = () => { | |
| isEditingTime = false; | |
| const targetSeconds = parseTimeInput(timeInput.value); | |
| if (targetSeconds !== null && targetSeconds <= video.duration) { | |
| video.currentTime = targetSeconds; | |
| } | |
| updateTimeDisplay(); | |
| }; | |
| timeInput.addEventListener('blur', applyTypedTime); | |
| timeInput.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter') { | |
| timeInput.blur(); | |
| } else if (e.key === 'Escape') { | |
| isEditingTime = false; | |
| timeInput.blur(); | |
| } | |
| }); | |
| updateTimeDisplay(); | |
| video.addEventListener('timeupdate', updateTimeDisplay); | |
| video.addEventListener('loadedmetadata', updateTimeDisplay); | |
| timeContainer.append(timeInput, durationSpan); | |
| elementsToAppend.push(timeContainer); | |
| } | |
| if (CONFIG.enableScreenshot) { | |
| elementsToAppend.push(createDivider()); | |
| const snapBtn = document.createElement('span'); | |
| snapBtn.className = 'media-above-btn'; | |
| snapBtn.innerHTML = '📷'; | |
| snapBtn.title = 'Take Screenshot'; | |
| snapBtn.addEventListener('click', () => { | |
| try { | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = video.videoWidth || video.clientWidth; | |
| canvas.height = video.videoHeight || video.clientHeight; | |
| canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height); | |
| const link = document.createElement('a'); | |
| link.download = 'screenshot-' + Math.floor(video.currentTime) + 's.png'; | |
| link.href = canvas.toDataURL('image/png'); | |
| link.click(); | |
| } catch (err) { | |
| alert('Screenshot failed: Video stream is protected by CORS.'); | |
| } | |
| }); | |
| elementsToAppend.push(snapBtn); | |
| } | |
| if (CONFIG.enableFullscreen) { | |
| const fsBtn = document.createElement('span'); | |
| fsBtn.className = 'media-above-btn'; | |
| fsBtn.innerHTML = '⛶'; | |
| fsBtn.title = 'Toggle Fullscreen'; | |
| fsBtn.addEventListener('click', () => { | |
| const target = video.closest('video-player') || video; | |
| if (document.fullscreenElement) { | |
| if (document.exitFullscreen) document.exitFullscreen(); | |
| } else { | |
| if (target.requestFullscreen) target.requestFullscreen(); | |
| else if (target.webkitRequestFullscreen) target.webkitRequestFullscreen(); | |
| } | |
| }); | |
| elementsToAppend.push(fsBtn); | |
| } | |
| if (CONFIG.enableClose || CONFIG.enableCloseAll) { | |
| elementsToAppend.push(createDivider()); | |
| if (CONFIG.enableClose) { | |
| const closeBtn = document.createElement('span'); | |
| closeBtn.className = 'media-above-close'; | |
| closeBtn.innerHTML = '×'; | |
| closeBtn.title = 'Close This Control'; | |
| closeBtn.addEventListener('click', () => wrapper.remove()); | |
| elementsToAppend.push(closeBtn); | |
| } | |
| if (CONFIG.enableCloseAll) { | |
| const closeAllBtn = document.createElement('span'); | |
| closeAllBtn.className = 'media-above-close-all'; | |
| closeAllBtn.innerHTML = '✖'; | |
| closeAllBtn.title = 'Close ALL Controls'; | |
| closeAllBtn.addEventListener('click', () => { | |
| document.querySelectorAll('.media-above-vol-wrapper').forEach(el => el.remove()); | |
| const s = document.getElementById('media-above-style'); | |
| if (s) s.remove(); | |
| }); | |
| elementsToAppend.push(closeAllBtn); | |
| } | |
| } | |
| elementsToAppend.forEach(el => ctrlBox.appendChild(el)); | |
| wrapper.appendChild(ctrlBox); | |
| if (container && container.parentNode) { | |
| container.parentNode.insertBefore(wrapper, container); | |
| } | |
| }); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment