Skip to content

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

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

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
13 changes: 13 additions & 0 deletions js&css/web-accessible/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,14 @@ ImprovedTube.init = function () {
this.YouTubeExperiments();
this.channelCompactTheme();


chrome.storage.local.get("prevent_shorts_looping", function (data) {
if (data.prevent_shorts_looping) {
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 +155,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
241 changes: 233 additions & 8 deletions js&css/web-accessible/www.youtube.com/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -711,11 +711,12 @@ ImprovedTube.playerRepeat = function () {
//ImprovedTube.elements.buttons['it-repeat-styles'].style.opacity = '1'; //old class from version 3.x? that both repeat buttons could have
}, 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 +1518,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