diff --git a/CLAUDE.md b/CLAUDE.md index 63931934..f023b889 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -618,9 +618,11 @@ then woke a fresh background with no record of the user's toolbar choice, so an allowlisted site auto-started MPV again even after the user had picked the in-page player or Off. The same loss dropped the one-hand-off-per-page latch, so the page's next stream request after a wake opened a second mpv window. -`TabTracker.saveTabState` now writes `url`, `isOn`, `isMpv`, `regexMatched`, +`TabTracker.saveTabState` now writes `url`, `isOn`, `isMpv`, `mpvOnPlay`, `regexMatched`, `mpvMatched` and `mpvAutoOpened` per tab to `chrome.storage.session` whenever one of them changes (toolbar click, URL change, mpv hand-off and its failure), +and since 2026-10-03 (#158) the failure shown as "!" (`mpvError`) and the shortcut's +waiting play (`mpvPlayPendingUntil`, `mpvPlayedVideo`, `mpvLastPlaySend`), and `restoreTabStates` puts them back inside `ensureOptions()`, which every state-changing listener already awaits. A new field that has to survive a wake goes into `PersistedTabFields`, and every place that sets it saves. Within one @@ -676,6 +678,16 @@ forgotten. A frame whose page never named itself to this background (it restarte since) proves nothing and the player counts. mpv-shortcut's `/late` page pins the state the race left. +**A player a page frames itself (2026-10-03, #225).** `player/index.html` is +web-accessible, so a page, or an ad's iframe in it, can frame it with any +`parent_frame_id`. The player's own load is a moz-extension request that webRequest never +sees, so the background took that id on trust: the named frame (the top one) counted as +holding a player, with the effect above. Now `PLAYER_LOADED` adopts the named parent only +when the opener is that frame's page or the player frame's own +(`TabHolder.playerParentProof`); when the background does not know the frame's name (it +restarted since), it asks the frame's content script (`IS_PLAYER_OPENER`). Otherwise the +player is answered null and forgotten. mpv-shortcut's `/framed` page pins it. + **Gone pages, and a page Back brings back (2026-09-30).** `TabHolder.goneDocuments` keeps the 16 latest pages that left a tab, by name, with what each had detected. A page leaves when its `FRAME_REMOVED` is taken, when another page names itself in its frame @@ -1554,7 +1566,7 @@ Opted in: `background.mjs` (2026-10-01) and the rest of `chrome/background/` but (`native-host/faststream-mpv-host.mjs`). The types are Chrome's (`@types/chrome`, which matches the `chrome.*` calls, callbacks included) plus Node's (the host, tests and tools), and `types/firefox-chrome.d.ts` adds the Firefox-only fields read here -(`cookieStoreId`, `originUrl`). background.mjs's own fixes were JSDoc, a few +(`cookieStoreId`). background.mjs's own fixes were JSDoc, a few `undefined` checks that return what the code returned before (through a throw), and one guard: a message from a page outside any tab is no longer handled as a tab's. The player's files are next; fix what tsc reports only with the playback suites to hand. diff --git a/chrome/background/BackgroundUtils.mjs b/chrome/background/BackgroundUtils.mjs index ad4fd4cd..63414c39 100644 --- a/chrome/background/BackgroundUtils.mjs +++ b/chrome/background/BackgroundUtils.mjs @@ -1,4 +1,6 @@ // @ts-check +import {URLUtils} from '../player/utils/URLUtils.mjs'; + const PlayerURL = chrome.runtime.getURL('player/index.html'); export class BackgroundUtils { @@ -21,6 +23,20 @@ export class BackgroundUtils { return BackgroundUtils.PlayerOpeningResponses.includes(response); } + /** + * The player mode of a source a site script reports (DETECTED_SOURCE, custom/*.js). Its + * page can post such a report too (Instagram's): one without an address, or of a type + * the player does not play, was recorded as a source of no mode. + * @param {*} msg - The message. + * @return {?string} The mode, or null for no source. + */ + static detectedSourceMode(msg) { + if (!msg || typeof msg.url !== 'string' || !msg.url) { + return null; + } + return URLUtils.getModeFromExtension(msg.ext) || null; + } + static checkMessageError(message, suppress = false) { if (chrome.runtime.lastError) { if (!suppress) console.warn(`Unable to send message '${message}'`, chrome.runtime.lastError); @@ -136,6 +152,25 @@ export class BackgroundUtils { return url.substring(0, PlayerURL.length) === PlayerURL; } + /** + * Whether a tab's new URL (tabs.onUpdated) is still the same page: only its fragment + * changed, to an anchor (#comments) or a time (#t=120), which Firefox reports as it + * reports a new page. A hash route (#/episode/2, #!/...) is a page of its own, as is + * the same URL again (a reload). + * @param {?string|undefined} oldUrl - The tab's URL before. + * @param {string} newUrl - Its URL now. + * @return {boolean} + */ + static isSamePageUrlChange(oldUrl, newUrl) { + if (!oldUrl || !newUrl || oldUrl === newUrl) { + return false; + } + const hash = (url) => (url.includes('#') ? url.slice(url.indexOf('#') + 1) : ''); + const strip = (url) => url.split('#')[0]; + const isRoute = (url) => hash(url).startsWith('/') || hash(url).startsWith('!'); + return strip(oldUrl) === strip(newUrl) && !isRoute(oldUrl) && !isRoute(newUrl); + } + static getPlayerUrl() { return PlayerURL; } diff --git a/chrome/background/ManifestTypes.mjs b/chrome/background/ManifestTypes.mjs index 2b277c5b..ad851ab9 100644 --- a/chrome/background/ManifestTypes.mjs +++ b/chrome/background/ManifestTypes.mjs @@ -30,3 +30,26 @@ export function contentTypeOf(headers) { export function modeFromContentType(headers) { return MANIFEST_TYPES.get(contentTypeOf(headers)); } + +// Files a media element plays that the player's MP4 mode cannot read (it reads MP4 only): +// played as they are, as a .webm URL already is (URLUtils). +const DIRECT_TYPES = ['video/webm', 'video/ogg']; + +/** + * The player mode for a file a