diff --git a/apps/electron-backend/native/src/embedded_mpv.mm b/apps/electron-backend/native/src/embedded_mpv.mm index 4928ffe0b..775e786ad 100644 --- a/apps/electron-backend/native/src/embedded_mpv.mm +++ b/apps/electron-backend/native/src/embedded_mpv.mm @@ -166,18 +166,27 @@ void runOnMainSync(dispatch_block_t block) dispatch_sync(dispatch_get_main_queue(), block); } -std::shared_ptr getSessionOrThrow(const std::string& sessionId) +std::shared_ptr findSession(const std::string& sessionId) { std::lock_guard sessionsLock(gSessionsMutex); const auto iterator = gSessions.find(sessionId); - if (iterator == gSessions.end()) { - throw std::runtime_error("Embedded MPV session not found."); + return nullptr; } - return iterator->second; } +std::shared_ptr getSessionOrThrow( + Napi::Env env, + const std::string& sessionId) +{ + auto session = findSession(sessionId); + if (!session) { + throw Napi::Error::New(env, "Embedded MPV session not found."); + } + return session; +} + void scheduleRender(const std::shared_ptr& session); void requestRender(const std::shared_ptr& session); void updateSessionError(const std::shared_ptr& session, const std::string& error); @@ -1604,7 +1613,7 @@ Napi::Value LoadPlayback(const Napi::CallbackInfo& info) const std::string sessionId = info[0].As().Utf8Value(); const auto playback = info[1].As(); - const auto session = getSessionOrThrow(sessionId); + const auto session = getSessionOrThrow(env, sessionId); const std::string streamUrl = readOptionalString(playback, "streamUrl"); if (streamUrl.empty()) { throw Napi::Error::New(env, "Embedded MPV playback requires a stream URL."); @@ -1713,7 +1722,7 @@ Napi::Value SetBounds(const Napi::CallbackInfo& info) const std::string sessionId = info[0].As().Utf8Value(); const auto bounds = info[1].As(); - const auto session = getSessionOrThrow(sessionId); + const auto session = getSessionOrThrow(env, sessionId); setSessionFrame( session, @@ -1734,7 +1743,7 @@ Napi::Value SetPaused(const Napi::CallbackInfo& info) } const std::string sessionId = info[0].As().Utf8Value(); - const auto session = getSessionOrThrow(sessionId); + const auto session = getSessionOrThrow(env, sessionId); int paused = info[1].As().Value() ? 1 : 0; const int result = mpv_set_property_async( session->handle, @@ -1774,7 +1783,7 @@ Napi::Value Seek(const Napi::CallbackInfo& info) } const std::string sessionId = info[0].As().Utf8Value(); - const auto session = getSessionOrThrow(sessionId); + const auto session = getSessionOrThrow(env, sessionId); const auto target = info[1].As().DoubleValue(); const std::string targetValue = std::to_string(target); const char* command[] = { @@ -1813,7 +1822,7 @@ Napi::Value SetVolume(const Napi::CallbackInfo& info) } const std::string sessionId = info[0].As().Utf8Value(); - const auto session = getSessionOrThrow(sessionId); + const auto session = getSessionOrThrow(env, sessionId); double volume = clampVolumePercent( info[1].As().DoubleValue() ); @@ -1849,7 +1858,7 @@ Napi::Value SetAudioTrack(const Napi::CallbackInfo& info) } const std::string sessionId = info[0].As().Utf8Value(); - const auto session = getSessionOrThrow(sessionId); + const auto session = getSessionOrThrow(env, sessionId); int64_t trackId = info[1].As().Int64Value(); const int result = mpv_set_property_async( session->handle, @@ -1886,7 +1895,7 @@ Napi::Value SetSubtitleTrack(const Napi::CallbackInfo& info) } const std::string sessionId = info[0].As().Utf8Value(); - const auto session = getSessionOrThrow(sessionId); + const auto session = getSessionOrThrow(env, sessionId); int64_t trackId = info[1].As().Int64Value(); int result = -1; @@ -1934,7 +1943,7 @@ Napi::Value SetSpeed(const Napi::CallbackInfo& info) } const std::string sessionId = info[0].As().Utf8Value(); - const auto session = getSessionOrThrow(sessionId); + const auto session = getSessionOrThrow(env, sessionId); double speed = info[1].As().DoubleValue(); speed = std::clamp(speed, 0.25, 4.0); @@ -1973,7 +1982,7 @@ Napi::Value SetAspect(const Napi::CallbackInfo& info) } const std::string sessionId = info[0].As().Utf8Value(); - const auto session = getSessionOrThrow(sessionId); + const auto session = getSessionOrThrow(env, sessionId); std::string aspect = info[1].As().Utf8Value(); if (aspect.empty()) { aspect = "no"; diff --git a/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-session-controller.ts b/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-session-controller.ts index 0804bb200..4bcb4b64a 100644 --- a/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-session-controller.ts +++ b/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-session-controller.ts @@ -194,13 +194,16 @@ export class EmbeddedMpvSessionController { } async togglePaused(): Promise { + 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 { + 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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( + call: () => Promise + ): Promise { + 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 { try { this.support.set(await window.electron!.getEmbeddedMpvSupport());