Skip to content
Merged
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
37 changes: 31 additions & 6 deletions js&css/web-accessible/www.youtube.com/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
FORCED PLAY VIDEO FROM THE BEGINNING
------------------------------------------------------------------------------*/
ImprovedTube.forcedPlayVideoFromTheBeginning = function () {
const player = this.elements.player, video = this.elements.video, paused = video?.paused;
const player = this.elements.player, video = this.elements.video;
const t = this.video_url.match(this.regex.video_time)?.[1];
if (t) {
if (/[#&]stop=|#t=/.test(this.video_url)) return;
Expand All @@ -16,11 +16,36 @@ ImprovedTube.forcedPlayVideoFromTheBeginning = function () {
// opening moments. Only seek when YouTube has resumed from a saved
// timestamp (currentTime > 0), which is the case this setting exists
// to override.
if (video.currentTime > 1.1) { // video.currentTime = 0; #262
player.seekTo(0);
// restore previous paused state after the seek
if (paused) { player.pauseVideo(); }
}
const forceRestart = () => {
if (video.currentTime > 1.1) { // video.currentTime = 0; #262
const wasPaused = video.paused;
player.seekTo(0);
// restore previous paused state after the seek
if (wasPaused) { player.pauseVideo(); }
}
};

forceRestart();

// On SPA navigations (playlist auto-advance, clicking a previously
// watched video from recommendations/history) YouTube applies its own
// "resume to saved position" seek asynchronously, after the
// synchronous check above already ran with currentTime still ~0. Catch
// that late seek once, bounded to a short grace window, instead of
// blindly re-polling. #4349
const navigatedUrl = this.video_url;
let settled = false;
const onSeeked = () => {
if (settled || this.video_url !== navigatedUrl) return;
settled = true;
video.removeEventListener('seeked', onSeeked);
forceRestart();
};
video.addEventListener('seeked', onSeeked);
setTimeout(() => {
settled = true;
video.removeEventListener('seeked', onSeeked);
}, 2000);
}
};
/*------------------------------------------------------------------------------
Expand Down
52 changes: 52 additions & 0 deletions tests/unit/play-video-from-beginning.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const fs = require('fs');
const path = require('path');

describe('Play video from the beginning', () => {
let playerContent;
let functionsContent;

beforeAll(() => {
playerContent = fs.readFileSync(
path.join(__dirname, '../../js&css/web-accessible/www.youtube.com/player.js'),
'utf8'
);
functionsContent = fs.readFileSync(
path.join(__dirname, '../../js&css/web-accessible/functions.js'),
'utf8'
);
});

test('forcedPlayVideoFromTheBeginning should be defined on ImprovedTube', () => {
expect(playerContent).toContain('ImprovedTube.forcedPlayVideoFromTheBeginning = function');
});

test('should still guard against the double-play of a fresh video (#262)', () => {
expect(playerContent).toContain("if (video.currentTime > 1.1) { // video.currentTime = 0; #262");
});

test('should catch YouTube resuming playback asynchronously after SPA navigation (#4349)', () => {
// The initial, synchronous check alone misses playlist auto-advance and
// clicking a previously watched video, because YouTube applies its own
// "resume to saved position" seek after that check already ran. A
// bounded one-shot 'seeked' listener re-checks once that late seek
// actually happens.
expect(playerContent).toContain("video.addEventListener('seeked', onSeeked)");
expect(playerContent).toContain("video.removeEventListener('seeked', onSeeked)");
});

test('the async re-check should be bounded to a short grace window', () => {
expect(playerContent).toContain('setTimeout(() => {');
expect(playerContent).toContain('}, 2000)');
});

test('the async re-check should bail out if the page has since navigated to a different video', () => {
expect(playerContent).toContain('this.video_url !== navigatedUrl');
});

test('initPlayer should call forcedPlayVideoFromTheBeginning first', () => {
const initPlayerMatch = functionsContent.match(/ImprovedTube\.initPlayer\s*=\s*function[^{]*\{([\s\S]*?)\n\};/);
expect(initPlayerMatch).not.toBeNull();
const body = initPlayerMatch[1];
expect(body).toContain('ImprovedTube.forcedPlayVideoFromTheBeginning();');
});
});