mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 05:06:16 -08:00
feat(player): add embedded-mpv player for macOS as experimental feature
- Introduced tooling for building and staging the macOS `libmpv` runtime for IPTVnator's embedded MPV player. - Added `build-macos-runtime.mjs` for building an LGPL-compatible runtime from source. - Created `stage-macos-runtime.mjs` for staging the built runtime artifacts. - Implemented validation for the packaged embedded MPV runtime in `electron-after-pack.cjs` and `embedded-mpv-macos.cjs`. - Updated packaging scripts to ensure the embedded MPV runtime is correctly integrated and validated during the build process. - Added README files to document the expected layout and usage for the embedded MPV runtime artifacts. Entire-Checkpoint: c6e522b4276c
This commit is contained in:
1 parent
10139955e7
commit
9f873e6bed
53 files changed
+6099
-49
No files matched your search
@@ -47,6 +47,7 @@ describe('PlayerService', () => {
|
||||
expect(service.isEmbeddedPlayer(VideoPlayer.VideoJs)).toBe(true);
|
||||
expect(service.isEmbeddedPlayer(VideoPlayer.Html5Player)).toBe(true);
|
||||
expect(service.isEmbeddedPlayer(VideoPlayer.ArtPlayer)).toBe(true);
|
||||
expect(service.isEmbeddedPlayer(VideoPlayer.EmbeddedMpv)).toBe(true);
|
||||
expect(service.isEmbeddedPlayer(VideoPlayer.MPV)).toBe(false);
|
||||
expect(service.isEmbeddedPlayer(VideoPlayer.VLC)).toBe(false);
|
||||
});
|
||||
@@ -57,7 +58,19 @@ describe('PlayerService', () => {
|
||||
title: 'Example Video',
|
||||
});
|
||||
|
||||
expect(dialog.open).toHaveBeenCalled();
|
||||
expect(dialog.open).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
streamUrl: 'https://example.com/video.mp4',
|
||||
title: 'Example Video',
|
||||
playback: expect.objectContaining({
|
||||
streamUrl: 'https://example.com/video.mp4',
|
||||
title: 'Example Video',
|
||||
}),
|
||||
}),
|
||||
})
|
||||
);
|
||||
expect(dataService.sendIpcEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -26,7 +26,8 @@ export class PlayerService {
|
||||
return (
|
||||
player === VideoPlayer.VideoJs ||
|
||||
player === VideoPlayer.Html5Player ||
|
||||
player === VideoPlayer.ArtPlayer
|
||||
player === VideoPlayer.ArtPlayer ||
|
||||
player === VideoPlayer.EmbeddedMpv
|
||||
);
|
||||
}
|
||||
|
||||
@@ -118,7 +119,13 @@ export class PlayerService {
|
||||
return import('@iptvnator/portal/xtream/feature').then(
|
||||
({ PlayerDialogComponent }) => {
|
||||
this.dialog.open(PlayerDialogComponent, {
|
||||
data: { streamUrl, title, contentInfo, startTime },
|
||||
data: {
|
||||
streamUrl,
|
||||
title,
|
||||
contentInfo,
|
||||
startTime,
|
||||
playback,
|
||||
},
|
||||
width: '80%',
|
||||
maxWidth: '1200px',
|
||||
maxHeight: '90vh',
|
||||
|
||||
@@ -402,7 +402,7 @@ describe('SettingsComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
/* it('hides the embedded mpv option when the desktop support probe reports unsupported', async () => {
|
||||
it('hides the embedded mpv option when the desktop support probe reports unsupported', async () => {
|
||||
window.electron = {
|
||||
...window.electron,
|
||||
getEmbeddedMpvSupport: jest.fn().mockResolvedValue({
|
||||
@@ -416,9 +416,9 @@ describe('SettingsComponent', () => {
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(
|
||||
component.players().some(
|
||||
(player) => player.id === VideoPlayer.EmbeddedMpv
|
||||
)
|
||||
component
|
||||
.players()
|
||||
.some((player) => player.id === VideoPlayer.EmbeddedMpv)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
@@ -435,9 +435,9 @@ describe('SettingsComponent', () => {
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(
|
||||
component.players().some(
|
||||
(player) => player.id === VideoPlayer.EmbeddedMpv
|
||||
)
|
||||
component
|
||||
.players()
|
||||
.some((player) => player.id === VideoPlayer.EmbeddedMpv)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
@@ -458,9 +458,9 @@ describe('SettingsComponent', () => {
|
||||
expect(component.settingsForm.value).toEqual(DEFAULT_SETTINGS);
|
||||
expect(window.electron.getEmbeddedMpvSupport).toHaveBeenCalled();
|
||||
expect(
|
||||
component.players().some(
|
||||
(player) => player.id === VideoPlayer.EmbeddedMpv
|
||||
)
|
||||
component
|
||||
.players()
|
||||
.some((player) => player.id === VideoPlayer.EmbeddedMpv)
|
||||
).toBe(false);
|
||||
|
||||
resolveSupport!({
|
||||
@@ -470,11 +470,11 @@ describe('SettingsComponent', () => {
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(
|
||||
component.players().some(
|
||||
(player) => player.id === VideoPlayer.EmbeddedMpv
|
||||
)
|
||||
component
|
||||
.players()
|
||||
.some((player) => player.id === VideoPlayer.EmbeddedMpv)
|
||||
).toBe(true);
|
||||
}); */
|
||||
});
|
||||
});
|
||||
|
||||
describe('Version check', () => {
|
||||
|
||||
@@ -56,7 +56,7 @@ import {
|
||||
PlaylistsService,
|
||||
} from 'services';
|
||||
import {
|
||||
/* EmbeddedMpvSupport, */
|
||||
EmbeddedMpvSupport,
|
||||
Language,
|
||||
StartupBehavior,
|
||||
StreamFormat,
|
||||
@@ -151,10 +151,10 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
|
||||
/** Flag that indicates whether the app runs in electron environment */
|
||||
readonly isDesktop = !!window.electron;
|
||||
/* readonly embeddedMpvSupport = signal<EmbeddedMpvSupport | null>(null);
|
||||
readonly embeddedMpvSupport = signal<EmbeddedMpvSupport | null>(null);
|
||||
readonly supportsEmbeddedMpv = computed(
|
||||
() => this.isDesktop && !!this.embeddedMpvSupport()?.supported
|
||||
); */
|
||||
);
|
||||
|
||||
isPwa = this.dataService.getAppEnvironment() === 'pwa';
|
||||
|
||||
@@ -162,14 +162,14 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
readonly activeSection = this.settingsCtx.activeSection;
|
||||
|
||||
readonly osPlayers = computed(() => [
|
||||
/* ...(this.supportsEmbeddedMpv()
|
||||
...(this.supportsEmbeddedMpv()
|
||||
? [
|
||||
{
|
||||
id: VideoPlayer.EmbeddedMpv,
|
||||
labelKey: 'SETTINGS.PLAYER_EMBEDDED_MPV',
|
||||
},
|
||||
]
|
||||
: []), */
|
||||
: []),
|
||||
{
|
||||
id: VideoPlayer.MPV,
|
||||
labelKey: 'SETTINGS.PLAYER_MPV',
|
||||
@@ -437,7 +437,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
// Wait for settings to load before setting the form
|
||||
await this.settingsStore.loadSettings();
|
||||
this.setSettings();
|
||||
/* void this.loadEmbeddedMpvSupport(); */
|
||||
void this.loadEmbeddedMpvSupport();
|
||||
this.checkAppVersion();
|
||||
void this.fetchLocalIpAddresses();
|
||||
|
||||
@@ -448,7 +448,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
requestAnimationFrame(() => this.setupSectionObserver());
|
||||
}
|
||||
|
||||
/* private async loadEmbeddedMpvSupport(): Promise<void> {
|
||||
private async loadEmbeddedMpvSupport(): Promise<void> {
|
||||
if (!this.isDesktop) {
|
||||
this.embeddedMpvSupport.set({
|
||||
supported: false,
|
||||
@@ -475,13 +475,10 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
this.embeddedMpvSupport.set({
|
||||
supported: false,
|
||||
platform: window.electron.platform,
|
||||
reason:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: String(error),
|
||||
reason: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
} */
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.cancelPendingScrollTargetClear();
|
||||
|
||||
Vendored
+41
@@ -5,6 +5,9 @@ interface NodeModule {
|
||||
}
|
||||
|
||||
import {
|
||||
EmbeddedMpvBounds,
|
||||
EmbeddedMpvSession,
|
||||
EmbeddedMpvSupport,
|
||||
EpgChannel,
|
||||
EpgChannelMetadata,
|
||||
EpgProgram,
|
||||
@@ -14,6 +17,7 @@ import {
|
||||
PlaylistRefreshEvent,
|
||||
PlaylistRefreshPayload,
|
||||
PortalDebugEvent,
|
||||
ResolvedPortalPlayback,
|
||||
XtreamBackupFavoriteItem,
|
||||
XtreamBackupHiddenCategory,
|
||||
XtreamBackupRecentlyViewedItem,
|
||||
@@ -321,9 +325,46 @@ declare global {
|
||||
originalError: string;
|
||||
}) => void
|
||||
) => void;
|
||||
onEmbeddedMpvSessionUpdate?: (
|
||||
callback: (data: EmbeddedMpvSession) => void
|
||||
) => () => void;
|
||||
onExternalPlayerSessionUpdate?: (
|
||||
callback: (data: ExternalPlayerSession) => void
|
||||
) => () => void;
|
||||
getEmbeddedMpvSupport: () => Promise<EmbeddedMpvSupport>;
|
||||
prepareEmbeddedMpv?: () => Promise<EmbeddedMpvSupport>;
|
||||
createEmbeddedMpvSession: (
|
||||
bounds: EmbeddedMpvBounds,
|
||||
title?: string,
|
||||
initialVolume?: number
|
||||
) => Promise<EmbeddedMpvSession>;
|
||||
loadEmbeddedMpvPlayback: (
|
||||
sessionId: string,
|
||||
playback: ResolvedPortalPlayback
|
||||
) => Promise<void>;
|
||||
setEmbeddedMpvBounds: (
|
||||
sessionId: string,
|
||||
bounds: EmbeddedMpvBounds
|
||||
) => Promise<void>;
|
||||
setEmbeddedMpvPaused: (
|
||||
sessionId: string,
|
||||
paused: boolean
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
seekEmbeddedMpv: (
|
||||
sessionId: string,
|
||||
seconds: number
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
setEmbeddedMpvVolume: (
|
||||
sessionId: string,
|
||||
volume: number
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
setEmbeddedMpvAudioTrack: (
|
||||
sessionId: string,
|
||||
trackId: number
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
disposeEmbeddedMpvSession: (
|
||||
sessionId: string
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
getLocalIpAddresses: () => Promise<string[]>;
|
||||
// EPG progress listener
|
||||
onEpgProgress?: (
|
||||
|
||||
Reference in new issue
Block a user