diff --git a/src/content.ts b/src/content.ts index 1a00dbef..86f51a50 100644 --- a/src/content.ts +++ b/src/content.ts @@ -60,6 +60,7 @@ let sponsorSkipped: boolean[] = []; let video: HTMLVideoElement; let videoMuted = false; // Has it been attempted to be muted let videoMutationObserver: MutationObserver = null; +let waitingForNewVideo = false; // List of videos that have had event listeners added to them const videosWithEventListeners: HTMLVideoElement[] = []; const controlsWithEventListeners: HTMLElement[] = [] @@ -97,10 +98,8 @@ const playerButtons: Record Config.config !== null, 1000, 1).then(() => videoIDChange(getYouTubeVideoID(document))); // wait for hover preview to appear, and refresh attachments if ever found -window.addEventListener("DOMContentLoaded", () => { - utils.waitForElement(".ytp-inline-preview-ui").then(() => refreshVideoAttachments()) - utils.waitForElement("[data-sessionlink='feature=player-title']").then(() => videoIDChange(getYouTubeVideoID(document))) -}); +utils.waitForElement(".ytp-inline-preview-ui").then(() => refreshVideoAttachments()) +utils.waitForElement("[data-sessionlink='feature=player-title']").then(() => videoIDChange(getYouTubeVideoID(document))) addPageListeners(); addHotkeyListener(); @@ -316,7 +315,7 @@ function resetValues() { categoryPill?.setVisibility(false); } -async function videoIDChange(id) { +async function videoIDChange(id): Promise { //if the id has not changed return unless the video element has changed if (sponsorVideoID === id && (isVisible(video) || !video)) return; @@ -500,6 +499,13 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?: return; } + // ensure we are on the correct video + const newVideoID = getYouTubeVideoID(document); + if (newVideoID !== sponsorVideoID) { + videoIDChange(newVideoID); + return; + } + logDebug(`Considering to start skipping: ${!video}, ${video?.paused}`); if (!video || video.paused) return; @@ -681,27 +687,30 @@ function setupVideoMutationListener() { }); } -function refreshVideoAttachments() { - const newVideo = findValidElement(document.querySelectorAll('video')) as HTMLVideoElement; - if (newVideo && newVideo !== video) { - video = newVideo; +async function refreshVideoAttachments(): Promise { + if (waitingForNewVideo) return; - if (!videosWithEventListeners.includes(video)) { - videosWithEventListeners.push(video); + waitingForNewVideo = true; + const newVideo = await utils.waitForElement("video", true) as HTMLVideoElement; + waitingForNewVideo = false; - setupVideoListeners(); - setupSkipButtonControlBar(); - setupCategoryPill(); - } + video = newVideo; + if (!videosWithEventListeners.includes(video)) { + videosWithEventListeners.push(video); - // Create a new bar in the new video element - if (previewBar && !utils.findReferenceNode()?.contains(previewBar.container)) { - previewBar.remove(); - previewBar = null; - - createPreviewBar(); - } + setupVideoListeners(); + setupSkipButtonControlBar(); + setupCategoryPill(); } + + if (previewBar && !utils.findReferenceNode()?.contains(previewBar.container)) { + previewBar.remove(); + previewBar = null; + + createPreviewBar(); + } + + videoIDChange(getYouTubeVideoID(document)); } function setupVideoListeners() { @@ -1057,23 +1066,24 @@ function getYouTubeVideoID(document: Document, url?: string): string | boolean { // clips should never skip, going from clip to full video has no indications. if (url.includes("youtube.com/clip/")) return false; // skip to document and don't hide if on /embed/ - if (url.includes("/embed/") && url.includes("youtube.com")) return getYouTubeVideoIDFromDocument(document, false); + if (url.includes("/embed/") && url.includes("youtube.com")) return getYouTubeVideoIDFromDocument(false); // skip to URL if matches youtube watch or invidious or matches youtube pattern if ((!url.includes("youtube.com")) || url.includes("/watch") || url.includes("/shorts/") || url.includes("playlist")) return getYouTubeVideoIDFromURL(url); // skip to document if matches pattern - if (url.includes("/channel/") || url.includes("/user/") || url.includes("/c/")) return getYouTubeVideoIDFromDocument(document); + if (url.includes("/channel/") || url.includes("/user/") || url.includes("/c/")) return getYouTubeVideoIDFromDocument(); // not sure, try URL then document - return getYouTubeVideoIDFromURL(url) || getYouTubeVideoIDFromDocument(document, false); + return getYouTubeVideoIDFromURL(url) || getYouTubeVideoIDFromDocument(false); } -function getYouTubeVideoIDFromDocument(document: Document, hideIcon = true): string | boolean { +function getYouTubeVideoIDFromDocument(hideIcon = true): string | boolean { // get ID from document (channel trailer / embedded playlist) - const videoURL = document.querySelector("[data-sessionlink='feature=player-title']")?.getAttribute("href"); + const element = video?.parentElement?.parentElement?.querySelector("[data-sessionlink='feature=player-title']"); + const videoURL = element?.getAttribute("href"); if (videoURL) { onInvidious = hideIcon; return getYouTubeVideoIDFromURL(videoURL); } else { - return false + return false; } } diff --git a/src/utils.ts b/src/utils.ts index c1b3f9b0..fbfbbda7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,7 +2,7 @@ import Config, { VideoDownvotes } from "./config"; import { CategorySelection, SponsorTime, FetchResponse, BackgroundScriptContainer, Registration, HashedValue, VideoID, SponsorHideType } from "./types"; import * as CompileConfig from "../config.json"; -import { findValidElementFromSelector } from "./utils/pageUtils"; +import { findValidElement, findValidElementFromSelector } from "./utils/pageUtils"; import { GenericUtils } from "./utils/genericUtils"; export default class Utils { @@ -22,8 +22,9 @@ export default class Utils { ]; /* Used for waitForElement */ - waitingMutationObserver:MutationObserver = null; - waitingElements: { selector: string, callback: (element: Element) => void }[] = []; + creatingWaitingMutationObserver = false; + waitingMutationObserver: MutationObserver = null; + waitingElements: { selector: string, visibleCheck: boolean, callback: (element: Element) => void }[] = []; constructor(backgroundScriptContainer: BackgroundScriptContainer = null) { this.backgroundScriptContainer = backgroundScriptContainer; @@ -34,40 +35,66 @@ export default class Utils { } /* Uses a mutation observer to wait asynchronously */ - async waitForElement(selector: string): Promise { + async waitForElement(selector: string, visibleCheck = false): Promise { return await new Promise((resolve) => { + const initialElement = this.getElement(selector, visibleCheck); + if (initialElement) { + resolve(initialElement); + return; + } + this.waitingElements.push({ selector, + visibleCheck, callback: resolve }); - if (!this.waitingMutationObserver) { - this.waitingMutationObserver = new MutationObserver(() => { - const foundSelectors = []; - for (const { selector, callback } of this.waitingElements) { - const element = document.querySelector(selector); - if (element) { - callback(element); - foundSelectors.push(selector); - } - } + if (!this.creatingWaitingMutationObserver) { + this.creatingWaitingMutationObserver = true; - this.waitingElements = this.waitingElements.filter((element) => !foundSelectors.includes(element.selector)); - - if (this.waitingElements.length === 0) { - this.waitingMutationObserver.disconnect(); - this.waitingMutationObserver = null; - } - }); - - this.waitingMutationObserver.observe(document.body, { - childList: true, - subtree: true - }); + if (document.body) { + this.setupWaitingMutationListener(); + } else { + window.addEventListener("DOMContentLoaded", () => { + this.setupWaitingMutationListener(); + }); + } } }); } + private setupWaitingMutationListener(): void { + if (!this.waitingMutationObserver) { + this.waitingMutationObserver = new MutationObserver(() => { + const foundSelectors = []; + for (const { selector, visibleCheck, callback } of this.waitingElements) { + const element = this.getElement(selector, visibleCheck); + if (element) { + callback(element); + foundSelectors.push(selector); + } + } + + this.waitingElements = this.waitingElements.filter((element) => !foundSelectors.includes(element.selector)); + + if (this.waitingElements.length === 0) { + this.waitingMutationObserver.disconnect(); + this.waitingMutationObserver = null; + this.creatingWaitingMutationObserver = false; + } + }); + + this.waitingMutationObserver.observe(document.body, { + childList: true, + subtree: true + }); + } + } + + private getElement(selector: string, visibleCheck: boolean) { + return visibleCheck ? findValidElement(document.querySelectorAll(selector)) : document.querySelector(selector); + } + containsPermission(permissions: chrome.permissions.Permissions): Promise { return new Promise((resolve) => { chrome.permissions.contains(permissions, resolve)