mirror of
https://github.com/ajayyy/SponsorBlock.git
synced 2025-12-07 12:07:11 +03:00
Merge pull request #1116 from mchangrh/embeddedVideos
Support embedded videos
This commit is contained in:
@@ -17,7 +17,7 @@ import { getCategoryActionType } from "./utils/categoryUtils";
|
|||||||
import { SkipButtonControlBar } from "./js-components/skipButtonControlBar";
|
import { SkipButtonControlBar } from "./js-components/skipButtonControlBar";
|
||||||
import { Tooltip } from "./render/Tooltip";
|
import { Tooltip } from "./render/Tooltip";
|
||||||
import { getStartTimeFromUrl } from "./utils/urlParser";
|
import { getStartTimeFromUrl } from "./utils/urlParser";
|
||||||
import { getControls } from "./utils/pageUtils";
|
import { findValidElement, getControls, isVisible } from "./utils/pageUtils";
|
||||||
import { CategoryPill } from "./render/CategoryPill";
|
import { CategoryPill } from "./render/CategoryPill";
|
||||||
import { AnimationUtils } from "./utils/animationUtils";
|
import { AnimationUtils } from "./utils/animationUtils";
|
||||||
import { GenericUtils } from "./utils/genericUtils";
|
import { GenericUtils } from "./utils/genericUtils";
|
||||||
@@ -91,7 +91,8 @@ let controls: HTMLElement | null = null;
|
|||||||
const playerButtons: Record<string, {button: HTMLButtonElement, image: HTMLImageElement, setupListener: boolean}> = {};
|
const playerButtons: Record<string, {button: HTMLButtonElement, image: HTMLImageElement, setupListener: boolean}> = {};
|
||||||
|
|
||||||
// Direct Links after the config is loaded
|
// Direct Links after the config is loaded
|
||||||
utils.wait(() => Config.config !== null, 1000, 1).then(() => videoIDChange(getYouTubeVideoID(document.URL)));
|
utils.wait(() => Config.config !== null, 1000, 1).then(() => videoIDChange(getYouTubeVideoID(document)));
|
||||||
|
addPageListeners();
|
||||||
addHotkeyListener();
|
addHotkeyListener();
|
||||||
|
|
||||||
//the amount of times the sponsor lookup has retried
|
//the amount of times the sponsor lookup has retried
|
||||||
@@ -142,7 +143,7 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
|
|||||||
//messages from popup script
|
//messages from popup script
|
||||||
switch(request.message){
|
switch(request.message){
|
||||||
case "update":
|
case "update":
|
||||||
videoIDChange(getYouTubeVideoID(document.URL));
|
videoIDChange(getYouTubeVideoID(document));
|
||||||
break;
|
break;
|
||||||
case "sponsorStart":
|
case "sponsorStart":
|
||||||
startOrEndTimingNewSegment()
|
startOrEndTimingNewSegment()
|
||||||
@@ -272,8 +273,8 @@ function resetValues() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function videoIDChange(id) {
|
async function videoIDChange(id) {
|
||||||
//if the id has not changed return
|
//if the id has not changed return unless the video element has changed
|
||||||
if (sponsorVideoID === id) return;
|
if (sponsorVideoID === id && isVisible(video)) return;
|
||||||
|
|
||||||
//set the global videoID
|
//set the global videoID
|
||||||
sponsorVideoID = id;
|
sponsorVideoID = id;
|
||||||
@@ -383,7 +384,7 @@ function createPreviewBar(): void {
|
|||||||
];
|
];
|
||||||
|
|
||||||
for (const selector of progressElementSelectors) {
|
for (const selector of progressElementSelectors) {
|
||||||
const el = document.querySelector<HTMLElement>(selector);
|
const el = findValidElement(document.querySelectorAll(selector));
|
||||||
|
|
||||||
if (el) {
|
if (el) {
|
||||||
previewBar = new PreviewBar(el, onMobileYouTube, onInvidious);
|
previewBar = new PreviewBar(el, onMobileYouTube, onInvidious);
|
||||||
@@ -404,6 +405,16 @@ function durationChangeListener(): void {
|
|||||||
updatePreviewBar();
|
updatePreviewBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggered once the video is ready.
|
||||||
|
* This is mainly to attach to embedded players who don't have a video element visible.
|
||||||
|
*/
|
||||||
|
function videoOnReadyListener(): void {
|
||||||
|
createPreviewBar();
|
||||||
|
updatePreviewBar();
|
||||||
|
createButtons();
|
||||||
|
}
|
||||||
|
|
||||||
function cancelSponsorSchedule(): void {
|
function cancelSponsorSchedule(): void {
|
||||||
if (currentSkipSchedule !== null) {
|
if (currentSkipSchedule !== null) {
|
||||||
clearTimeout(currentSkipSchedule);
|
clearTimeout(currentSkipSchedule);
|
||||||
@@ -515,7 +526,7 @@ function inMuteSegment(currentTime: number): boolean {
|
|||||||
* This makes sure the videoID is still correct and if the sponsorTime is included
|
* This makes sure the videoID is still correct and if the sponsorTime is included
|
||||||
*/
|
*/
|
||||||
function incorrectVideoCheck(videoID?: string, sponsorTime?: SponsorTime): boolean {
|
function incorrectVideoCheck(videoID?: string, sponsorTime?: SponsorTime): boolean {
|
||||||
const currentVideoID = getYouTubeVideoID(document.URL);
|
const currentVideoID = getYouTubeVideoID(document);
|
||||||
if (currentVideoID !== (videoID || sponsorVideoID) || (sponsorTime
|
if (currentVideoID !== (videoID || sponsorVideoID) || (sponsorTime
|
||||||
&& (!sponsorTimes || !sponsorTimes?.some((time) => time.segment === sponsorTime.segment))
|
&& (!sponsorTimes || !sponsorTimes?.some((time) => time.segment === sponsorTime.segment))
|
||||||
&& !sponsorTimesSubmitting.some((time) => time.segment === sponsorTime.segment))) {
|
&& !sponsorTimesSubmitting.some((time) => time.segment === sponsorTime.segment))) {
|
||||||
@@ -546,7 +557,7 @@ function setupVideoMutationListener() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function refreshVideoAttachments() {
|
function refreshVideoAttachments() {
|
||||||
const newVideo = document.querySelector('video');
|
const newVideo = findValidElement(document.querySelectorAll('video')) as HTMLVideoElement;
|
||||||
if (newVideo && newVideo !== video) {
|
if (newVideo && newVideo !== video) {
|
||||||
video = newVideo;
|
video = newVideo;
|
||||||
|
|
||||||
@@ -557,11 +568,20 @@ function refreshVideoAttachments() {
|
|||||||
setupSkipButtonControlBar();
|
setupSkipButtonControlBar();
|
||||||
setupCategoryPill();
|
setupCategoryPill();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a new bar in the new video element
|
||||||
|
if (previewBar && !utils.findReferenceNode()?.contains(previewBar.container)) {
|
||||||
|
previewBar.remove();
|
||||||
|
previewBar = null;
|
||||||
|
|
||||||
|
createPreviewBar();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupVideoListeners() {
|
function setupVideoListeners() {
|
||||||
//wait until it is loaded
|
//wait until it is loaded
|
||||||
|
video.addEventListener('loadstart', videoOnReadyListener)
|
||||||
video.addEventListener('durationchange', durationChangeListener);
|
video.addEventListener('durationchange', durationChangeListener);
|
||||||
|
|
||||||
if (!Config.config.disableSkipping) {
|
if (!Config.config.disableSkipping) {
|
||||||
@@ -653,7 +673,7 @@ function setupCategoryPill() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function sponsorsLookup(id: string, keepOldSubmissions = true) {
|
async function sponsorsLookup(id: string, keepOldSubmissions = true) {
|
||||||
if (!video) refreshVideoAttachments();
|
if (!video || !isVisible(video)) refreshVideoAttachments();
|
||||||
//there is still no video here
|
//there is still no video here
|
||||||
if (!video) {
|
if (!video) {
|
||||||
setTimeout(() => sponsorsLookup(id), 100);
|
setTimeout(() => sponsorsLookup(id), 100);
|
||||||
@@ -924,8 +944,30 @@ async function getVideoInfo(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getYouTubeVideoID(url: string): string | boolean {
|
function getYouTubeVideoID(document: Document): string | boolean {
|
||||||
// For YouTube TV support
|
const url = document.URL;
|
||||||
|
// 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 and don't hide if on /embed/
|
||||||
|
if (url.includes("/embed/")) return getYouTubeVideoIDFromDocument(document, false);
|
||||||
|
// skip to document if matches pattern
|
||||||
|
if (url.includes("/channel/") || url.includes("/user/") || url.includes("/c/")) return getYouTubeVideoIDFromDocument(document);
|
||||||
|
// not sure, try URL then document
|
||||||
|
return getYouTubeVideoIDFromURL(url) || getYouTubeVideoIDFromDocument(document);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getYouTubeVideoIDFromDocument(document: Document, hideIcon = true): string | boolean {
|
||||||
|
// get ID from document (channel trailer / embedded playlist)
|
||||||
|
const videoURL = document.querySelector("[data-sessionlink='feature=player-title']")?.getAttribute("href");
|
||||||
|
if (videoURL) {
|
||||||
|
onInvidious = hideIcon;
|
||||||
|
return getYouTubeVideoIDFromURL(videoURL);
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getYouTubeVideoIDFromURL(url: string): string | boolean {
|
||||||
if(url.startsWith("https://www.youtube.com/tv#/")) url = url.replace("#", "");
|
if(url.startsWith("https://www.youtube.com/tv#/")) url = url.replace("#", "");
|
||||||
|
|
||||||
//Attempt to parse url
|
//Attempt to parse url
|
||||||
@@ -945,7 +987,7 @@ function getYouTubeVideoID(url: string): string | boolean {
|
|||||||
} else if (!["m.youtube.com", "www.youtube.com", "www.youtube-nocookie.com", "music.youtube.com"].includes(urlObject.host)) {
|
} else if (!["m.youtube.com", "www.youtube.com", "www.youtube-nocookie.com", "music.youtube.com"].includes(urlObject.host)) {
|
||||||
if (!Config.config) {
|
if (!Config.config) {
|
||||||
// Call this later, in case this is an Invidious tab
|
// Call this later, in case this is an Invidious tab
|
||||||
utils.wait(() => Config.config !== null).then(() => videoIDChange(getYouTubeVideoID(url)));
|
utils.wait(() => Config.config !== null).then(() => videoIDChange(getYouTubeVideoIDFromURL(url)));
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@@ -1862,6 +1904,16 @@ function getSegmentsMessage(sponsorTimes: SponsorTime[]): string {
|
|||||||
return sponsorTimesMessage;
|
return sponsorTimesMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addPageListeners(): void {
|
||||||
|
const refreshListners = () => {
|
||||||
|
if (!isVisible(video)) {
|
||||||
|
refreshVideoAttachments();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("yt-navigate-finish", refreshListners);
|
||||||
|
}
|
||||||
|
|
||||||
function addHotkeyListener(): void {
|
function addHotkeyListener(): void {
|
||||||
document.addEventListener("keydown", hotkeyListener);
|
document.addEventListener("keydown", hotkeyListener);
|
||||||
}
|
}
|
||||||
|
|||||||
15
src/utils.ts
15
src/utils.ts
@@ -2,6 +2,7 @@ import Config from "./config";
|
|||||||
import { CategorySelection, SponsorTime, FetchResponse, BackgroundScriptContainer, Registration } from "./types";
|
import { CategorySelection, SponsorTime, FetchResponse, BackgroundScriptContainer, Registration } from "./types";
|
||||||
|
|
||||||
import * as CompileConfig from "../config.json";
|
import * as CompileConfig from "../config.json";
|
||||||
|
import { findValidElementFromSelector } from "./utils/pageUtils";
|
||||||
import { GenericUtils } from "./utils/genericUtils";
|
import { GenericUtils } from "./utils/genericUtils";
|
||||||
|
|
||||||
export default class Utils {
|
export default class Utils {
|
||||||
@@ -329,11 +330,15 @@ export default class Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
findReferenceNode(): HTMLElement {
|
findReferenceNode(): HTMLElement {
|
||||||
let referenceNode = document.getElementById("player-container-id")
|
const selectors = [
|
||||||
?? document.getElementById("movie_player")
|
"#player-container-id",
|
||||||
?? document.querySelector("#main-panel.ytmusic-player-page") // YouTube music
|
"#movie_player",
|
||||||
?? document.querySelector("#player-container .video-js") // Invidious
|
"#c4-player", // Channel Trailer
|
||||||
?? document.querySelector(".main-video-section > .video-container"); // Cloudtube
|
"#main-panel.ytmusic-player-page", // YouTube music
|
||||||
|
"#player-container .video-js", // Invidious
|
||||||
|
".main-video-section > .video-container" // Cloudtube
|
||||||
|
]
|
||||||
|
let referenceNode = findValidElementFromSelector(selectors)
|
||||||
if (referenceNode == null) {
|
if (referenceNode == null) {
|
||||||
//for embeds
|
//for embeds
|
||||||
const player = document.getElementById("player");
|
const player = document.getElementById("player");
|
||||||
|
|||||||
@@ -18,3 +18,26 @@ export function getControls(): HTMLElement | false {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isVisible(element: HTMLElement): boolean {
|
||||||
|
return element && element.offsetWidth > 0 && element.offsetHeight > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findValidElementFromSelector(selectors: string[]): HTMLElement {
|
||||||
|
return findValidElementFromGenerator(selectors, (selector) => document.querySelector(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findValidElement(elements: HTMLElement[] | NodeListOf<HTMLElement>): HTMLElement {
|
||||||
|
return findValidElementFromGenerator(elements);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findValidElementFromGenerator<T>(objects: T[] | NodeListOf<HTMLElement>, generator?: (obj: T) => HTMLElement): HTMLElement {
|
||||||
|
for (const obj of objects) {
|
||||||
|
const element = generator ? generator(obj as T) : obj as HTMLElement;
|
||||||
|
if (element && isVisible(element)) {
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user