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
14 changes: 14 additions & 0 deletions content.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ class YTMBlockController {
artist: ''
};

// Cached media element reference to avoid repeated global DOM querying
this.cachedMediaElement = null;

// Cooldown and Rate Limiting
this.lastClickTime = 0; // Timestamp of the last next-button click
this.cooldownDuration = 1000; // Cooldown rate limit window in milliseconds (1 second)
Expand Down Expand Up @@ -2482,13 +2485,24 @@ class YTMBlockController {
}
}

if (this.cachedMediaElement && this.cachedMediaElement.isConnected) {
if (!this.cachedMediaElement.paused && !this.cachedMediaElement.ended && this.cachedMediaElement.currentTime > 0) {
return true;
}
}

const mediaElements = document.querySelectorAll('audio, video');
for (const media of mediaElements) {
if (!media.paused && !media.ended && media.currentTime > 0) {
this.cachedMediaElement = media;
return true;
}
}

if (mediaElements.length > 0) {
this.cachedMediaElement = mediaElements[0];
}

return false;
}

Expand Down
121 changes: 121 additions & 0 deletions tests/isPlaybackActive.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
const test = require('node:test');
const assert = require('node:assert');

const { YTMBlockController } = require('../content.js');

function setupMockDOM() {
const state = {
querySelectorCalls: 0,
querySelectorAllCalls: 0,
mediaElement: {
tagName: 'VIDEO',
isConnected: true,
paused: false,
ended: false,
currentTime: 10
},
playPauseBtn: null,
mediaSessionState: null
};

try {
Object.defineProperty(navigator, 'mediaSession', {
get() {
return state.mediaSessionState ? { playbackState: state.mediaSessionState } : null;
},
configurable: true
});
} catch (e) {
// Fallback if redefine fails
}

global.document = {
querySelector: (selector) => {
state.querySelectorCalls++;
if (selector.includes('play-pause-button')) {
return state.playPauseBtn;
}
return null;
},
querySelectorAll: (selector) => {
state.querySelectorAllCalls++;
if (selector === 'audio, video') {
return state.mediaElement ? [state.mediaElement] : [];
}
return [];
}
};

return state;
}

test('YTMBlockController - isPlaybackActive', async (t) => {
await t.test('detects active playback from navigator.mediaSession', () => {
const domState = setupMockDOM();
domState.mediaSessionState = 'playing';
const controller = new YTMBlockController();

assert.strictEqual(controller.isPlaybackActive(), true);

domState.mediaSessionState = 'paused';
assert.strictEqual(controller.isPlaybackActive(), false);
});

await t.test('detects active playback from play pause button title/aria-label', () => {
const domState = setupMockDOM();
domState.playPauseBtn = {
getAttribute: (attr) => attr === 'title' ? 'Pause (k)' : ''
};
const controller = new YTMBlockController();

assert.strictEqual(controller.isPlaybackActive(), true);
});

await t.test('detects active playback from DOM media element', () => {
const domState = setupMockDOM();
const controller = new YTMBlockController();

assert.strictEqual(controller.isPlaybackActive(), true);
});

await t.test('caches media element and avoids redundant querySelectorAll calls', () => {
const domState = setupMockDOM();
const controller = new YTMBlockController();

// First call populates cache
assert.strictEqual(controller.isPlaybackActive(), true);
const initialQueryCount = domState.querySelectorAllCalls;

// Second call should hit cache if cached element is connected and playing
assert.strictEqual(controller.isPlaybackActive(), true);

// If caching is implemented, querySelectorAll should NOT be called again
assert.strictEqual(controller.cachedMediaElement, domState.mediaElement);
assert.strictEqual(domState.querySelectorAllCalls, initialQueryCount);
});

await t.test('re-queries DOM when cached media element is disconnected', () => {
const domState = setupMockDOM();
const controller = new YTMBlockController();

controller.isPlaybackActive();
const queryCount1 = domState.querySelectorAllCalls;

// Disconnect cached media element
domState.mediaElement.isConnected = false;

// Create a new connected media element
const newMedia = {
tagName: 'AUDIO',
isConnected: true,
paused: false,
ended: false,
currentTime: 5
};
domState.mediaElement = newMedia;

assert.strictEqual(controller.isPlaybackActive(), true);
assert.strictEqual(domState.querySelectorAllCalls, queryCount1 + 1);
assert.strictEqual(controller.cachedMediaElement, newMedia);
});
});
Loading