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
28 changes: 27 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
19 changes: 13 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
9 changes: 0 additions & 9 deletions src/content/player.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
57 changes: 9 additions & 48 deletions src/content/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,20 @@ 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 = {
exit: 'M6 6l12 12M18 6L6 18',
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',
Expand Down Expand Up @@ -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 }) => {
Expand Down Expand Up @@ -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, () => {
Expand All @@ -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
Expand Down Expand Up @@ -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,
);
Expand Down
2 changes: 1 addition & 1 deletion src/manifest.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down