Runtime and Lifecycle
Kawaikara uses separate surfaces for remote content and application-owned UI.
Window model
Kawaikara uses separate surfaces for remote content and application-owned UI.
flowchart TB
Host["Viewer BrowserWindow\napp host and internal Video renderer"]
SiteView["Site WebContentsView\nremote streaming site"]
Overlay["Overlay BrowserWindow\nMenu or full Preferences"]
PiP["Frameless PiP BrowserWindow\nsame SiteView, video-only layout"]
Popup["Site popup\nOAuth with opener semantics"]
Browser["External browser\nlogin and cookie transfer"]
Host --> SiteView
Overlay -. parent .-> Host
SiteView -. reparent while active .-> PiP
Popup -. same Session .-> SiteView
Browser -. copies cookies on completion .-> SiteView
Viewer and remote SiteView
The main BrowserWindow owns application geometry, Dock/task-switcher identity,
native fullscreen, and the app-owned Video renderer. Remote streaming sites fill
a sandboxed child WebContentsView; entering Video removes that remote view and
loads the internal document in the main window so the libmpv preload can remain
isolated from remote pages.
When a site changes, Kawaikara:
- Exits PiP and prepares the current document for removal.
- Cancels an external login and closes site popups.
- Unloads the active Provider.
- Closes the old
WebContentsView. - Creates a new view with the selected Electron Session.
- Constructs and loads the next Provider.
The view uses sandbox: true, contextIsolation: true, and nodeIntegration: false. disableHtmlFullscreenWindowResize prevents a web player’s fullscreen control from resizing the entire application window. App-level fullscreen remains an explicit Kawaikara shortcut.
Remote SiteViews receive a Chrome-style user agent without Electron product tokens before their first navigation. A user-origin scrollbar theme gives every site the same compact, rounded, activity-only scrollbar; the restricted preload toggles its visibility marker from captured scroll events.
GPU acceleration is a restart-required General preference and is off by
default. Main reads it synchronously before Electron becomes ready. The disabled
mode calls app.disableHardwareAcceleration(), covering Chromium decoding,
compositing, WebGL, WebGPU, rasterization, and platform video overlays on both
macOS and Windows. Enabling it restores the complete GPU path. Changing the
preference opens a confirmation dialog; applying saves all pending preferences,
relaunches Kawaikara, and selects the new mode before the next renderer exists.
KAWAIKARA_FORCE_SOFTWARE_RENDERING=1 always selects the disabled mode.
Native Video decoding remains independent of Electron’s compositor. Main keeps
MPV_HWDEC=auto-safe in both modes, so libmpv may continue using VideoToolbox or
D3D11 hardware decoding. With Electron GPU acceleration enabled, frames use the
shared-texture/WebGPU presentation path. With it disabled, libmpv copies the
decoded frames through Canvas 2D. The software surface is limited to CSS
resolution and 720p, and the RGBA IPC path reuses the native buffer’s complete
ArrayBuffer instead of making an additional copy. This keeps capture-compatible
playback responsive without changing the hardware decoder selection.
Overlay
The overlay is a frameless child BrowserWindow above the viewer. Its width depends on its current route:
- Menu: compact left-side panel, capped at 380 pixels.
- Preferences: the entire viewer content area.
The gear button changes the overlay route from Menu to Preferences; Preferences is not nested inside the menu panel. The back button, Escape, or a non-editing Backspace returns to the existing menu without replaying its entry animation. Tab remains normal focus navigation while Preferences is open.
While the menu is open, closing on Escape and closing on an outside click are independently configurable. Plain Tab toggles the menu only when it is not owned by an editable field in the current web content.
Site popup
A Provider can request a popup when an OAuth service requires window.opener, postMessage, or response_mode=web_message. The popup uses the same Session as the site but receives no application bridge. Laftel’s Sign in with Apple flow is the current built-in example.
External browser
Some services reject embedded Electron login. ExternalBrowserManager launches a temporary Chrome/Edge profile through Patchright, waits for a Provider-supplied completion URL pattern, transfers only its cookies into the selected Electron Session, and restores the viewer. The external profile is never reused. Providers may transiently seed it with the Electron Session’s cookies for an add-account flow, but the completed cookie jar is copied back to Electron and the temporary profile is removed.
Completion, cancellation, a site change, or app shutdown closes the browser and removes the temporary profile. Legacy persistent external-browser state is deleted before a new login starts. Netflix and Coupang Play currently use this path. YouTube and YouTube Music instead keep authentication inside their shared Electron Session, using Chromium’s native user agent and UA Client Hints without an external cookie-transfer round trip.
This managed login path is intentionally distinct from a normal external link:
cookie import requires a browser context controlled by Patchright, so it tries
Chrome, Edge, and the app-managed Chromium runtime. A Provider’s ordinary
openExternal() call and the external new-window policy use Electron’s Main
process shell integration, which delegates HTTP(S) URLs to the default browser
registered by macOS, Windows, or Linux. Default-browser URLs are validated
before dispatch and are never used as a silent substitute for a cookie-import
login flow.
Browser profiles and Session isolation
The runtime chooses a browser profile in this order:
- A user assignment for the site (
isolated,user:<id>, orplugin:<plugin-id>:<profile-id>). - The Provider’s
isolation.defaultBrowserProfile. - A persistent profile dedicated to the site.
Partitions are generated as follows:
| Profile | Partition pattern |
|---|---|
| Site-specific persistent isolation | persist:kawaikara.site.<site-id> |
| Persistent shared profile | persist:kawaikara.profile.<runtime-profile-id> |
| Non-persistent shared profile | kawaikara.profile.<runtime-profile-id> |
Unsafe characters are replaced with underscores before the partition is used. Sites sharing a partition share cookies, cache, storage, and DRM-related session state, but still receive a new WebContentsView when activated.
This model addresses cross-site failures such as a DRM service becoming unusable after visiting another service. Isolation is the default; sharing is an explicit Provider default or user choice. YouTube and YouTube Music intentionally share the built-in google Bundle profile.
Preferences can clear one resolved profile partition or a site’s dedicated
isolated partition. If that partition owns the active site, SiteManager
unloads its Provider and view before Electron clears cookies, local storage,
IndexedDB, authentication state, and caches, then loads the site again. A site
assigned to shared state exposes no site-level clear action; clearing its shared
profile is intentionally a profile-wide operation.
Provider and Plugin lifecycle
stateDiagram-v2
[*] --> Registered: PluginHost.install
Registered --> Loading: SiteManager.load(id)
Loading --> PluginsActive: activate matching Plugins
PluginsActive --> Active: Provider.load succeeds
PluginsActive --> Disposed: load fails, deactivate Plugins, unload Provider
Active --> Disposed: deactivate Plugins, unload Provider
Disposed --> [*]
Only one Provider is active. SiteManager waits for the previous Provider’s Plugins to deactivate and for its unload(), creates the next context and Provider, activates matching Plugins, and calls load(). A failed activation or load deactivates already-started Plugins in reverse order before unloading the Provider. Constructors should only store state; external work belongs in activate() or load().
The Provider and Plugin base classes each own a DisposableStore. Listeners added to subscriptions are released by super.unload() or super.deactivate().
URL Provider loading
The public Site API AbstractUrlProvider performs:
beforeLoad()
-> register dom-ready and did-finish-load hooks
-> viewer.loadURL(url)
-> afterLoad()
An SPA can replace its first document and cause Electron to reject the original navigation with ERR_ABORTED. The helper treats only that code as a normal handoff. It invokes afterLoad() from both DOM/load events and the resolved loadURL() path, so injected code must be idempotent and use a page marker.
Navigation and new windows
allowNavigation(url) guards main-frame navigation and redirects before commit. onNewWindow(url) controls target=_blank and window.open():
| Policy | Runtime behavior | Typical use |
|---|---|---|
viewer |
Load the URL in the current view | Normal same-service navigation |
popup |
Create a child window with the site Session | OAuth requiring an opener |
external |
Delegate to the operating system | Explicit external documentation or app links |
deny |
Ignore the request | Ads or unwanted popups |
default |
Allow Electron’s default behavior | Compatibility-only escape hatch |
Providers should parse the URL and use exact scheme/hostname allowlists rather than broad substring checks.
The base Provider keeps HTTP(S) window.open() requests in a sandboxed,
Session-sharing popup so unknown OAuth providers still work. Override the
policy when a Provider can safely classify ordinary links and unwanted windows.
Remote page actions
Remote pages do not receive ipcRenderer. When an injected control must request a Main action, the Provider creates a kawaikara-action:// URL.
sequenceDiagram
participant Page as Remote page
participant Window as WindowManager
participant Site as Active Provider
Site->>Page: inject action URL and click interceptor
Page->>Window: navigate to kawaikara-action://invoke/login
Window->>Window: prevent navigation
Window->>Site: onAction("login")
Site-->>Window: handled or unhandled
context.actions.createUrl() validates the action name. Unknown actions return false.
Request-header hook
Each configured Session receives one onBeforeSendHeaders listener. Requests are offered to the active Provider and the resolved site locale can set Accept-Language afterward.
onBeforeSendHeaders(details: SiteRequestDetails) {
if (!isExampleDomain(details.url)) return undefined;
return { ...details.requestHeaders, 'User-Agent': this.userAgent };
}
A shared Session may carry requests for multiple integrations over time. Header hooks must check the hostname and return a new object instead of mutating the original headers.
Shortcut scope
Application and Provider shortcuts are matched from before-input-event in the focused viewer or overlay. Defaults come from APP_SHORTCUTS and each Provider’s shortcut.defaultKey; user overrides are stored in preferences.
- Plain
Tabdoes not toggle the menu while the focused web content reports that the user is editing. - Menu-category shortcuts, defaulting to
1,2,3, and so on, exist only while the menu route is visible and no editable control owns the key event. - Menu uses
Tabas its close toggle without moving focus; Preferences consumes it while the fixed Menu underlay remains visible. - The PiP shortcut is application-local in normal mode.
- Only while PiP is active is its accelerator registered with Electron’s
globalShortcut, allowing restoration even when another application has focus. It is unregistered immediately on exit or shutdown. - Previous Short/Clip, next Short/Clip, and short-form auto-advance toggle use the same local matching in normal mode. While unified PiP is active, those three configured accelerators are also registered globally and routed to the active YouTube or CHZZK Provider; they are removed with the PiP accelerator.
- A short in-flight/debounce guard prevents one physical key event from triggering both the local and global PiP paths.
Global accelerators may still be unavailable when the operating system, Wine, a game, or another application owns the same key combination. The manager logs that registration failure instead of silently claiming success.
File-drop redirection
Both app-owned and remote viewer preloads install a capture-phase file drop target. Main validates that a dropped path is absolute, is a real file, and has a supported video extension. It queues a file: open request, loads kawaikara.video, and delivers the request to the Video renderer. Dropping another file while Video is active follows the same route.
IPC flow
flowchart LR
Renderer["Renderer view"] --> API["Typed window API<br/>window.kawaikara or window.kawaikaraVideo"]
API --> Preload["contextBridge preload"]
Preload --> IPC["ipcRenderer.invoke<br/>or event subscription"]
IPC --> IpcManager["IpcManager"]
IpcManager --> Managers["SiteManager / WindowManager<br/>PreferenceManager / another manager"]
Adding an IPC operation requires coordinated changes to the channel tree and types, preload implementation, Main handler, and renderer caller. Main handlers also validate values at runtime; TypeScript types do not validate untrusted IPC input.