diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 162fdbf5d..69fa978f1 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1694,6 +1694,15 @@ "smartSpeedWhitelisted": { "message": "Channel Whitelisted for Smart Speed!" }, + "smartBufferManager": { + "message": "Smart Buffer Manager" + }, + "smartBufferEnable": { + "message": "Enable Smart Buffer Manager" + }, + "smartBufferCeilingSeconds": { + "message": "Buffer ceiling (seconds)" + }, "autoVideoRecovery": { "message": "Auto video recovery" } diff --git a/js&css/web-accessible/core.js b/js&css/web-accessible/core.js index c744c1625..e2e309ca6 100644 --- a/js&css/web-accessible/core.js +++ b/js&css/web-accessible/core.js @@ -530,6 +530,13 @@ document.addEventListener('it-message-from-extension', function () { } else if (ImprovedTube.storage.smart_speed === false) { if(ImprovedTube.heatmap) { ImprovedTube.heatmap.isEnabled = false; document.querySelector("video").playbackRate = 1.0; } } break + case 'smartBuffer': + // When toggled OFF while quality is throttled, smartBufferManager's + // cleanup path restores the user's original quality immediately. + // When toggled ON, the existing playerOnTimeUpdate poll picks it up + // on the next 500ms tick without needing an explicit call here. + ImprovedTube.smartBufferManager(); + break case 'returnYoutubeDislike': if (ImprovedTube.storage.return_youtube_dislike === true) { ImprovedTube.returnYoutubeDislike(); diff --git a/js&css/web-accessible/functions.js b/js&css/web-accessible/functions.js index d2241bda2..f28e09615 100644 --- a/js&css/web-accessible/functions.js +++ b/js&css/web-accessible/functions.js @@ -459,6 +459,7 @@ ImprovedTube.initPlayer = function () { if (ImprovedTube.storage.disable_auto_dubbing === true) { ImprovedTube.observeAutoDubbedMenu(); } if (ImprovedTube.storage.preferred_dubbing_language) { ImprovedTube.preferredDubbingLanguage(); } if (ImprovedTube.storage.player_default_dubbed_language && ImprovedTube.storage.player_default_dubbed_language !== 'disabled') { ImprovedTube.selectDubbedLanguage(); } + ImprovedTube.smartBufferManager(); } }; @@ -486,6 +487,7 @@ ImprovedTube.playerOnTimeUpdate = function () { if (ImprovedTube.storage.always_show_progress_bar === true) { ImprovedTube.showProgressBar(); } if (ImprovedTube.storage.player_remaining_duration === true && document.documentElement.dataset.pageType === 'video') { ImprovedTube.playerRemainingDuration(); } + if (ImprovedTube.storage.smart_buffer === true) { ImprovedTube.smartBufferManager(); } ImprovedTube.played_time += .5; //Counting time of the player playing for the analyzer feature. (not equal to video time if playback speed isnt 1.00) //We can also allow to measure session times too and HID times. diff --git a/js&css/web-accessible/www.youtube.com/player.js b/js&css/web-accessible/www.youtube.com/player.js index 8fc34d888..906195ba4 100644 --- a/js&css/web-accessible/www.youtube.com/player.js +++ b/js&css/web-accessible/www.youtube.com/player.js @@ -615,6 +615,85 @@ ImprovedTube.batteryFeatures = async function () { } }; /*------------------------------------------------------------------------------ +SMART BUFFER MANAGER +Monitors how many seconds of video are buffered ahead of the current playhead. +When the ahead-buffer exceeds the user-configured ceiling, quality is temporarily +lowered to slow down further buffering. Quality is restored once the buffer +drops back below 60% of the ceiling (hysteresis band to prevent oscillation). +This achieves a similar benefit to "releasing" the buffer by preventing +unbounded RAM growth on long videos — via the only mechanism a browser +extension can actually control: stream quality selection. +------------------------------------------------------------------------------*/ +ImprovedTube._sbmThrottled = false; +ImprovedTube._sbmQualityBeforeThrottle = undefined; + +ImprovedTube.smartBufferManager = function () { + if (this.storage.smart_buffer !== true) { + // Cleanup path: if we left quality throttled and the feature is now off, + // restore to the user's preferred quality immediately. + if (this._sbmThrottled && this._sbmQualityBeforeThrottle) { + this.playerQuality(this._sbmQualityBeforeThrottle); + this._sbmQualityBeforeThrottle = undefined; + } + this._sbmThrottled = false; + return; + } + + var video = this.elements.video, + player = this.elements.player; + + if (!video || !player) return; + + // Guard: skip while paused — no buffering occurs and quality changes would + // be confusing to the user if the video is paused mid-watch. + if (video.paused) return; + + // Guard: skip for live streams — they have no finite duration, so a buffer + // ceiling is meaningless. Uses the same check as playerPlaybackSpeed(). + if (player.getVideoData && player.getVideoData().isLive) return; + + // Guard: skip if duration isn't known yet (video still loading metadata). + if (!isFinite(video.duration) || video.duration <= 0) return; + + // --- Calculate seconds buffered ahead of the current playhead --- + var currentTime = video.currentTime, + buffered = video.buffered, + aheadBuffered = 0; + + for (var i = 0; i < buffered.length; i++) { + // Find the buffered range that contains the current playhead. + if (buffered.start(i) <= currentTime && buffered.end(i) > currentTime) { + aheadBuffered = buffered.end(i) - currentTime; + break; + } + } + + // --- Read user-configured ceiling (default: 120 seconds) --- + var ceiling = Number(this.storage.smart_buffer_ceiling_seconds) || 120; + + // --- Throttle decision with hysteresis --- + if (aheadBuffered > ceiling && !this._sbmThrottled) { + // Buffer exceeds ceiling: lower quality to slow down further buffering. + // Store the current quality so we can restore it precisely later. + // This mirrors the exact pattern of ImprovedTube.qualityBeforeBlur in + // playerQualityWithoutFocus(). + if (player.getPlaybackQuality) { + this._sbmQualityBeforeThrottle = player.getPlaybackQuality(); + } + this.playerQuality('large'); // 480p — low enough to slow buffering, still watchable + this._sbmThrottled = true; + } else if (aheadBuffered < (ceiling * 0.6) && this._sbmThrottled) { + // Buffer has dropped to 60% of ceiling: restore user's preferred quality. + // The 60% restore threshold prevents rapid oscillation if the buffer + // hovers just below the ceiling. + if (this._sbmQualityBeforeThrottle) { + this.playerQuality(this._sbmQualityBeforeThrottle); + this._sbmQualityBeforeThrottle = undefined; + } + this._sbmThrottled = false; + } +}; +/*------------------------------------------------------------------------------ FORCED VOLUME ------------------------------------------------------------------------------*/ ImprovedTube.playerVolume = function () { diff --git a/menu/skeleton-parts/player.js b/menu/skeleton-parts/player.js index ea57686e0..860cceb52 100644 --- a/menu/skeleton-parts/player.js +++ b/menu/skeleton-parts/player.js @@ -1359,28 +1359,34 @@ extension.skeleton.main.layers.section.player.on.click = { } } }, - /* - qualityWhenRunningOnBattery: { - component: 'select', - text: 'qualityWhenRunningOnBattery', - options: function () { - return extension.skeleton.main.layers.section.player.on.click.section_1.player_quality.options; - }, + smart_buffer: { + component: 'button', + text: 'smartBufferManager', on: { - render: function () { - extension.skeleton.main.layers.section.player.on.click.section_1.player_quality.on.render.call(this); + click: { + component: 'section', + variant: 'card', + smart_buffer_card: { + component: 'section', + variant: 'card', + smart_buffer: { + component: 'switch', + text: 'smartBufferEnable', + value: false + }, + smart_buffer_ceiling_seconds: { + component: 'slider', + text: 'smartBufferCeilingSeconds', + value: 120, + min: 30, + max: 600, + step: 30, + textarea: true + } + } } } }, - whenBatteryIslowDecreaseQuality: { - component: 'switch', - text: 'whenBatteryIslowDecreaseQuality' - }, - pauseWhileIUnplugTheCharger: { - component: 'switch', - text: 'pauseWhileIUnplugTheCharger' - }, -*/ mini_player: { component: 'switch', text: 'customMiniPlayer'