fix(embedded-mpv): stop crashing on IPC for not-yet-created sessions

The renderer was reading session.id and forwarding it to the addon, but
during the loading window that id is the placeholder
"embedded-mpv-starting" set by createLoadingSession. If the user adjusted
volume, seeked, or toggled audio/subtitle/speed/aspect before the addon's
createSession returned the real id, that placeholder id reached the
addon — and the addon's getSessionOrThrow threw a raw std::runtime_error
which libc++abi terminated the process on.

Two fixes, defense in depth:

1. Renderer (session controller): use the canonical sessionId() signal,
   which is null until the addon hands back a real id, as the gate for all
   IPC calls. Wrap every IPC call in a guardIpc helper that swallows
   addon-side throws so a torn-down session or race won't surface as an
   uncaught promise rejection.

2. Native (embedded_mpv.mm): change getSessionOrThrow to take a
   Napi::Env and throw Napi::Error::New(env, ...) instead of
   std::runtime_error. node-addon-api converts Napi::Error to a JS
   exception cleanly; the previous std::runtime_error escaped the C++
   frame and aborted the process when the addon was built without
   NAPI_CPP_EXCEPTIONS translation. Refactor splits findSession (returns
   nullptr) from getSessionOrThrow (env-aware) so call paths that just
   probe a session's existence don't pay the throw cost.

The native fix needs an addon rebuild to take effect; the renderer fix
prevents the crash trigger immediately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
This commit is contained in:
4grayandClaude Opus 4.7 committed 2026-05-05 10:29:23 +02:00
1 parent 126025aa41
commit 9424dedafc
2 files changed
+73 -43

No files matched your search

@@ -194,13 +194,16 @@ export class EmbeddedMpvSessionController {
}
async togglePaused(): Promise<void> {
const id = this.sessionId();
const session = this.session();
if (!session?.id || !window.electron?.setEmbeddedMpvPaused) {
if (!id || !session || !window.electron?.setEmbeddedMpvPaused) {
return;
}
const updated = await window.electron.setEmbeddedMpvPaused(
session.id,
session.status !== 'paused'
const updated = await this.guardIpc(() =>
window.electron!.setEmbeddedMpvPaused(
id,
session.status !== 'paused'
)
);
if (updated) {
this.session.set(updated);
@@ -208,12 +211,15 @@ export class EmbeddedMpvSessionController {
}
async seekBy(deltaSeconds: number): Promise<boolean> {
const id = this.sessionId();
const session = this.session();
if (!session?.id || !window.electron?.seekEmbeddedMpv) {
if (!id || !session || !window.electron?.seekEmbeddedMpv) {
return false;
}
const next = Math.max(0, session.positionSeconds + deltaSeconds);
const updated = await window.electron.seekEmbeddedMpv(session.id, next);
const updated = await this.guardIpc(() =>
window.electron!.seekEmbeddedMpv(id, next)
);
if (updated) {
this.session.set(updated);
}
@@ -221,35 +227,38 @@ export class EmbeddedMpvSessionController {
}
async seekTo(seconds: number): Promise<void> {
const session = this.session();
if (!session?.id || !window.electron?.seekEmbeddedMpv) {
const id = this.sessionId();
if (!id || !window.electron?.seekEmbeddedMpv) {
return;
}
const updated = await window.electron.seekEmbeddedMpv(session.id, seconds);
const updated = await this.guardIpc(() =>
window.electron!.seekEmbeddedMpv(id, seconds)
);
if (updated) {
this.session.set(updated);
}
}
async applyVolume(value: number): Promise<void> {
const session = this.session();
if (!session?.id || !window.electron?.setEmbeddedMpvVolume) {
const id = this.sessionId();
if (!id || !window.electron?.setEmbeddedMpvVolume) {
return;
}
const updated = await window.electron.setEmbeddedMpvVolume(session.id, value);
const updated = await this.guardIpc(() =>
window.electron!.setEmbeddedMpvVolume(id, value)
);
if (updated) {
this.session.set(updated);
}
}
async setAudioTrack(trackId: number): Promise<void> {
const session = this.session();
if (!session?.id || !window.electron?.setEmbeddedMpvAudioTrack) {
const id = this.sessionId();
if (!id || !window.electron?.setEmbeddedMpvAudioTrack) {
return;
}
const updated = await window.electron.setEmbeddedMpvAudioTrack(
session.id,
trackId
const updated = await this.guardIpc(() =>
window.electron!.setEmbeddedMpvAudioTrack(id, trackId)
);
if (updated) {
this.session.set(updated);
@@ -257,13 +266,12 @@ export class EmbeddedMpvSessionController {
}
async setSubtitleTrack(trackId: number): Promise<void> {
const session = this.session();
if (!session?.id || !window.electron?.setEmbeddedMpvSubtitleTrack) {
const id = this.sessionId();
if (!id || !window.electron?.setEmbeddedMpvSubtitleTrack) {
return;
}
const updated = await window.electron.setEmbeddedMpvSubtitleTrack(
session.id,
trackId
const updated = await this.guardIpc(() =>
window.electron!.setEmbeddedMpvSubtitleTrack!(id, trackId)
);
if (updated) {
this.session.set(updated);
@@ -271,30 +279,43 @@ export class EmbeddedMpvSessionController {
}
async setSpeed(speed: number): Promise<void> {
const session = this.session();
if (!session?.id || !window.electron?.setEmbeddedMpvSpeed) {
const id = this.sessionId();
if (!id || !window.electron?.setEmbeddedMpvSpeed) {
return;
}
const updated = await window.electron.setEmbeddedMpvSpeed(session.id, speed);
const updated = await this.guardIpc(() =>
window.electron!.setEmbeddedMpvSpeed!(id, speed)
);
if (updated) {
this.session.set(updated);
}
}
async setAspect(aspect: string): Promise<void> {
const session = this.session();
if (!session?.id || !window.electron?.setEmbeddedMpvAspect) {
const id = this.sessionId();
if (!id || !window.electron?.setEmbeddedMpvAspect) {
return;
}
const updated = await window.electron.setEmbeddedMpvAspect(
session.id,
aspect
const updated = await this.guardIpc(() =>
window.electron!.setEmbeddedMpvAspect!(id, aspect)
);
if (updated) {
this.session.set(updated);
}
}
private async guardIpc<T>(
call: () => Promise<T>
): Promise<T | null> {
try {
return await call();
} catch {
// The session may have been torn down or an addon-side throw
// raced the IPC. Swallow — the next snapshot will resync state.
return null;
}
}
private async loadSupport(): Promise<void> {
try {
this.support.set(await window.electron!.getEmbeddedMpvSupport());