index.ts
1 /* eslint-disable no-console */ 2 import { createApp } from "vue"; 3 import App from "./ContentScript.vue"; 4 import { createVuetify } from "vuetify"; 5 import * as components from "vuetify/components"; 6 import * as directives from "vuetify/directives"; 7 import "@mdi/font/css/materialdesignicons.css"; 8 import "vuetify/styles"; 9 10 (() => { 11 console.info("BrowserEQ v2 initialized"); 12 13 /** 14 * Find the most appropriate media element on the page 15 * This performs an intelligent search for the active/main media 16 */ 17 function findActiveMediaElement(): HTMLMediaElement | null { 18 try { 19 // Check for known video player patterns 20 const specialSelectors = [ 21 // YouTube 22 () => document.querySelector(".html5-main-video"), 23 () => document.querySelector("video.video-stream"), 24 // Netflix 25 () => document.querySelector(".VideoContainer video"), 26 // Generic large video 27 () => { 28 const videos = Array.from(document.querySelectorAll("video")); 29 return videos.sort( 30 (a, b) => 31 b.videoWidth * b.videoHeight - a.videoWidth * a.videoHeight 32 )[0]; // Return the largest video 33 }, 34 ]; 35 36 // Try each selector strategy in order 37 for (const selector of specialSelectors) { 38 const element = selector(); 39 if (element instanceof HTMLVideoElement && element.readyState > 0) { 40 return element; 41 } 42 } 43 44 // Find any playing video 45 const playingVideos = Array.from( 46 document.querySelectorAll("video") 47 ).filter((v) => !v.paused && v.currentTime > 0); 48 if (playingVideos.length > 0) { 49 return playingVideos[0]; 50 } 51 52 // Fall back to any video with a source 53 const videosWithSource = Array.from( 54 document.querySelectorAll("video") 55 ).filter((v) => v.src || v.querySelector("source")); 56 if (videosWithSource.length > 0) { 57 return videosWithSource[0]; 58 } 59 60 // Last resort: audio elements 61 const audios = Array.from(document.querySelectorAll("audio")); 62 const playingAudio = audios.find((a) => !a.paused); 63 if (playingAudio) return playingAudio; 64 65 // Return the first audio element if none are playing 66 return audios[0] || null; 67 } catch (e) { 68 console.error("Error finding media element:", e); 69 return null; 70 } 71 } 72 73 /** 74 * Perform a safe seek operation on a media element 75 */ 76 function safeSeek(media: HTMLMediaElement, newTime: number): boolean { 77 if (!media || media.readyState < 1) return false; 78 79 try { 80 // Handle YouTube special case 81 if (window.location.hostname.includes("youtube.com")) { 82 const ytPlayer = document.querySelector(".html5-video-player"); 83 if (ytPlayer && "seekTo" in ytPlayer) { 84 // @ts-ignore: YouTube player API 85 ytPlayer.seekTo(newTime); 86 return true; 87 } 88 } 89 90 // Standard HTML5 seek 91 const wasPlaying = !media.paused; 92 93 // Clamp time value to valid range 94 const clampedTime = Math.max(0, Math.min(media.duration || 0, newTime)); 95 96 // Set currentTime and handle seeking events 97 media.currentTime = clampedTime; 98 99 // Resume playback if it was playing before 100 if (wasPlaying && media.paused) { 101 const playPromise = media.play(); 102 if (playPromise !== undefined) { 103 playPromise.catch((e) => console.warn("Error resuming playback:", e)); 104 } 105 } 106 107 return true; 108 } catch (e) { 109 console.warn("Seek operation failed:", e); 110 return false; 111 } 112 } 113 114 // Enhanced media control message handler 115 chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 116 const media = findActiveMediaElement(); 117 118 // Prepare response object 119 let response = { 120 success: false, 121 message: "No media element found", 122 }; 123 124 if (media) { 125 try { 126 switch (message.action) { 127 case "pauseStream": 128 if (!media.paused) { 129 media.pause(); 130 response = { success: true, message: "Media paused" }; 131 } else { 132 response = { success: true, message: "Media already paused" }; 133 } 134 break; 135 136 case "resumeStream": 137 if (media.paused) { 138 const playPromise = media.play(); 139 if (playPromise !== undefined) { 140 playPromise.catch((e) => 141 console.warn("Error playing media:", e) 142 ); 143 } 144 response = { success: true, message: "Media resumed" }; 145 } else { 146 response = { success: true, message: "Media already playing" }; 147 } 148 break; 149 150 case "rewindStream": { 151 const seconds = message.seconds || 15; 152 const newTime = Math.max(0, media.currentTime - seconds); 153 const success = safeSeek(media, newTime); 154 response = { 155 success, 156 message: success 157 ? `Rewound ${seconds}s to ${newTime.toFixed(1)}` 158 : "Failed to rewind", 159 }; 160 break; 161 } 162 163 case "forwardStream": { 164 const seconds = message.seconds || 15; 165 const newTime = Math.min( 166 media.duration || 0, 167 media.currentTime + seconds 168 ); 169 const success = safeSeek(media, newTime); 170 response = { 171 success, 172 message: success 173 ? `Fast-forwarded ${seconds}s to ${newTime.toFixed(1)}` 174 : "Failed to fast-forward", 175 }; 176 break; 177 } 178 179 case "restartStream": { 180 const success = safeSeek(media, 0); 181 response = { 182 success, 183 message: success ? "Media restarted" : "Failed to restart", 184 }; 185 break; 186 } 187 188 case "getMediaStatus": { 189 response = { 190 success: true, 191 message: JSON.stringify({ 192 playing: !media.paused, 193 currentTime: media.currentTime, 194 duration: media.duration || 0, 195 canSeek: media.seekable && media.seekable.length > 0, 196 }), 197 }; 198 break; 199 } 200 } 201 } catch (e) { 202 console.error("Media control error:", e); 203 response = { success: false, message: `Error: ${e.message}` }; 204 } 205 } 206 207 sendResponse(response); 208 return true; // Keep message channel open for async response 209 }); 210 211 // Create mount point first 212 const mountPoint = document.createElement("div"); 213 mountPoint.id = "browser-eq-app"; 214 document.documentElement.appendChild(mountPoint); 215 216 // Initialize Vuetify 217 const vuetify = createVuetify({ 218 components, 219 directives, 220 theme: { 221 defaultTheme: "light", 222 }, 223 }); 224 225 // Initialize Vue app with error handling 226 function initializeApp() { 227 try { 228 const app = createApp(App); 229 app.use(vuetify); 230 231 // Make sure mount point exists 232 const container = document.querySelector("#browser-eq-app"); 233 if (!container) { 234 console.error("Mount point not found"); 235 return; 236 } 237 238 // Mount with error handling 239 app.mount("#browser-eq-app"); 240 } catch (error) { 241 console.error("Failed to initialize app:", error); 242 } 243 } 244 245 // Wait for document to be ready 246 if (document.readyState === "loading") { 247 document.addEventListener("DOMContentLoaded", initializeApp); 248 } else { 249 initializeApp(); 250 } 251 252 // Cleanup mount point if needed 253 window.addEventListener("unload", () => { 254 const container = document.querySelector("#browser-eq-app"); 255 if (container && container.parentNode) { 256 container.parentNode.removeChild(container); 257 } 258 }); 259 })();