/ src / contentScripts / index.ts
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  })();