Unified Picture in Picture
Kawaikara intentionally does not use a site's native HTMLVideoElement.requestPictureInPicture() result as its active PiP mode. Browser-owned PiP cannot reliably provide Kawaikara's configured placement, custom controls, focus restoration, Provider route policy, or macOS full-screen Space behavior. It can also leave playback alive after Kawaikara's visible window has disappeared.
Why Kawaikara owns the PiP window
Kawaikara intentionally does not use a site’s native
HTMLVideoElement.requestPictureInPicture() result as its active PiP mode.
Browser-owned PiP cannot reliably provide Kawaikara’s configured placement,
custom controls, focus restoration, Provider route policy, or macOS full-screen
Space behavior. It can also leave playback alive after Kawaikara’s visible
window has disappeared.
The active implementation is UnifiedPictureInPictureManager. Both the normal
and the legacy Game PiP IPC actions are routed to the same unified window mode.
The obsolete browser-native/Game manager was removed so there is only one
runtime policy and cleanup path to maintain.
There are two playback hosts and therefore two related PiP pipelines:
| Source | Normal host | PiP operation | Playback continuity |
|---|---|---|---|
| Provider site | A sandboxed WebContentsView inside the Viewer |
Move the existing view into a new frameless BrowserWindow |
The same document, session, decoder, and media element continue running |
| Internal Video | A child BrowserWindow containing the Video renderer |
Detach and reshape the existing Video window | The same Chromium/libmpv renderer and playback surface continue running |
Neither path opens the media URL in a second renderer. Reloading or cloning the page would lose page JavaScript state and can produce a second DRM/media pipeline.
Provider PiP lifecycle
sequenceDiagram
participant Viewer as Viewer window
participant View as Existing SiteView
participant Page as Active Provider page
participant PiP as Frameless PiP window
Viewer->>Page: validate Provider and route
Viewer->>Page: inspect frames and select a video
Viewer->>Page: inject video-only layout and controls
Viewer->>PiP: create with configured size and placement
Viewer->>PiP: reparent the existing SiteView
Viewer->>Viewer: hide
PiP->>Viewer: return button, Escape, close, or shortcut
Viewer->>Page: restore styles and native controls
PiP->>Viewer: reparent the SiteView
Viewer->>Viewer: restore presentation, show, and focus
Entry is deliberately ordered as follows:
WindowManagerchecks Provider and route eligibility.- The manager inspects all accessible frames for a ready video. A not-ready result is retried once after 100 ms to tolerate a player attaching its media immediately after the command.
- The selected frame receives the reversible video-only transformation. Kawaikara always installs its Return and Play/Pause overlay here; this is not delegated to Provider injection code.
- The source aspect ratio selects portrait or landscape preferences and the initial monitor bounds.
- A hidden, frameless PiP
BrowserWindowis created and all event handlers are attached. - The existing SiteView is transferred to that window.
- Only after the target has submitted renderer frames is the Viewer hidden and the platform-specific PiP presentation applied.
- The state change is broadcast, which enables the PiP-only global shortcut.
The transfer order is a rendering hack, not incidental bookkeeping.
transferWebContentsView() first makes the target native window visible without
activating it, removes the view from its source, adds it to the target, updates
its bounds, and calls webContents.invalidate(). It then waits for two renderer
animation frames plus one Main-process turn. The frame wait has a 250 ms escape
timeout so an in-flight navigation cannot deadlock cleanup. This avoids the
black or frozen surface seen when the old host was hidden before Chromium had a
visible destination surface.
Provider views are created with backgroundThrottling: false. This must be set
when the WebContentsView is created: applying a workaround after the transfer
is too late if Chromium or the service has already paused a background media
pipeline. disableHtmlFullscreenWindowResize: true also keeps a site’s HTML5
full-screen request inside Kawaikara’s current host instead of turning the
application itself into a native full-screen window.
Video discovery and route guards
The manager walks the main frame, accessible child frames, and open shadow
roots. It scores every <video> using the following priority:
- Currently playing and not ended.
- Having at least
HAVE_CURRENT_DATA. - Visible area inside the frame viewport.
The winning element must also report real videoWidth and videoHeight.
Missing elements return no-video; an element without decoded dimensions
returns not-ready.
DOM scoring alone cannot distinguish a real programme from an autoplaying home page preview. Provider policy is therefore checked before DOM inspection:
metadata.pictureInPicture.enabled === trueexposes the shared action. Omitted PiP metadata is disabled, so enabling PiP is an explicit capability.AbstractProvider.allowPictureInPicture(currentUrl)performs route-level validation.- YouTube currently accepts
/watch?v=...,/shorts/..., and/live/.... - CHZZK currently accepts detail routes below
/live,/video, and/clips.
A Provider with preview videos should implement a strict route guard even if its current DOM happens to select the correct element.
Suppressing page-native PiP
Page-owned PiP is suppressed by default, independently of whether Kawaikara’s shared PiP action is enabled for that Provider. Allowing both paths would let a site bypass Kawaikara’s window state, placement, close, focus, and shortcut lifecycle.
The application-owned page policy performs several layers of suppression:
- sets
disablePictureInPictureon current and newly inserted videos; - reports
document.pictureInPictureEnabledas false where it can be overridden; - replaces
requestPictureInPicture()and Document PiP’srequestWindow()with rejected requests; - immediately exits an unexpected
enterpictureinpictureevent; - hides, disables, removes from tab order, and blocks clicks/keys on known PiP controls;
- observes DOM mutations and open shadow roots so React/Vue player controls inserted later are also processed.
CHZZK is the exception to the API-level block. When its SPA leaves a live
route, it moves the stream into a page-owned mini-player and keeps that
detached player responsible for playback and audio. Its Provider declares
@provider({ pictureInPicture: { pageRequestPolicy: 'allow' } }): Kawaikara
still hides and blocks trusted user
input on CHZZK’s manual page button, but synthetic activation used by the SPA
is allowed and the automatic mini-player remains visible and controllable.
Semantic PiP matching is restricted to interactive controls; matching a
toolbar or mini-player container merely because a descendant contains the word
PIP would hide the whole surface while audio continued. transient remains
available for sites that need only a short enter/leave cleanup cycle.
Generic selectors and semantic labels cover common English, Korean, and
Japanese PiP controls. A Provider can add service-specific selectors through
the decorator’s pictureInPicture.pageControlSelectors; CHZZK and Laftel
currently do this. pictureInPicture.suppressPageControls: false is an explicit
code-owned escape hatch for a Provider that deliberately implements a compatible
page-owned lifecycle. The manifest only enables the app-level PiP capability.
This policy belongs to the application, not to the Provider’s
script-injection permission. Providers declare selectors; they do not receive
extra application authority merely to hide their native PiP button. The policy
is refreshed on DOM ready and load completion, while its observer handles later
DOM changes.
Reversible video-only transformation
The selected element remains in its original DOM, but is visually hoisted into a fixed, full-window layer. The entry script:
- records the original inline style for the video and each ancestor;
- neutralizes ancestor transforms, filters, clipping, containment, opacity, and overflow that could crop the fixed video;
- fixes the selected video to the viewport with
object-fit: contain; - hides unrelated body content, media controls, timeline UI, scrollbars, and overscroll;
- adds a black backdrop; and
- adds a closed-shadow-root overlay above the site.
The application core supplies a standard DOM-caption selector set covering
Video.js, Shaka, JW Player, THEOplayer, AMP, and common semantic
subtitle/caption container naming for every Provider that enables unified PiP.
Providers add only stable service-specific selectors for Netflix, Coupang Play,
Prime Video, Disney+, Apple TV+, Wavve, Crunchyroll, Twitch, YouTube, and CHZZK.
Native WebVTT cues painted by the selected <video> remain visible without a
DOM selector. DOM captions are raised above the video but stay non-interactive,
so they cannot intercept the PiP controls.
The transformation also walks composed ancestors across open Shadow DOM boundaries. Equivalent video/subtitle styles are installed in each accessible shadow root, including roots added while PiP is active, and removed during restoration. Closed third-party shadow roots remain intentionally inaccessible; their native video-painted cues still work, but a DOM caption hidden entirely inside one requires cooperation from the player.
The closed shadow root prevents service CSS from restyling Kawaikara’s Return and Play/Pause buttons. The video itself is made non-interactive, so hidden player controls cannot capture clicks.
The overlay also requests Chromium’s top layer through a manual popover, with the maximum-z-index fixed layer retained as a compatibility fallback. When a site such as Apple TV+ replaces its player iframe during playback, Main reinjects the same shared overlay and forcibly synchronizes the current native hover state so both controls are immediately available in the replacement frame.
The buttons cannot call Electron IPC directly because remote Provider pages
stay sandboxed and do not receive a privileged PiP bridge. Their DOM click
handlers write separate process-randomized sentinels through console.debug();
Main accepts only those exact values from the active SiteView. Main also maps
trusted native mouse-down coordinates over the shared button bounds to the
same actions. That fallback keeps Return and Play/Pause responsive when a DRM
or composited player displays the overlay but does not deliver the DOM click.
The two paths are debounced so one click cannot toggle playback twice.
Tab and Shift+Tab are consumed while PiP is active so focus cannot move into
hidden page UI or reveal the application menu behind PiP. Plain Escape
returns to the Viewer.
Exit restores the original controls value, every captured inline style and
marker, and removes all injected nodes. It waits for two animation frames before
the normal page becomes visible again. This wait prevents the restored Viewer
from flashing a partially transformed player.
Hover and dragging hacks
The overlay buttons appear whenever the pointer is anywhere inside the PiP
window. DOM hover alone is not reliable here: native draggable regions on
Windows can stop delivering mouse movement or emit a transient mouseLeave
when crossing into a no-drag button.
Main therefore polls Electron’s global cursor position every 80 ms and compares it with the PiP bounds. Input events may reveal controls early, but the screen-coordinate check is authoritative for hiding them.
Dragging is platform-specific:
- Windows uses
-webkit-app-region: dragwith explicit no-drag button regions. - macOS and Linux track Main-process mouse down/move/up input, save the initial cursor and window coordinates, and move the native window by the delta.
- Return and Play/Pause hit boxes are excluded from dragging.
This split keeps the whole video surface draggable without sacrificing button clicks on any supported platform.
macOS full-screen Space hack
alwaysOnTop alone is not enough on macOS. A normal Dock application can have
an always-on-top window on desktop Spaces while still being absent from a true
full-screen Space that already belongs to a game or another application.
Kawaikara applies the following sequence for both Provider and internal Video
PiP:
- Change the process activation policy from
regulartoaccessoryand hide the Dock icon. This makes the PiP behave like a UI-element window instead of a competing foreground application. - Set the native level to Electron’s
screen-saveralways-on-top level. - Call
setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }). - Pass Electron’s native window handle to
kawaikara_macos_window_spaces.node. - The AppKit bridge adds
NSWindowCollectionBehaviorCanJoinAllSpacesandNSWindowCollectionBehaviorFullScreenAuxiliary, then setsNSScreenSaverWindowLeveldirectly. - Show the window with
showInactive()and callmoveTop(), preserving focus in the current game or full-screen application.
AppKit may rewrite collection behavior while transferring a child view,
changing Spaces, or processing focus. Kawaikara therefore reapplies the entire
presentation immediately, after 250 ms, and after 1 second. A PiP blur event
schedules the same three reassertions. Timers are cancelled on exit so they
cannot raise a restored or destroyed window later.
The bridge is a small Node-API Objective-C++ add-on compiled as one universal
arm64/x86_64 binary by pnpm build:native. electron-builder copies it to the
packaged application’s resources/native directory. On non-macOS platforms
the build script removes stale Mach-O output; at runtime the TypeScript wrapper
is a no-op outside macOS. If the add-on cannot load, Electron’s public
all-workspaces behavior remains as a best-effort fallback and a warning is
logged.
Exit reverses the process state before focusing the Viewer: clear
FullScreenAuxiliary, remove the all-workspaces/full-screen presentation,
restore activation policy regular, show the Dock icon, and only then show and
activate Kawaikara. This order fixes the failure where Kawaikara disappeared
from both the Dock and Command-Tab after returning from PiP.
Always-on-top and focus state
The user’s Viewer always-on-top preference is the desired state, not simply the
current native flag. While either PiP path is active, WindowManager clears the
Viewer’s native always-on-top flag and cancels its reassert timers. This avoids
two hidden/visible Kawaikara windows competing at the top layer.
Every normal exit, failed entry, navigation exit, and close-button exit restores the desired Viewer flag. If the preference was enabled, its own reassert logic is restarted.
The PiP window’s native close event is intercepted and converted into Return;
it must never quit the application or leave an invisible page playing audio.
After restoration, focusViewer():
- restores macOS’s regular activation policy and Dock presence;
- restores a minimized Viewer and shows it;
- synchronizes SiteView and internal Video bounds;
- uses
app.focus({ steal: true })on macOS; - calls
moveTop()andfocus(); and - focuses the active SiteView or Video renderer.
A real main-frame document navigation exits Provider PiP because the injected element and frame are no longer stable. Same-document SPA navigation remains in PiP, which is required for YouTube Shorts to change URLs without replacing the SiteView.
Internal Video PiP
The local/HLS Video view is already a separate frameless child
BrowserWindow, so it cannot use SiteView transfer. The Video renderer reports
whether a playable source is ready plus its decoded dimensions. Entry is
rejected until that state is ready.
On entry, WindowManager saves the Video window’s minimum size, movable,
resizable, and all-workspaces state. It then hides the overlay, detaches the
Video window from the Viewer, hides the Viewer, applies the selected aspect
ratio and placement, enables moving/resizing, and presents that same window as
PiP. The renderer receives video.pictureInPictureChanged and hides its normal
header, errors, and transport controls while adding the PiP Return and
Play/Pause overlay.
On exit, the current placement is recorded, PiP renderer state is cleared, the window is hidden, and its aspect ratio, native presentation, minimum size, movable/resizable state, and parent are restored. The Viewer is shown before the child Video window is returned to its full content bounds. The same macOS Space and focus restoration sequence is used as Provider PiP.
Closing the Video window while it is in PiP also routes through restoration. In normal embedded Video mode, closing the child window still follows the Viewer close lifecycle.
Size and placement
Landscape and portrait sources have separate preferences. Each supports compact, medium, large, or validated custom dimensions. The actual source aspect ratio is preserved where possible and the result is fitted inside the selected display work area.
Positions are top-left, top-right, bottom-left, bottom-right, or last position. The monitor source can be the current Viewer display, video/current display, previous display, or a specific display. Manual movement is stored as normalized X/Y ratios rather than absolute pixels, allowing the last position to be reconstructed after resolution, scaling, or monitor changes.
Shortcut lifetime
In normal mode, the PiP shortcut is handled only from Kawaikara’s focused input events. On successful entry, the same configured accelerator is registered as an Electron global shortcut. This is necessary because a game, Wine process, or another application normally owns focus while PiP is visible.
The global registration exists only while PiP is active. It is removed on exit and application disposal, so it cannot start PiP while another application is focused in normal Viewer mode. Toggle requests are serialized and inputs within 300 ms are ignored to prevent a key repeat or duplicated local/global event from entering and immediately exiting.
Games, Wine, macOS, or another process may reserve an accelerator such as
Command+P. Electron then reports a registration failure. The failure is
logged, and the user must choose an accelerator not consumed by the foreground
application.
YouTube Shorts behavior
The YouTube Provider advances to the next Short only after actual completion:
- it handles a normal
endedevent; - because Shorts commonly loop, it also detects a genuine end-to-start time wrap;
- merely reaching the last fraction of a second does not advance;
- the active, mostly visible Shorts video must match the current URL; and
- PiP entry and exit increment a progress generation so a transition cannot be mistaken for a completed loop.
Same-document navigation to the next Short keeps the active PiP session attached to the existing view.
When PiP starts from a visible Menu, WindowManager hides both the overlay and
Viewer and remembers that Menu state. Returning restores the Viewer and reopens
the Menu. Preference and update overlays reject PiP entry because their draft
or modal state must remain visible and interactive.
Failure recovery and invariants
Entry and exit promises are serialized. A second toggle joins the operation already in progress rather than starting another native-window transfer.
If entry fails after page transformation, cleanup runs in this order: restore the injected page, transfer the SiteView back, restore macOS application presentation, destroy the partial PiP window, show the Viewer, and restore its always-on-top state. Normal exit additionally records placement before changing window ownership. Renderer-frame synchronization errors are tolerated during navigation so native cleanup can still complete.
The implementation should preserve these invariants:
- exactly one live playback document or Video renderer exists;
- a SiteView belongs to only one native window at a time;
- page transformation is removed before the normal Viewer is exposed;
- only the active PiP window owns the screen-saver top level;
- macOS activation policy and Dock state are regular outside PiP;
- the PiP global shortcut is registered only while PiP is active; and
- every user-visible close path restores a usable, focused Viewer.
Known compatibility boundaries
Unified Provider PiP requires an accessible HTML <video> in the current frame
tree. A protected surface hidden from JavaScript, canvas-only rendering, a
closed cross-origin implementation detail that Electron cannot inspect, or a
radically changed service DOM can return no-video, not-ready, or failed.
Provider route guards, selectors, and manual regression tests remain part of
each integration.
The macOS bridge targets full-screen Spaces and normal native window levels. It cannot override protected system UI or a hardware/exclusive presentation path that the window server does not compose with ordinary application windows.
Regression checklist
For every Provider PiP change, verify:
- entry and exit from a normal playback route;
- rejection on a home page containing preview videos;
- landscape and portrait sizing;
- Return button, Play/Pause,
Escape, native close, and configured shortcut; - resize and drag on macOS and Windows;
- site navigation during PiP and same-document SPA navigation;
- Viewer always-on-top off and on, including restoration after failed entry;
- page-native PiP buttons remain hidden after dynamic player updates;
- no audio remains after closing/returning; and
- macOS desktop Spaces, true full-screen Spaces, Dock, Command-Tab, and focus restoration.
For internal Video, repeat the lifecycle with both a local file and HLS source, and verify that the active Chromium/libmpv rendering backend is not recreated.
Relevant source files
src/Main/Manager/UnifiedPictureInPictureManager.ts: Provider video state transitions, PiP window, transfer, hover, placement, and macOS presentation.src/Main/Inject/PictureInPictureVideo.tsandsrc/Main/Inject/PictureInPictureControls.ts: typed page-world operations serialized only at the Electron execution boundary.src/Main/Functional/WebContentsViewTransfer.ts: visible-target transfer and renderer-frame synchronization.src/Main/Manager/WindowManager.ts: Provider/internal Video routing, always-on-top ownership, internal Video PiP, navigation, and focus recovery.src/Main/Manager/ShortcutManager.ts: local/global shortcut lifetime and toggle serialization.src/Main/Functional/PagePictureInPicturePolicy.ts: page-native PiP API and control suppression.packages/site-api/src/Provider.ts: Provider PiP metadata and route contract.src/Main/Functional/MacOSWindowSpaces.tsandscripts/MacOSWindowSpaces.mm: native macOS full-screen Space bridge.scripts/build-macos-window-spaces.cjsandelectron-builder.config.cjs: universal add-on build and packaging.src/Renderer/View/Video/App.tsxandsrc/Renderer/Styles/Video.css: internal Video PiP renderer state and overlay.