Compare commits
No commits in common. "e65ba271fdc9cde5f845aeb4a4e29af111e11830" and "b922e4236fa586e06cc42e286075381c902775c8" have entirely different histories.
e65ba271fd
...
b922e4236f
5 changed files with 0 additions and 124 deletions
|
|
@ -344,10 +344,6 @@ Then put it behind your existing reverse proxy like everything else
|
||||||
action — resolved server-side (one atomic operation, not one
|
action — resolved server-side (one atomic operation, not one
|
||||||
request per track) the same way launching a playlist already
|
request per track) the same way launching a playlist already
|
||||||
expands into everything it contains — see docs/architecture.md
|
expands into everything it contains — see docs/architecture.md
|
||||||
- [x] Picture-in-picture for the video player (standard
|
|
||||||
`requestPictureInPicture`, confirmed present on Android Chrome) —
|
|
||||||
asked for directly for mobile, where there's no second window to
|
|
||||||
put this app in while switching apps — see docs/architecture.md
|
|
||||||
- [ ] Try a full party end-to-end with two real accounts (still the main gap)
|
- [ ] Try a full party end-to-end with two real accounts (still the main gap)
|
||||||
- [ ] Try casting to a real Chromecast (only the SDK's own bootstrap is
|
- [ ] Try casting to a real Chromecast (only the SDK's own bootstrap is
|
||||||
confirmed here — no real device/session)
|
confirmed here — no real device/session)
|
||||||
|
|
|
||||||
|
|
@ -366,15 +366,6 @@
|
||||||
|
|
||||||
.fsBtn:hover { background: rgba(255, 255, 255, .18); }
|
.fsBtn:hover { background: rgba(255, 255, 255, .18); }
|
||||||
|
|
||||||
/* #pipBtn while active - same "clearly-on" treatment as .iconBtn's own
|
|
||||||
[aria-pressed="true"] rule (#castBtn), just scoped to .fsBtn instead -
|
|
||||||
the two aren't combined onto one shared class since .iconBtn and
|
|
||||||
.fsBtn otherwise disagree on padding/border-radius/background. */
|
|
||||||
.fsBtn[aria-pressed="true"] {
|
|
||||||
color: #ff9800;
|
|
||||||
background: rgba(255, 152, 0, .18);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Replaces the video content while casting - the local <video> is
|
/* Replaces the video content while casting - the local <video> is
|
||||||
paused/torn down (see Player.ts's startCasting/cast.ts), so leaving
|
paused/torn down (see Player.ts's startCasting/cast.ts), so leaving
|
||||||
its last frame frozen with no explanation would just look stuck. */
|
its last frame frozen with no explanation would just look stuck. */
|
||||||
|
|
|
||||||
|
|
@ -127,17 +127,6 @@
|
||||||
<span id="ambientNotice" class="ambientNotice hide"></span>
|
<span id="ambientNotice" class="ambientNotice hide"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="bottomBarZone right">
|
<div class="bottomBarZone right">
|
||||||
<!-- Hidden by default, shown only once Player.ts confirms
|
|
||||||
document.pictureInPictureEnabled - not every browser supports
|
|
||||||
this (see its own comment), and there's no live
|
|
||||||
"availability changed" event to react to the way the cast
|
|
||||||
button has, since this never changes mid-session. -->
|
|
||||||
<button id="pipBtn" class="fsBtn hide" title="Picture-in-picture" aria-label="Picture-in-picture" aria-pressed="false">
|
|
||||||
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
|
|
||||||
<rect x="3" y="4" width="18" height="14" rx="2" fill="none" stroke="currentColor" stroke-width="2"></rect>
|
|
||||||
<rect x="12" y="11" width="7" height="5" rx="1" fill="currentColor"></rect>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button id="fullscreenBtn" class="fsBtn" title="Plein écran" aria-label="Plein écran"></button>
|
<button id="fullscreenBtn" class="fsBtn" title="Plein écran" aria-label="Plein écran"></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -196,7 +196,6 @@ function renderVideoPlayer(root: HTMLElement, partyId: string, party: PartyPubli
|
||||||
const qualitySelect = root.querySelector('#qualitySelect') as HTMLSelectElement;
|
const qualitySelect = root.querySelector('#qualitySelect') as HTMLSelectElement;
|
||||||
const burnInHint = root.querySelector('#burnInHint')!;
|
const burnInHint = root.querySelector('#burnInHint')!;
|
||||||
const castBtn = root.querySelector('#castBtn') as HTMLButtonElement;
|
const castBtn = root.querySelector('#castBtn') as HTMLButtonElement;
|
||||||
const pipBtn = root.querySelector('#pipBtn') as HTMLButtonElement;
|
|
||||||
const fullscreenBtn = root.querySelector('#fullscreenBtn') as HTMLButtonElement;
|
const fullscreenBtn = root.querySelector('#fullscreenBtn') as HTMLButtonElement;
|
||||||
const bottomBar = root.querySelector('#bottomBar') as HTMLElement;
|
const bottomBar = root.querySelector('#bottomBar') as HTMLElement;
|
||||||
const progressBar = root.querySelector('#progressBar') as HTMLElement;
|
const progressBar = root.querySelector('#progressBar') as HTMLElement;
|
||||||
|
|
@ -997,44 +996,6 @@ function renderVideoPlayer(root: HTMLElement, partyId: string, party: PartyPubli
|
||||||
});
|
});
|
||||||
updateFullscreenBtn();
|
updateFullscreenBtn();
|
||||||
|
|
||||||
/**
|
|
||||||
* Picture-in-picture - asked for directly, mobile in particular: the
|
|
||||||
* one way to keep watching in a small floating window while switching
|
|
||||||
* to another app, since a phone has no second window/screen to put
|
|
||||||
* this app in the way a desktop might. Feature-detected once at mount,
|
|
||||||
* not live like #castBtn's own onCastAvailabilityChanged - unlike a
|
|
||||||
* Chromecast appearing/disappearing on the network mid-session,
|
|
||||||
* browser support for this is fixed for the lifetime of the page, so
|
|
||||||
* there's nothing to react to after this first check. video itself
|
|
||||||
* needs no new attribute for this to work - unlike disableremoteplayback
|
|
||||||
* (added specifically to suppress Chrome's own auto cast icon), there's
|
|
||||||
* no equivalent auto-injected native PiP control this app needs to
|
|
||||||
* fight with.
|
|
||||||
*/
|
|
||||||
if (document.pictureInPictureEnabled && !video.disablePictureInPicture) {
|
|
||||||
pipBtn.classList.remove('hide');
|
|
||||||
pipBtn.addEventListener('click', () => {
|
|
||||||
if (document.pictureInPictureElement) {
|
|
||||||
document.exitPictureInPicture().catch(() => {});
|
|
||||||
} else {
|
|
||||||
video.requestPictureInPicture().catch((err) => {
|
|
||||||
// Can genuinely fail even when supported (no user gesture in
|
|
||||||
// some browsers' stricter reading of that, video not yet
|
|
||||||
// ready) - same "try it, log it, don't crash anything else"
|
|
||||||
// instinct as every other best-effort browser API this app
|
|
||||||
// calls (video.play().catch, requestFullscreen().catch, ...).
|
|
||||||
console.error('[watchparty player] picture-in-picture failed', err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Reflects state via the same [aria-pressed] treatment #castBtn
|
|
||||||
// uses (see Player.css's .fsBtn[aria-pressed="true"]) - covers the
|
|
||||||
// browser's own PiP window being closed directly (its native close
|
|
||||||
// button, or the OS), not just this button's own click.
|
|
||||||
video.addEventListener('enterpictureinpicture', () => pipBtn.setAttribute('aria-pressed', 'true'));
|
|
||||||
video.addEventListener('leavepictureinpicture', () => pipBtn.setAttribute('aria-pressed', 'false'));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onItemChanged(msg: ItemChangedMsg) {
|
async function onItemChanged(msg: ItemChangedMsg) {
|
||||||
itemTitleEl.textContent = msg.itemName || '';
|
itemTitleEl.textContent = msg.itemName || '';
|
||||||
hasNextQueued = !!msg.hasNext;
|
hasNextQueued = !!msg.hasNext;
|
||||||
|
|
|
||||||
|
|
@ -3343,67 +3343,6 @@ Authorization` Device/Client/Version header a real client would, only
|
||||||
`X-Emby-Token` - is genuinely unconfirmed. Worth watching the real
|
`X-Emby-Token` - is genuinely unconfirmed. Worth watching the real
|
||||||
dashboard the first time a party runs after this ships.
|
dashboard the first time a party runs after this ships.
|
||||||
|
|
||||||
## Picture-in-picture
|
|
||||||
|
|
||||||
Asked for directly, mobile in particular: a phone has no second window
|
|
||||||
to put this app in the way a desktop might, so the standard browser
|
|
||||||
Picture-in-Picture API - popping the `<video>` out into a small,
|
|
||||||
OS-managed floating window - is the one way to keep watching while
|
|
||||||
switching to another app. Standard `HTMLVideoElement
|
|
||||||
.requestPictureInPicture()`/`document.exitPictureInPicture()`, not the
|
|
||||||
Chromecast-adjacent `disableremoteplayback` trick or anything WebKit-
|
|
||||||
prefixed - confirmed directly in a real Chromium build that
|
|
||||||
`document.pictureInPictureEnabled`/`requestPictureInPicture`/
|
|
||||||
`exitPictureInPicture` are all present, which is what actually matters
|
|
||||||
for Android Chrome, the mobile browser this was asked for.
|
|
||||||
|
|
||||||
New `#pipBtn`, right next to `#fullscreenBtn` (`.bottomBarZone.right`)
|
|
||||||
- both are "how is this displayed" toggles, so they share the same
|
|
||||||
`.fsBtn` sizing, and a new `.fsBtn[aria-pressed="true"]` rule gives PiP
|
|
||||||
the identical "clearly on" orange treatment `#castBtn` already has
|
|
||||||
(kept as its own rule rather than combined onto `.iconBtn` - that class
|
|
||||||
and `.fsBtn` disagree on padding/border-radius/background, so
|
|
||||||
combining them risked an inconsistent size instead of a clean shared
|
|
||||||
look). Feature-detected once at mount (`document.pictureInPictureEnabled
|
|
||||||
&& !video.disablePictureInPicture`), not live the way `#castBtn`'s own
|
|
||||||
`onCastAvailabilityChanged` is - browser support for this doesn't
|
|
||||||
appear/disappear mid-session the way a Chromecast on the network does,
|
|
||||||
so there's nothing to react to after the first check. No new attribute
|
|
||||||
needed on `<video>` itself either - unlike `disableremoteplayback`
|
|
||||||
(added specifically to suppress Chrome's own auto-injected cast icon),
|
|
||||||
there's no equivalent native PiP affordance this app needs to
|
|
||||||
suppress.
|
|
||||||
|
|
||||||
State (`aria-pressed`) is kept in sync via the video's own
|
|
||||||
`enterpictureinpicture`/`leavepictureinpicture` events rather than the
|
|
||||||
button's own click handler alone - covers the browser's native PiP
|
|
||||||
window being closed directly (its own close control, or the OS), not
|
|
||||||
just a second click back in this app. Requires no cleanup on route
|
|
||||||
navigation: like every other per-mount element here, `<video>` itself
|
|
||||||
is destroyed and replaced by the next `root.innerHTML = html` (router.ts
|
|
||||||
runs this unconditionally on any navigation) - browsers already close
|
|
||||||
an active PiP window automatically once its source element leaves the
|
|
||||||
document, matching this app's very deliberate opposite choice for
|
|
||||||
casting (leaving an active Chromecast session running after
|
|
||||||
navigating away - see "Casting to a Chromecast" above): PiP has no
|
|
||||||
independent existence apart from the `<video>` element it mirrors, a
|
|
||||||
Chromecast session does.
|
|
||||||
|
|
||||||
Verified: full `typecheck`/`build` (client only - this is a pure
|
|
||||||
client-side feature, no server involved). Confirmed directly in a real
|
|
||||||
browser, not just type-checked: `document.pictureInPictureEnabled` is
|
|
||||||
`true` here, and running the exact show/hide + click-handling logic
|
|
||||||
against a real (sourceless) `<video>` correctly shows the button and
|
|
||||||
catches the resulting `InvalidStateError` from `requestPictureInPicture()`
|
|
||||||
without it becoming an uncaught exception - the same rejection a real
|
|
||||||
video would throw for a different reason (already in PiP, or the
|
|
||||||
document not focused), confirming the error handling path itself
|
|
||||||
rather than just assuming it. **Not verified**: an actual floating PiP
|
|
||||||
window on a real mobile browser, playing a real Emby stream - no
|
|
||||||
device/login reachable from this sandbox, same standing gap as this
|
|
||||||
project's other browser-native integrations (casting, the intro-skip
|
|
||||||
buttons).
|
|
||||||
|
|
||||||
## Queuing a whole album, or an artist's entire discography, in one action
|
## Queuing a whole album, or an artist's entire discography, in one action
|
||||||
|
|
||||||
Asked for directly: `views/album`'s and `views/artist`'s per-track/
|
Asked for directly: `views/album`'s and `views/artist`'s per-track/
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue