From 7bf0bab6839b08817fa9ed1c2f0baed03ec8d0a7 Mon Sep 17 00:00:00 2001 From: Vlad Pohorilets Date: Sun, 16 Aug 2026 10:22:10 +0000 Subject: [PATCH 1/2] feat(controls): skip 5 seconds back, 10 forward The back button undoes a line of dialogue you missed; the forward one jumps an opening. Double-tap in each zone follows its own button. Picture in picture goes with it: Gecko has no floating-window API on Android, so the button asked the system for the home screen and hoped. --- src/content/player.css | 9 ------- src/content/ui.js | 57 +++++++----------------------------------- 2 files changed, 9 insertions(+), 57 deletions(-) diff --git a/src/content/player.css b/src/content/player.css index d73a501..8921637 100644 --- a/src/content/player.css +++ b/src/content/player.css @@ -516,15 +516,6 @@ color: #16151c; } -.hidden-frame { - position: absolute; - width: 0; - height: 0; - border: 0; - opacity: 0; - pointer-events: none; -} - .file-picker { position: absolute; width: 1px; diff --git a/src/content/ui.js b/src/content/ui.js index 0a16133..4a957bb 100644 --- a/src/content/ui.js +++ b/src/content/ui.js @@ -17,18 +17,13 @@ const SCRUB_STEP_SECONDS = 0.2; const SCRUB_TICK_MS = 100; const TOAST_MS = 900; const HINT_MS = 2200; -const SKIP_SECONDS = 10; +// Back is the button that undoes a line of dialogue you missed, forward the +// one that jumps an opening — they are not the same distance. +const SKIP_BACK_SECONDS = 5; +const SKIP_FORWARD_SECONDS = 10; const PLAYLIST_SETTLE_MS = 600; const FILL_RETRY_MS = 400; -const FRAME_LIFE_MS = 2000; const CHAPTER_TRIES_MS = [1200, 4000, 10000]; - -// The Android launcher, asked for by intent. Nothing is loaded from anywhere: -// the URL names an activity on the phone, and the browser either hands it to -// the system or ignores it. -const HOME_INTENT = - 'intent://#Intent;action=android.intent.action.MAIN;' + - 'category=android.intent.category.HOME;end'; const FILL_ATTEMPTS = 25; const ICON = { @@ -36,7 +31,6 @@ const ICON = { chevron: 'M7 10l5 5 5-5', colour: 'M12 3a9 9 0 1 0 0 18 2.5 2.5 0 0 0 0-5h-1a2 2 0 0 1 0-4h3a5 5 0 0 0 0-9z', - pip: 'M4 5h16v14H4zM12.5 12.5h6v5h-6z', menu: 'M4 7h16M4 12h16M4 17h16', play: 'M8 5 19 12 8 19z', pause: 'M6 5h3.6v14H6zM14.4 5h3.6v14h-3.6z', @@ -254,9 +248,9 @@ export const createOverlay = ({ closePanels(); return; } - const seconds = (count - 1) * SKIP_SECONDS; - if (zone === ZONE.HOLD_LEFT) skip(-seconds); - else if (zone === ZONE.HOLD_RIGHT) skip(seconds); + const steps = count - 1; + if (zone === ZONE.HOLD_LEFT) skip(-steps * SKIP_BACK_SECONDS); + else if (zone === ZONE.HOLD_RIGHT) skip(steps * SKIP_FORWARD_SECONDS); else setChromeVisible(chrome.hasAttribute('hidden')); }, holdStart: ({ zone }) => { @@ -301,9 +295,9 @@ export const createOverlay = ({ }; const centreRow = el('div', { class: 'centre-row' }, [ - buildSkipButton(-SKIP_SECONDS), + buildSkipButton(-SKIP_BACK_SECONDS), buttons.play, - buildSkipButton(SKIP_SECONDS), + buildSkipButton(SKIP_FORWARD_SECONDS), ]); buttons.colour = buildButton('Colour', ICON.colour, () => { @@ -322,38 +316,6 @@ export const createOverlay = ({ setChromeVisible(true); }); - // Gecko has no Web API for the floating window on Android. What Android has - // is a rule: a video playing in a fullscreen app keeps playing, floating, - // once that app goes to the background. So the button does the one thing - // that reliably triggers it — it goes to the home screen. - // - // The launcher is asked for through an intent URL in a throwaway frame. In a - // frame, a browser that does not handle the scheme fails quietly instead of - // navigating the film away, which is what would happen from the top window. - const leaveForHomeScreen = () => { - const attributes = { class: 'hidden-frame', 'aria-hidden': 'true' }; - const frame = el('iframe', attributes); - document.body.append(frame); - try { - frame.src = HOME_INTENT; - } catch (error) { - console.warn('Nocturne: could not reach the home screen', error); - } - setTimeout(() => frame.remove(), FRAME_LIFE_MS); - }; - - const enterPictureInPicture = () => { - setChromeVisible(true); - if (video.paused) video.play().catch(() => {}); - - if (typeof video.requestPictureInPicture === 'function') { - video.requestPictureInPicture().catch(leaveForHomeScreen); - return; - } - - leaveForHomeScreen(); - }; - // The playlist sits where a thumb reaches it while the phone is held // sideways — beside the way out, not buried in the settings sheet, which is // no place to be changing episode from. One dropdown per step of the path @@ -419,7 +381,6 @@ export const createOverlay = ({ buildButton('Exit player', ICON.exit, () => onExit()), playlistBar, el('div', { class: 'spacer' }), - buildButton('Picture in picture', ICON.pip, enterPictureInPicture), buttons.colour, buttons.menu, ); From c686b3f72d5c81f6c90472a1c7608b292bea22fa Mon Sep 17 00:00:00 2001 From: Vlad Pohorilets Date: Sun, 16 Aug 2026 10:22:21 +0000 Subject: [PATCH 2/2] docs: 0.6.0, and why a screenshot button is not possible Firefox for Android hands no decoded video frame to any script: a 2D canvas comes back empty, a WebGL texture reads back one flat colour, and captureVisibleTab draws the video area black. --- CHANGELOG.md | 28 +++++++++++++++++++++++++++- README.md | 19 +++++++++++++------ package.json | 2 +- src/manifest.json | 2 +- 4 files changed, 42 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ae3ade..f3b05a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased][unreleased] +## [0.6.0][] - 2026-08-16 + +### Changed + +- **The back button skips 5 seconds, not 10.** Going back is for the line of + dialogue you missed; going forward is for the opening you have seen. Forward + is unchanged at 10 seconds, and a double-tap in each zone now follows its own + button: 5 seconds a tap on the left, 10 on the right. + +### Removed + +- **Picture in picture.** Gecko has no floating-window API on Android, so the + button asked the system for the home screen and hoped: it worked on some + builds and did nothing on others. The top row is one button shorter. + +### Notes + +- **A screenshot button was attempted and is not possible.** Firefox for + Android does not give a decoded video frame to any script: `drawImage` of the + video into a 2D canvas leaves it transparent, a WebGL texture of it reads + back as a single flat colour (`#330033`), and `tabs.captureVisibleTab` draws + the page with the video area black. This was measured from the page's own + scripts, from a content script, and from a background script with host + permissions, on Firefox 153 on an AOSP Android 16 emulator and on a Pixel 9 + Pro. The button is therefore not shipped rather than shipped broken. ## [0.5.0][] - 2026-08-09 ### Added @@ -228,7 +253,8 @@ First release submitted to addons.mozilla.org. There is no sound while scrubbing. - Volume is left to the phone's own buttons. -[unreleased]: https://github.com/kvachikk/nocturne-player/compare/v0.5.0...HEAD +[unreleased]: https://github.com/kvachikk/nocturne-player/compare/v0.6.0...HEAD +[0.6.0]: https://github.com/kvachikk/nocturne-player/releases/tag/v0.6.0 [0.5.0]: https://github.com/kvachikk/nocturne-player/releases/tag/v0.5.0 [0.4.0]: https://github.com/kvachikk/nocturne-player/releases/tag/v0.4.0 [0.3.0]: https://github.com/kvachikk/nocturne-player/releases/tag/v0.3.0 diff --git a/README.md b/README.md index fd7491f..47ed9e5 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,8 @@ screen can never stop the film by accident. Pausing leaves the colour alone, for when you paused to look at something. **Hold for 2x** — hold the right zone to play at double speed, hold the left -zone to rewind. Double-tap either zone for ±10 seconds. +zone to rewind. Double-tap the right zone to jump 10 seconds forward, the left +one to step 5 seconds back. **Pinch to zoom** — the picture fills the screen from the start, with no black bars; pinch to go back to the full frame and it snaps cleanly between the two. @@ -71,17 +72,23 @@ quality switches between them, and stays out of the way when there is only one. of the player: the season, and the episode beside it. Picking one plays it without leaving the player. A film has no bar at all. -**Picture in picture** — the last button in the top row asks Android for the -home screen and leaves the film playing behind it, which is the hand-off the -system already knows how to float. The player survives being backgrounded, so -coming back finds it where you left it. - **Chapters** — where a site publishes sections, the seek bar is cut at each one and names the section your finger is over. **On the site's own player** — a single fullscreen button sits on an inline video. It fades out when it is left alone and comes back on any tap. +## What is not here + +**Saving a frame.** A screenshot button — press it, the controls step aside, +the frame you are watching is saved to the phone — cannot be built on Firefox +for Android today. The browser will not give the decoded frame to any script: +drawing the video into a 2D canvas leaves it empty, uploading it to a WebGL +texture returns one flat placeholder colour, and `tabs.captureVisibleTab` +renders the page with the video area black. This holds for the page's own +scripts as much as for an add-on, so there is nothing an add-on can do about +it. The phone's own screenshot still works, controls and all. + ## Privacy This is the whole story, and you can verify every line of it: diff --git a/package.json b/package.json index 11baeaf..84f47fe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nocturne-player", - "version": "0.5.0", + "version": "0.6.0", "private": true, "description": "A touch-first video player for Firefox on Android", "license": "MPL-2.0", diff --git a/src/manifest.json b/src/manifest.json index 5e24fa2..290513a 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "Nocturne Player", - "version": "0.5.0", + "version": "0.6.0", "description": "A touch-first video player for streaming sites and YouTube: thick seek bar, gesture controls, quality and subtitle picker, night light and colour tuning.", "author": "Vlad Pohorilets", "homepage_url": "https://github.com/kvachikk/nocturne-player",