Skip to content
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
9 changes: 9 additions & 0 deletions _locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
7 changes: 7 additions & 0 deletions js&css/web-accessible/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions js&css/web-accessible/functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
};

Expand Down Expand Up @@ -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.
Expand Down
79 changes: 79 additions & 0 deletions js&css/web-accessible/www.youtube.com/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
42 changes: 24 additions & 18 deletions menu/skeleton-parts/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down