diff --git a/js&css/extension/init.js b/js&css/extension/init.js index 2f2609eb6..8b47af74d 100644 --- a/js&css/extension/init.js +++ b/js&css/extension/init.js @@ -71,6 +71,18 @@ function finishPageWorldInit() { extension.events.trigger('init'); } +function syncPageWorldAutoplayDisable(callback) { + chrome.storage.local.get('player_autoplay_disable', function (items) { + if (items.player_autoplay_disable === true) { + localStorage['it-player-autoplay-disable'] = 'true'; + } else { + localStorage.removeItem('it-player-autoplay-disable'); + } + + callback(); + }); +} + const pageWorldFiles = [ '/js&css/web-accessible/core.js', '/js&css/web-accessible/functions.js', @@ -89,27 +101,29 @@ const pageWorldFiles = [ '/js&css/web-accessible/init.js' ]; -if ((navigator.userAgent.indexOf('Safari') !== -1 - || (typeof browser !== 'undefined' && browser.runtime?.getURL('')?.startsWith('safari-'))) - && (!/Chrom|Android|Windows|Linux/.test(navigator.userAgent) - || /iPhone|iPad/.test(navigator.userAgent) - ) -) { - - chrome.runtime.sendMessage({ - action: 'inject-main-world', - files: pageWorldFiles - }, function (response) { - if (response && response.ok) { - finishPageWorldInit(); - } else { - console.warn('Falling back to DOM injection for page-world scripts', chrome.runtime.lastError?.message || response?.error); - extension.inject(pageWorldFiles.slice(), finishPageWorldInit); - } - }); -} else { - extension.inject(pageWorldFiles.slice(), finishPageWorldInit); -} +syncPageWorldAutoplayDisable(function () { + if ((navigator.userAgent.indexOf('Safari') !== -1 + || (typeof browser !== 'undefined' && browser.runtime?.getURL('')?.startsWith('safari-'))) + && (!/Chrom|Android|Windows|Linux/.test(navigator.userAgent) + || /iPhone|iPad/.test(navigator.userAgent) + ) + ) { + + chrome.runtime.sendMessage({ + action: 'inject-main-world', + files: pageWorldFiles + }, function (response) { + if (response && response.ok) { + finishPageWorldInit(); + } else { + console.warn('Falling back to DOM injection for page-world scripts', chrome.runtime.lastError?.message || response?.error); + extension.inject(pageWorldFiles.slice(), finishPageWorldInit); + } + }); + } else { + extension.inject(pageWorldFiles.slice(), finishPageWorldInit); + } +}); document.addEventListener('DOMContentLoaded', function () { extension.domReady = true; diff --git a/js&css/web-accessible/core.js b/js&css/web-accessible/core.js index 80059614b..426f7aef5 100644 --- a/js&css/web-accessible/core.js +++ b/js&css/web-accessible/core.js @@ -93,6 +93,28 @@ var ImprovedTube = { defaultApiKey: 'AIzaSyCXRRCFwKAXOiF1JkUBmibzxJF1cPuKNwA' }; +ImprovedTube.syncAutoplayDisableLocalStorage = function () { + if (ImprovedTube.storage.player_autoplay_disable === true) { + localStorage['it-player-autoplay-disable'] = 'true'; + } else { + localStorage.removeItem('it-player-autoplay-disable'); + } +}; + +ImprovedTube.shouldPreventInitialAutoplay = function (video) { + if (ImprovedTube.user_interacted || !location.href.includes('/watch?') || location.href.includes('list=')) { + return false; + } + + if (localStorage['it-player-autoplay-disable'] !== 'true' && ImprovedTube.storage.player_autoplay_disable !== true) { + return false; + } + + var player = video.closest && (video.closest('.html5-video-player') || video.closest('#movie_player')); + + return !player || !player.classList || !player.classList.contains('ad-showing'); +}; + /*-------------------------------------------------------------- CODEC || 30FPS ---------------------------------------------------------------- @@ -125,6 +147,29 @@ if (localStorage['it-codec'] || localStorage['it-player30fps']) { } }; +HTMLMediaElement.prototype.play = (function (original) { + if (original.improvedTubeInitialAutoplayGuard) { + return original; + } + + function play() { + if (ImprovedTube.shouldPreventInitialAutoplay(this)) { + try { + this.pause(); + } catch (error) { + } + + return Promise.resolve(); + } + + return original.apply(this, arguments); + } + + play.improvedTubeInitialAutoplayGuard = true; + + return play; +})(HTMLMediaElement.prototype.play); + /*-------------------------------------------------------------- # MESSAGES ---------------------------------------------------------------- @@ -182,6 +227,7 @@ document.addEventListener('it-message-from-extension', function () { if (message.action === 'storage-loaded') { ImprovedTube.storage = message.storage; + ImprovedTube.syncAutoplayDisableLocalStorage(); if (ImprovedTube.storage.block_vp9 || ImprovedTube.storage.block_av1 || ImprovedTube.storage.block_h264) { let atlas = { block_vp9: 'vp9|vp09', block_h264: 'avc1', block_av1: 'av01' }, @@ -233,6 +279,9 @@ document.addEventListener('it-message-from-extension', function () { localStorage.removeItem('it-player30fps'); } } + if (message.key === 'player_autoplay_disable') { + ImprovedTube.syncAutoplayDisableLocalStorage(); + } switch (camelized_key) { case 'blocklist': case 'blocklistActivate': diff --git a/tests/unit/autoplay-disable.test.js b/tests/unit/autoplay-disable.test.js new file mode 100644 index 000000000..13eafb7c4 --- /dev/null +++ b/tests/unit/autoplay-disable.test.js @@ -0,0 +1,75 @@ +describe('initial autoplay guard', () => { + let originalPlay; + let video; + + function loadCore() { + jest.resetModules(); + + const storage = {}; + storage.removeItem = key => { + delete storage[key]; + }; + + global.localStorage = storage; + global.location = { + href: 'https://www.youtube.com/watch?v=abcdefghijk' + }; + global.window = {}; + global.CustomEvent = function CustomEvent(type) { + this.type = type; + }; + global.document = { + addEventListener: jest.fn(), + createElement: jest.fn(() => ({style: {}})), + dispatchEvent: jest.fn(), + documentElement: { + appendChild: jest.fn() + }, + querySelector: jest.fn() + }; + + originalPlay = jest.fn(() => 'played'); + global.HTMLMediaElement = function HTMLMediaElement() {}; + global.HTMLMediaElement.prototype.play = originalPlay; + + require('../../js&css/web-accessible/core.js'); + + video = { + closest: jest.fn(() => ({ + classList: { + contains: jest.fn(() => false) + } + })), + pause: jest.fn() + }; + } + + beforeEach(() => { + loadCore(); + }); + + afterEach(() => { + delete global.CustomEvent; + delete global.document; + delete global.HTMLMediaElement; + delete global.localStorage; + delete global.location; + delete global.window; + }); + + test('prevents the first direct watch-page play when autoplay is disabled', async () => { + localStorage['it-player-autoplay-disable'] = 'true'; + + await expect(HTMLMediaElement.prototype.play.call(video)).resolves.toBeUndefined(); + + expect(video.pause).toHaveBeenCalledTimes(1); + expect(originalPlay).not.toHaveBeenCalled(); + }); + + test('allows playback when autoplay is not disabled', () => { + expect(HTMLMediaElement.prototype.play.call(video)).toBe('played'); + + expect(video.pause).not.toHaveBeenCalled(); + expect(originalPlay).toHaveBeenCalledTimes(1); + }); +});