Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented "Prevent playback looping of youtube shorts video" #2819

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion js&css/web-accessible/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,10 @@ document.addEventListener('it-message-from-extension', function () {
ImprovedTube.elements.player.querySelector('video').playbackRate = 1;
}
break


case 'preventShortsLooping':
ImprovedTube.preventShortsLooping();
break;
case 'theme':
case 'themePrimaryColor':
case 'themeTextColor':
Expand Down
12 changes: 12 additions & 0 deletions js&css/web-accessible/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,13 @@ ImprovedTube.init = function () {
this.YouTubeExperiments();
this.channelCompactTheme();


let preventShortsLooping = localStorage.getItem("prevent_shorts_looping") === "true";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need local storage? its not available in incognito mode.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed it to chrome.storage.local so it works in incognito mode now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then 💡 it sounds like you might be the one person in the world now, who is likely to bring forward:
#1408 (comment)

if (preventShortsLooping) {
console.log("Prevent Shorts Looping is active!");
ImprovedTube.preventShortsLooping();
}

if (ImprovedTube.elements.player && ImprovedTube.elements.player.setPlaybackRate) {
ImprovedTube.videoPageUpdate();
ImprovedTube.initPlayer();
Expand Down Expand Up @@ -147,6 +154,11 @@ document.addEventListener('yt-navigate-finish', function () {
} else if (document.documentElement.dataset.pageType === 'channel') {
ImprovedTube.channelPlayAllButton();
}

let preventShortsLooping = localStorage.getItem("prevent_shorts_looping") === "true";
if (preventShortsLooping) {
ImprovedTube.preventShortsLooping();
}
});

window.addEventListener('load', function () {
Expand Down
251 changes: 239 additions & 12 deletions js&css/web-accessible/www.youtube.com/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -706,16 +706,19 @@ REPEAT
ImprovedTube.playerRepeat = function () {
setTimeout(function () {
if (!/ad-showing/.test(ImprovedTube.elements.player.className)) {
ImprovedTube.elements.video.setAttribute('loop', '');
// Prevent looping for Shorts videos
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if ( Improvedtube.storage.preventLoopingShorts === true) (...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reverted it back to the original code for the repeat section and functionality remains the same.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if ( Improvedtube.storage.preventLoopingShorts === true) (...

How would I go about making the toggle for this feature work in the menu? I have a Prevent Shorts Looping toggle option in the player settings but it doesn't function currently and I can't seem to figure out what else to do for it.

Copy link
Member

@ImprovedTube ImprovedTube Feb 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's good, no need to revert., just to make conditional to a setting of the extension

sorry i didnt read all yet. compare to the the code for our always repeat button for example

if (this.storage.player_always_repeat === true) { ImprovedTube.playerRepeat(); };

these variables are managed automatically through adding it in the menu:

player_always_repeat: {
component: 'switch',
text: 'alwaysActive',
on: {
click: function () {
if (this.dataset.value === 'true') {
document.getElementById('player_repeat_button').flip(true);
}
}
}
},
player_screenshot_button: {
component: 'switch',
text: 'Screenshot',
id: 'player_screenshot_button'
},
embed_subtitle: {
component: 'switch',
text: 'Subtitle_Capture_including_the_current_words',

( and we also add them as attributes to the root element for convenience. ) (while L1080-L1086 are rare/optional/disputable for visually suggesting and imposing to switch on the repeat button then too, for explaining that "always repeat" is a big change)

if (!location.href.includes("shorts/")) {
ImprovedTube.elements.video.setAttribute('loop', '');
}
}
//ImprovedTube.elements.buttons['it-repeat-styles'].style.opacity = '1'; //old class from version 3.x? that both repeat buttons could have
}, 200);
}
}, 200);
};

/*------------------------------------------------------------------------------
REPEAT BUTTON
------------------------------------------------------------------------------*/
ImprovedTube.playerRepeatButton = function () {
if (this.storage.player_repeat_button === true) {
if (this.storage.player_repeat_button === true && !location.href.includes("shorts/")) {
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
svg.setAttributeNS(null, 'viewBox', '0 0 24 24');
Expand Down Expand Up @@ -1517,10 +1520,234 @@ ImprovedTube.pauseWhileTypingOnYoutube = function () {
/*------------------------------------------------------------------------------
HIDE PROGRESS BAR PREVIEW
------------------------------------------------------------------------------*/
ImprovedTube.playerHideProgressPreview = function () {
if (this.storage.player_hide_progress_preview === true) {
document.documentElement.setAttribute('it-hide-progress-preview', 'true');
} else {
document.documentElement.removeAttribute('it-hide-progress-preview');
}
};



/*------------------------------------------------------------------------------
SHORTS LOOP PREVENTION & AD DETECTION
Prevent Shorts from looping indefinitely.
------------------------------------------------------------------------------*/
(function () {
console.log("Initializing Shorts Loop Prevention");

let observer = null;
let lastVideoId = "";
let currentVideo = null;
let preloadedVideos = new Map();
let videoStartTime = new WeakMap();
let userInteractedVideos = new WeakSet();
let videoWasPausedByScript = new WeakSet();

function getShortsVideoElement() {
return document.querySelector("#shorts-player video");
}

function resetVideoListeners(video) {
if (!video) return;
console.log("Resetting event listeners for previous video");
video.onended = null;
video.onseeked = null;
video.removeEventListener("timeupdate", handleTimeUpdate);
video.removeEventListener("play", trackUserPlay);
video.removeEventListener("pause", trackPauseEvent);
videoStartTime.delete(video);
userInteractedVideos.delete(video);
videoWasPausedByScript.delete(video);
}

function handleTimeUpdate(event) {
let video = event.target;

if (!userInteractedVideos.has(video) || video.currentTime < 1) {
return;
}

if (video.currentTime < 0.5 && !video.paused && !videoWasPausedByScript.has(video)) {
console.log("Detected auto-restart! Checking if stopping is safe.");
if (!isNaN(video.duration) && video.duration !== Infinity && video.duration > 0) {
console.log("Stopping video.");
video.pause();
video.currentTime = video.duration - 0.01;
videoWasPausedByScript.add(video);
} else {
console.warn("Skipped stopping video due to invalid duration:", video.duration);
}
}
}

function trackUserPlay(event) {
let video = event.target;
console.log("User interacted. Allowing natural playback.");
userInteractedVideos.add(video);
video.removeEventListener("play", trackUserPlay);
}

function trackPauseEvent(event) {
let video = event.target;
if (!videoWasPausedByScript.has(video)) {
console.log("User paused manually. Ignoring.");
videoWasPausedByScript.add(video);
}
}

function preventShortsLoop() {
let video = getShortsVideoElement();
if (!video) return;

let videoId = new URLSearchParams(window.location.search).get("v") || document.location.href;

if (preloadedVideos.has(video.src)) {
let preloadedData = preloadedVideos.get(video.src);
if (preloadedData.isAd) {
console.log("Ad detected from preload.");
resetVideoListeners(video);
return;
}
}

if (videoId === lastVideoId) {
console.log("Already processed this video. Skipping.");
return;
}

console.log("New Shorts video detected! Applying loop prevention.");
lastVideoId = videoId;

resetVideoListeners(currentVideo);
currentVideo = video;

videoStartTime.set(video, Date.now());

function handleLoadedMetadata() {
console.log("Video metadata loaded!");

video.onended = function () {
if (!this.paused) {
console.log("Video ended, pausing.");
this.pause();
}
};

video.onseeked = function () {
let startTime = videoStartTime.get(video) || 0;
let timeElapsed = (Date.now() - startTime) / 1000;

if (timeElapsed < 2 || videoWasPausedByScript.has(video)) {
console.log("Ignoring seek event (video just started or script-paused)");
return;
}

console.log("Blocked seek event!");
this.pause();
};

video.addEventListener("timeupdate", handleTimeUpdate);
video.addEventListener("pause", trackPauseEvent);
}

video.addEventListener("play", trackUserPlay, { once: true });

if (video.readyState >= 2) {
handleLoadedMetadata();
} else {
video.addEventListener("loadeddata", handleLoadedMetadata, { once: true });
}

preloadNextVideos();
}

function preloadNextVideos() {
console.log("Preloading metadata for upcoming Shorts...");
let videos = document.querySelectorAll("#shorts-player video");

videos.forEach(video => {
if (!preloadedVideos.has(video.src)) {
video.addEventListener("loadedmetadata", function () {
let isAd = detectAdDuringPreload(video);
preloadedVideos.set(video.src, { duration: video.duration, isAd: isAd });

console.log(`Preloaded video: ${video.src}`);
console.log(` - Duration: ${video.duration}s`);
console.log(` - Detected as Ad? ${isAd}`);
}, { once: true });
}
});
}

function detectAdDuringPreload(video) {
if (!video) return false;

const player = video.closest('.html5-video-player') || video.closest('#movie_player');
if (player && player.classList.contains('ad-showing')) {
console.warn("Ad detected during preload (ad-showing class).");
return true;
}

if (video.closest("ytd-ad-slot-renderer, ytd-in-feed-ad-layout-renderer")) {
console.warn("Shorts ad detected during preload (Ad container found).");
return true;
}

let adBadge = document.querySelector(".badge-shape-wiz_text");
if (adBadge && adBadge.innerText.includes("Sponsored")) {
console.warn("Sponsored Shorts detected during preload.");
return true;
}

let skipButton = document.querySelector('.ytp-ad-skip-button-modern.ytp-button,[class*="ytp-ad-skip-button"].ytp-button');
if (skipButton) {
console.warn("Skippable Ad detected during preload.");
return true;
}

if (isNaN(video.duration) || video.duration === Infinity || video.duration < 3) {
console.warn("Possible ad detected during preload (Invalid duration).");
return true;
}

return false;
}

function observeShortsChanges() {
const shortsContainer = document.querySelector("#shorts-player");
if (!shortsContainer) {
console.warn("Shorts container not found. Retrying...");
setTimeout(observeShortsChanges, 1000);
return;
}

if (observer) observer.disconnect();

observer = new MutationObserver(() => {
let video = getShortsVideoElement();
if (video && video !== currentVideo) {
console.log("Shorts video changed! Reapplying loop prevention.");
preventShortsLoop();
}
});

observer.observe(shortsContainer, { childList: true, subtree: false });
}

document.addEventListener("DOMContentLoaded", preventShortsLoop);
document.addEventListener("yt-navigate-finish", preventShortsLoop);
})();



















16 changes: 16 additions & 0 deletions js&css/web-accessible/www.youtube.com/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,19 @@ ImprovedTube.youtubeLanguage = function () {
}
}
};

/*------------------------------------------------------------------------------
PREVENT SHORTS LOOPING
------------------------------------------------------------------------------*/

ImprovedTube.preventShortsLooping = function () {
let value = this.storage.prevent_shorts_looping;

if (value === true) {
console.log("Prevent Shorts Looping: Enabled");
localStorage.setItem("prevent_shorts_looping", "true");
} else {
console.log("Prevent Shorts Looping: Disabled");
localStorage.setItem("prevent_shorts_looping", "false");
}
};
20 changes: 20 additions & 0 deletions menu/skeleton-parts/general.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,26 @@ extension.skeleton.main.layers.section.general = {
section_1: {
component: 'section',
variant: 'card',
prevent_shorts_looping: {
component: 'switch',
text: 'Prevent Shorts Looping',
storage: 'prevent_shorts_looping',
tags: 'shorts looping auto-play',

on: {
change: function (event) {
const isEnabled = event.target.checked;
console.log("🔄 Prevent Shorts Looping is now " + (isEnabled ? "ENABLED" : "DISABLED"));


ImprovedTube.storage.prevent_shorts_looping = isEnabled;

if (isEnabled) {
preventShortsLoop();
}
}
}
},
improvedtube_youtube_icon: {
text: 'improvedtubeIconOnYoutube',
component: 'select',
Expand Down
13 changes: 13 additions & 0 deletions menu/skeleton-parts/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,19 @@ extension.skeleton.main.layers.section.player.on.click = {
section_1: {
component: 'section',
variant: 'card',
prevent_shorts_looping: {
component: 'switch',
text: 'Prevent Shorts Looping',
storage: 'player_prevent_shorts_looping',
id: 'player_prevent_shorts_looping',
on: {
click: function () {
let isEnabled = this.dataset.value === 'true';
console.log(`🔁 Prevent Shorts Looping: ${isEnabled ? "Enabled" : "Disabled"}`);
chrome.storage.local.set({ "player_prevent_shorts_looping": isEnabled });
}
}
},
autopause_when_switching_tabs: {
component: 'switch',
text: 'autopauseWhenSwitchingTabs',
Expand Down