diff --git a/apps/electron-backend/native/src/embedded_mpv.mm b/apps/electron-backend/native/src/embedded_mpv.mm index b23f84ab8..4928ffe0b 100644 --- a/apps/electron-backend/native/src/embedded_mpv.mm +++ b/apps/electron-backend/native/src/embedded_mpv.mm @@ -96,6 +96,10 @@ struct SessionSnapshot { std::string error; std::vector audioTracks; int64_t selectedAudioTrackId = -1; + std::vector subtitleTracks; + int64_t selectedSubtitleTrackId = -1; + double playbackSpeed = 1.0; + std::string aspectOverride = "no"; }; struct Session { @@ -880,17 +884,31 @@ void updateSelectedAudioTrack(SessionSnapshot& snapshot, int64_t selectedTrackId } } -void updateAudioTracksFromNode(SessionSnapshot& snapshot, const mpv_node& node) +void updateSelectedSubtitleTrack(SessionSnapshot& snapshot, int64_t selectedTrackId) { + snapshot.selectedSubtitleTrackId = + selectedTrackId >= 0 ? selectedTrackId : -1; + for (auto& track : snapshot.subtitleTracks) { + track.selected = track.id == snapshot.selectedSubtitleTrackId; + } +} + +void updateTracksFromNode( + SessionSnapshot& snapshot, + const mpv_node& node, + const std::string& typeFilter, + std::vector& outTracks, + int64_t& outSelectedId) +{ + outTracks.clear(); + outSelectedId = -1; + if (node.format != MPV_FORMAT_NODE_ARRAY || !node.u.list || !node.u.list->values) { return; } - std::vector audioTracks; - int64_t selectedTrackId = -1; - for (int index = 0; index < node.u.list->num; index += 1) { const mpv_node& trackNode = node.u.list->values[index]; if (trackNode.format != MPV_FORMAT_NODE_MAP) { @@ -898,7 +916,7 @@ void updateAudioTracksFromNode(SessionSnapshot& snapshot, const mpv_node& node) } const mpv_node* typeNode = getNodeMapValue(trackNode, "type"); - if (!typeNode || readNodeString(*typeNode) != "audio") { + if (!typeNode || readNodeString(*typeNode) != typeFilter) { continue; } @@ -908,32 +926,51 @@ void updateAudioTracksFromNode(SessionSnapshot& snapshot, const mpv_node& node) continue; } - AudioTrack audioTrack; - audioTrack.id = id; + AudioTrack track; + track.id = id; if (const mpv_node* titleNode = getNodeMapValue(trackNode, "title")) { - audioTrack.title = readNodeString(*titleNode); + track.title = readNodeString(*titleNode); } if (const mpv_node* languageNode = getNodeMapValue(trackNode, "lang")) { - audioTrack.language = readNodeString(*languageNode); + track.language = readNodeString(*languageNode); } if (const mpv_node* selectedNode = getNodeMapValue(trackNode, "selected")) { - audioTrack.selected = readNodeFlag(*selectedNode); + track.selected = readNodeFlag(*selectedNode); } if (const mpv_node* defaultNode = getNodeMapValue(trackNode, "default")) { - audioTrack.defaultTrack = readNodeFlag(*defaultNode); + track.defaultTrack = readNodeFlag(*defaultNode); } if (const mpv_node* forcedNode = getNodeMapValue(trackNode, "forced")) { - audioTrack.forced = readNodeFlag(*forcedNode); + track.forced = readNodeFlag(*forcedNode); } - if (audioTrack.selected) { - selectedTrackId = audioTrack.id; + if (track.selected) { + outSelectedId = track.id; } - audioTracks.push_back(audioTrack); + outTracks.push_back(track); } +} - snapshot.audioTracks = std::move(audioTracks); - snapshot.selectedAudioTrackId = selectedTrackId; +void updateAudioTracksFromNode(SessionSnapshot& snapshot, const mpv_node& node) +{ + updateTracksFromNode( + snapshot, + node, + "audio", + snapshot.audioTracks, + snapshot.selectedAudioTrackId + ); +} + +void updateSubtitleTracksFromNode(SessionSnapshot& snapshot, const mpv_node& node) +{ + updateTracksFromNode( + snapshot, + node, + "sub", + snapshot.subtitleTracks, + snapshot.selectedSubtitleTrackId + ); } void updateSessionError(const std::shared_ptr& session, const std::string& error) @@ -969,6 +1006,8 @@ void runEventLoop(const std::shared_ptr& session) session->snapshot.error.clear(); session->snapshot.audioTracks.clear(); session->snapshot.selectedAudioTrackId = -1; + session->snapshot.subtitleTracks.clear(); + session->snapshot.selectedSubtitleTrackId = -1; session->loadedPath = false; break; case MPV_EVENT_FILE_LOADED: @@ -1050,9 +1089,15 @@ void runEventLoop(const std::shared_ptr& session) if (propertyName == "track-list" && property->format == MPV_FORMAT_NODE && property->data) { + const auto& trackListNode = + *static_cast(property->data); updateAudioTracksFromNode( session->snapshot, - *static_cast(property->data) + trackListNode + ); + updateSubtitleTracksFromNode( + session->snapshot, + trackListNode ); break; } @@ -1071,6 +1116,40 @@ void runEventLoop(const std::shared_ptr& session) break; } + if (propertyName == "sid" && + property->format == MPV_FORMAT_STRING && + property->data) { + const auto* sidValue = static_cast(property->data); + int64_t selectedTrackId = -1; + if (sidValue && parseIntegerString(sidValue, selectedTrackId)) { + updateSelectedSubtitleTrack( + session->snapshot, + selectedTrackId + ); + } else { + updateSelectedSubtitleTrack(session->snapshot, -1); + } + break; + } + + if (propertyName == "speed" && + property->format == MPV_FORMAT_DOUBLE && + property->data) { + session->snapshot.playbackSpeed = + *static_cast(property->data); + break; + } + + if (propertyName == "video-aspect-override" && + property->format == MPV_FORMAT_STRING && + property->data) { + const auto* aspectValue = + static_cast(property->data); + session->snapshot.aspectOverride = + aspectValue ? aspectValue : "no"; + break; + } + break; } case MPV_EVENT_LOG_MESSAGE: { @@ -1488,6 +1567,14 @@ Napi::Value CreateSession(const Napi::CallbackInfo& info) mpv_observe_property(session->handle, 5, "path", MPV_FORMAT_STRING); mpv_observe_property(session->handle, 6, "track-list", MPV_FORMAT_NODE); mpv_observe_property(session->handle, 7, "aid", MPV_FORMAT_STRING); + mpv_observe_property(session->handle, 8, "sid", MPV_FORMAT_STRING); + mpv_observe_property(session->handle, 9, "speed", MPV_FORMAT_DOUBLE); + mpv_observe_property( + session->handle, + 10, + "video-aspect-override", + MPV_FORMAT_STRING + ); session->running.store(true); session->eventThread = std::thread(runEventLoop, session); @@ -1788,6 +1875,135 @@ Napi::Value SetAudioTrack(const Napi::CallbackInfo& info) return env.Undefined(); } +Napi::Value SetSubtitleTrack(const Napi::CallbackInfo& info) +{ + Napi::Env env = info.Env(); + if (info.Length() < 2 || !info[0].IsString() || !info[1].IsNumber()) { + throw Napi::TypeError::New( + env, + "Expected session id and subtitle track id." + ); + } + + const std::string sessionId = info[0].As().Utf8Value(); + const auto session = getSessionOrThrow(sessionId); + int64_t trackId = info[1].As().Int64Value(); + + int result = -1; + if (trackId < 0) { + const char* disabledValue = "no"; + std::string disabled = disabledValue; + result = mpv_set_property_async( + session->handle, + nextAsyncRequestId(), + "sid", + MPV_FORMAT_STRING, + const_cast(&disabledValue) + ); + } else { + result = mpv_set_property_async( + session->handle, + nextAsyncRequestId(), + "sid", + MPV_FORMAT_INT64, + &trackId + ); + } + + if (result < 0) { + throw Napi::Error::New( + env, + std::string("Failed to update subtitle track: ") + + mpv_error_string(result) + ); + } + + { + std::lock_guard lock(session->mutex); + updateSelectedSubtitleTrack(session->snapshot, trackId); + } + + return env.Undefined(); +} + +Napi::Value SetSpeed(const Napi::CallbackInfo& info) +{ + Napi::Env env = info.Env(); + if (info.Length() < 2 || !info[0].IsString() || !info[1].IsNumber()) { + throw Napi::TypeError::New(env, "Expected session id and speed."); + } + + const std::string sessionId = info[0].As().Utf8Value(); + const auto session = getSessionOrThrow(sessionId); + double speed = info[1].As().DoubleValue(); + speed = std::clamp(speed, 0.25, 4.0); + + const int result = mpv_set_property_async( + session->handle, + nextAsyncRequestId(), + "speed", + MPV_FORMAT_DOUBLE, + &speed + ); + + if (result < 0) { + throw Napi::Error::New( + env, + std::string("Failed to update playback speed: ") + + mpv_error_string(result) + ); + } + + { + std::lock_guard lock(session->mutex); + session->snapshot.playbackSpeed = speed; + } + + return env.Undefined(); +} + +Napi::Value SetAspect(const Napi::CallbackInfo& info) +{ + Napi::Env env = info.Env(); + if (info.Length() < 2 || !info[0].IsString() || !info[1].IsString()) { + throw Napi::TypeError::New( + env, + "Expected session id and aspect override." + ); + } + + const std::string sessionId = info[0].As().Utf8Value(); + const auto session = getSessionOrThrow(sessionId); + std::string aspect = info[1].As().Utf8Value(); + if (aspect.empty()) { + aspect = "no"; + } + + const char* aspectValue = aspect.c_str(); + const int result = mpv_set_property_async( + session->handle, + nextAsyncRequestId(), + "video-aspect-override", + MPV_FORMAT_STRING, + const_cast(&aspectValue) + ); + + if (result < 0) { + throw Napi::Error::New( + env, + std::string("Failed to update aspect override: ") + + mpv_error_string(result) + ); + } + + { + std::lock_guard lock(session->mutex); + session->snapshot.aspectOverride = aspect; + } + + return env.Undefined(); +} + Napi::Value GetSessionSnapshot(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); @@ -1857,6 +2073,47 @@ Napi::Value GetSessionSnapshot(const Napi::CallbackInfo& info) audioTracks.Set(index, trackObject); } result.Set("audioTracks", audioTracks); + + if (snapshot.selectedSubtitleTrackId >= 0) { + result.Set( + "selectedSubtitleTrackId", + Napi::Number::New(env, snapshot.selectedSubtitleTrackId) + ); + } else { + result.Set("selectedSubtitleTrackId", env.Null()); + } + auto subtitleTracks = + Napi::Array::New(env, snapshot.subtitleTracks.size()); + for (size_t index = 0; index < snapshot.subtitleTracks.size(); + index += 1) { + const AudioTrack& track = snapshot.subtitleTracks[index]; + auto trackObject = Napi::Object::New(env); + trackObject.Set("id", Napi::Number::New(env, track.id)); + if (!track.title.empty()) { + trackObject.Set("title", Napi::String::New(env, track.title)); + } + if (!track.language.empty()) { + trackObject.Set("language", Napi::String::New(env, track.language)); + } + trackObject.Set("selected", Napi::Boolean::New(env, track.selected)); + trackObject.Set( + "defaultTrack", + Napi::Boolean::New(env, track.defaultTrack) + ); + trackObject.Set("forced", Napi::Boolean::New(env, track.forced)); + subtitleTracks.Set(index, trackObject); + } + result.Set("subtitleTracks", subtitleTracks); + + result.Set( + "playbackSpeed", + Napi::Number::New(env, snapshot.playbackSpeed) + ); + result.Set( + "aspectOverride", + Napi::String::New(env, snapshot.aspectOverride) + ); + if (!snapshot.error.empty()) { result.Set("error", Napi::String::New(env, snapshot.error)); } @@ -1898,6 +2155,12 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) exports.Set("seek", Napi::Function::New(env, Seek)); exports.Set("setVolume", Napi::Function::New(env, SetVolume)); exports.Set("setAudioTrack", Napi::Function::New(env, SetAudioTrack)); + exports.Set( + "setSubtitleTrack", + Napi::Function::New(env, SetSubtitleTrack) + ); + exports.Set("setSpeed", Napi::Function::New(env, SetSpeed)); + exports.Set("setAspect", Napi::Function::New(env, SetAspect)); exports.Set( "getSessionSnapshot", Napi::Function::New(env, GetSessionSnapshot) diff --git a/apps/electron-backend/src/app/api/main.preload.ts b/apps/electron-backend/src/app/api/main.preload.ts index a00f8f6a7..80573f24f 100644 --- a/apps/electron-backend/src/app/api/main.preload.ts +++ b/apps/electron-backend/src/app/api/main.preload.ts @@ -393,6 +393,25 @@ const electronApi = { trackId: number ): Promise => ipcRenderer.invoke('EMBEDDED_MPV_SET_AUDIO_TRACK', sessionId, trackId), + setEmbeddedMpvSubtitleTrack: ( + sessionId: string, + trackId: number + ): Promise => + ipcRenderer.invoke( + 'EMBEDDED_MPV_SET_SUBTITLE_TRACK', + sessionId, + trackId + ), + setEmbeddedMpvSpeed: ( + sessionId: string, + speed: number + ): Promise => + ipcRenderer.invoke('EMBEDDED_MPV_SET_SPEED', sessionId, speed), + setEmbeddedMpvAspect: ( + sessionId: string, + aspect: string + ): Promise => + ipcRenderer.invoke('EMBEDDED_MPV_SET_ASPECT', sessionId, aspect), disposeEmbeddedMpvSession: ( sessionId: string ): Promise => diff --git a/apps/electron-backend/src/app/events/embedded-mpv.events.ts b/apps/electron-backend/src/app/events/embedded-mpv.events.ts index ff761a4fb..c55628fe4 100644 --- a/apps/electron-backend/src/app/events/embedded-mpv.events.ts +++ b/apps/electron-backend/src/app/events/embedded-mpv.events.ts @@ -5,9 +5,12 @@ import { EMBEDDED_MPV_LOAD_PLAYBACK, EMBEDDED_MPV_PREPARE, EMBEDDED_MPV_SEEK, + EMBEDDED_MPV_SET_ASPECT, EMBEDDED_MPV_SET_AUDIO_TRACK, EMBEDDED_MPV_SET_BOUNDS, EMBEDDED_MPV_SET_PAUSED, + EMBEDDED_MPV_SET_SPEED, + EMBEDDED_MPV_SET_SUBTITLE_TRACK, EMBEDDED_MPV_SET_VOLUME, EMBEDDED_MPV_SUPPORT, EmbeddedMpvBounds, @@ -76,6 +79,24 @@ ipcMain.handle( getService().setAudioTrack(sessionId, trackId) ); +ipcMain.handle( + EMBEDDED_MPV_SET_SUBTITLE_TRACK, + (_event, sessionId: string, trackId: number) => + getService().setSubtitleTrack(sessionId, trackId) +); + +ipcMain.handle( + EMBEDDED_MPV_SET_SPEED, + (_event, sessionId: string, speed: number) => + getService().setSpeed(sessionId, speed) +); + +ipcMain.handle( + EMBEDDED_MPV_SET_ASPECT, + (_event, sessionId: string, aspect: string) => + getService().setAspect(sessionId, aspect) +); + ipcMain.handle(EMBEDDED_MPV_DISPOSE_SESSION, (_event, sessionId: string) => getService().disposeSession(sessionId) ); diff --git a/apps/electron-backend/src/app/services/embedded-mpv-native.service.ts b/apps/electron-backend/src/app/services/embedded-mpv-native.service.ts index aaf82ebd9..d1f8eeaa0 100644 --- a/apps/electron-backend/src/app/services/embedded-mpv-native.service.ts +++ b/apps/electron-backend/src/app/services/embedded-mpv-native.service.ts @@ -6,8 +6,10 @@ import App from '../app'; import { EmbeddedMpvAudioTrack, EmbeddedMpvBounds, + EmbeddedMpvCapabilities, EmbeddedMpvSession, EmbeddedMpvSessionStatus, + EmbeddedMpvSubtitleTrack, EmbeddedMpvSupport, EMBEDDED_MPV_SESSION_UPDATE, ResolvedPortalPlayback, @@ -21,6 +23,10 @@ interface NativeEmbeddedMpvSessionSnapshot { streamUrl: string; audioTracks?: EmbeddedMpvAudioTrack[]; selectedAudioTrackId?: number | null; + subtitleTracks?: EmbeddedMpvSubtitleTrack[]; + selectedSubtitleTrackId?: number | null; + playbackSpeed?: number; + aspectOverride?: string; error?: string; } @@ -38,6 +44,9 @@ interface NativeEmbeddedMpvAddon { seek(sessionId: string, seconds: number): void; setVolume(sessionId: string, volume: number): void; setAudioTrack(sessionId: string, trackId: number): void; + setSubtitleTrack?(sessionId: string, trackId: number): void; + setSpeed?(sessionId: string, speed: number): void; + setAspect?(sessionId: string, aspect: string): void; getSessionSnapshot( sessionId: string ): NativeEmbeddedMpvSessionSnapshot | null; @@ -69,6 +78,16 @@ export class EmbeddedMpvNativeService { private powerBlockerId: number | null = null; private readonly loadAddonModule = createRequire(__filename); + private detectCapabilities(): EmbeddedMpvCapabilities { + const addon = this.addon; + return { + subtitles: typeof addon?.setSubtitleTrack === 'function', + playbackSpeed: typeof addon?.setSpeed === 'function', + aspectOverride: typeof addon?.setAspect === 'function', + screenshot: false, + }; + } + getSupport(): EmbeddedMpvSupport { if (process.platform !== 'darwin') { return { @@ -99,6 +118,7 @@ export class EmbeddedMpvNativeService { return { supported: true, platform: process.platform, + capabilities: this.detectCapabilities(), }; } catch (error) { return { @@ -153,6 +173,7 @@ export class EmbeddedMpvNativeService { return { supported: true, platform: process.platform, + capabilities: this.detectCapabilities(), }; } @@ -175,6 +196,7 @@ export class EmbeddedMpvNativeService { return { supported: true, platform: process.platform, + capabilities: this.detectCapabilities(), }; } catch (error) { return { @@ -225,6 +247,10 @@ export class EmbeddedMpvNativeService { volume: 1, audioTracks: [], selectedAudioTrackId: null, + subtitleTracks: [], + selectedSubtitleTrackId: null, + playbackSpeed: 1, + aspectOverride: 'no', startedAt, updatedAt: startedAt, }; @@ -270,6 +296,45 @@ export class EmbeddedMpvNativeService { return this.refreshSession(sessionId); } + setSubtitleTrack( + sessionId: string, + trackId: number + ): EmbeddedMpvSession | null { + this.assertEmbeddedMpvEnabled(); + const addon = this.getAddon(); + if (typeof addon.setSubtitleTrack !== 'function') { + throw new Error( + 'Embedded MPV addon does not support subtitle tracks. Rebuild the native addon to enable this feature.' + ); + } + addon.setSubtitleTrack(sessionId, trackId); + return this.refreshSession(sessionId); + } + + setSpeed(sessionId: string, speed: number): EmbeddedMpvSession | null { + this.assertEmbeddedMpvEnabled(); + const addon = this.getAddon(); + if (typeof addon.setSpeed !== 'function') { + throw new Error( + 'Embedded MPV addon does not support playback speed. Rebuild the native addon to enable this feature.' + ); + } + addon.setSpeed(sessionId, speed); + return this.refreshSession(sessionId); + } + + setAspect(sessionId: string, aspect: string): EmbeddedMpvSession | null { + this.assertEmbeddedMpvEnabled(); + const addon = this.getAddon(); + if (typeof addon.setAspect !== 'function') { + throw new Error( + 'Embedded MPV addon does not support aspect override. Rebuild the native addon to enable this feature.' + ); + } + addon.setAspect(sessionId, aspect); + return this.refreshSession(sessionId); + } + disposeSession(sessionId: string): EmbeddedMpvSession | null { const session = this.sessions.get(sessionId); if (!session) { @@ -290,6 +355,10 @@ export class EmbeddedMpvNativeService { volume: 1, audioTracks: [], selectedAudioTrackId: null, + subtitleTracks: [], + selectedSubtitleTrackId: null, + playbackSpeed: 1, + aspectOverride: 'no', startedAt: session.startedAt, updatedAt: new Date().toISOString(), }; @@ -361,6 +430,21 @@ export class EmbeddedMpvNativeService { typeof snapshot.selectedAudioTrackId === 'number' ? snapshot.selectedAudioTrackId : null, + subtitleTracks: Array.isArray(snapshot.subtitleTracks) + ? snapshot.subtitleTracks + : [], + selectedSubtitleTrackId: + typeof snapshot.selectedSubtitleTrackId === 'number' + ? snapshot.selectedSubtitleTrackId + : null, + playbackSpeed: + typeof snapshot.playbackSpeed === 'number' + ? snapshot.playbackSpeed + : 1, + aspectOverride: + typeof snapshot.aspectOverride === 'string' + ? snapshot.aspectOverride + : 'no', startedAt: session.startedAt, updatedAt: new Date().toISOString(), ...(snapshot.error ? { error: snapshot.error } : {}), diff --git a/apps/web/src/typings.d.ts b/apps/web/src/typings.d.ts index 8ae50e31f..5a193fc82 100644 --- a/apps/web/src/typings.d.ts +++ b/apps/web/src/typings.d.ts @@ -369,6 +369,18 @@ declare global { sessionId: string, trackId: number ) => Promise; + setEmbeddedMpvSubtitleTrack?: ( + sessionId: string, + trackId: number + ) => Promise; + setEmbeddedMpvSpeed?: ( + sessionId: string, + speed: number + ) => Promise; + setEmbeddedMpvAspect?: ( + sessionId: string, + aspect: string + ) => Promise; disposeEmbeddedMpvSession: ( sessionId: string ) => Promise; diff --git a/global.d.ts b/global.d.ts index c5f02cc19..fba443ac3 100644 --- a/global.d.ts +++ b/global.d.ts @@ -155,6 +155,18 @@ declare global { sessionId: string, trackId: number ) => Promise; + setEmbeddedMpvSubtitleTrack?: ( + sessionId: string, + trackId: number + ) => Promise; + setEmbeddedMpvSpeed?: ( + sessionId: string, + speed: number + ) => Promise; + setEmbeddedMpvAspect?: ( + sessionId: string, + aspect: string + ) => Promise; disposeEmbeddedMpvSession: ( sessionId: string ) => Promise; diff --git a/libs/shared/interfaces/src/lib/embedded-mpv-session.interface.ts b/libs/shared/interfaces/src/lib/embedded-mpv-session.interface.ts index 94d826133..173edab45 100644 --- a/libs/shared/interfaces/src/lib/embedded-mpv-session.interface.ts +++ b/libs/shared/interfaces/src/lib/embedded-mpv-session.interface.ts @@ -13,10 +13,18 @@ export interface EmbeddedMpvBounds { height: number; } +export interface EmbeddedMpvCapabilities { + subtitles: boolean; + playbackSpeed: boolean; + aspectOverride: boolean; + screenshot: boolean; +} + export interface EmbeddedMpvSupport { supported: boolean; platform: string; reason?: string; + capabilities?: EmbeddedMpvCapabilities; } export interface EmbeddedMpvAudioTrack { @@ -28,6 +36,8 @@ export interface EmbeddedMpvAudioTrack { forced?: boolean; } +export type EmbeddedMpvSubtitleTrack = EmbeddedMpvAudioTrack; + export interface EmbeddedMpvSession { id: string; title: string; @@ -38,6 +48,10 @@ export interface EmbeddedMpvSession { volume: number; audioTracks: EmbeddedMpvAudioTrack[]; selectedAudioTrackId: number | null; + subtitleTracks: EmbeddedMpvSubtitleTrack[]; + selectedSubtitleTrackId: number | null; + playbackSpeed: number; + aspectOverride: string; startedAt: string; updatedAt: string; error?: string; diff --git a/libs/shared/interfaces/src/lib/ipc-commands.ts b/libs/shared/interfaces/src/lib/ipc-commands.ts index 335d4e95a..e71af008c 100644 --- a/libs/shared/interfaces/src/lib/ipc-commands.ts +++ b/libs/shared/interfaces/src/lib/ipc-commands.ts @@ -46,6 +46,10 @@ export const EMBEDDED_MPV_SET_PAUSED = 'EMBEDDED_MPV_SET_PAUSED'; export const EMBEDDED_MPV_SEEK = 'EMBEDDED_MPV_SEEK'; export const EMBEDDED_MPV_SET_VOLUME = 'EMBEDDED_MPV_SET_VOLUME'; export const EMBEDDED_MPV_SET_AUDIO_TRACK = 'EMBEDDED_MPV_SET_AUDIO_TRACK'; +export const EMBEDDED_MPV_SET_SUBTITLE_TRACK = + 'EMBEDDED_MPV_SET_SUBTITLE_TRACK'; +export const EMBEDDED_MPV_SET_SPEED = 'EMBEDDED_MPV_SET_SPEED'; +export const EMBEDDED_MPV_SET_ASPECT = 'EMBEDDED_MPV_SET_ASPECT'; export const EMBEDDED_MPV_DISPOSE_SESSION = 'EMBEDDED_MPV_DISPOSE_SESSION'; export const EMBEDDED_MPV_SESSION_UPDATE = 'EMBEDDED_MPV_SESSION_UPDATE'; export const EMBEDDED_MPV_PREPARE = 'EMBEDDED_MPV_PREPARE'; diff --git a/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-player.component.html b/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-player.component.html index 30388d336..0b542c53f 100644 --- a/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-player.component.html +++ b/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-player.component.html @@ -14,7 +14,20 @@ (focusin)="onPlayerInteraction()" >
- @if (isLoading() && !stalled()) { + @if (isErrored()) { + + } @else if (isLoading() && !stalled()) {
} + @if (capabilities().subtitles && hasSubtitleTracks()) { +
+ + @if (subtitleMenuOpen()) { + + } +
+ } + + @if (capabilities().playbackSpeed) { +
+ + @if (speedMenuOpen()) { + + } +
+ } + + @if (capabilities().aspectOverride) { +
+ + @if (aspectMenuOpen()) { + + } +
+ } +