mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
build: configure Flatpak finish arguments for external player launching
Entire-Checkpoint: 21b9b9f90f23
This commit is contained in:
1 parent
c06ed66c41
commit
35141bec31
5 files changed
+401
-59
No files matched your search
@@ -0,0 +1,128 @@
|
||||
jest.mock('electron', () => ({
|
||||
ipcMain: {
|
||||
handle: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../app', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
mainWindow: null,
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../services/store.service', () => ({
|
||||
MPV_PLAYER_PATH: 'MPV_PLAYER_PATH',
|
||||
MPV_REUSE_INSTANCE: 'MPV_REUSE_INSTANCE',
|
||||
VLC_PLAYER_PATH: 'VLC_PLAYER_PATH',
|
||||
store: {
|
||||
get: jest.fn(),
|
||||
set: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../services/stalker-playback-context.service', () => ({
|
||||
getStalkerPlaybackContextHeaders: jest.fn(() => undefined),
|
||||
}));
|
||||
|
||||
import {
|
||||
buildExternalPlayerSpawnSpec,
|
||||
isRunningInFlatpak,
|
||||
resolveExternalPlayerLaunchContext,
|
||||
shouldReuseMpvInstance,
|
||||
shouldUseMpvSocketBridge,
|
||||
} from './player.events';
|
||||
|
||||
function createPathExists(existingPaths: string[]) {
|
||||
return (candidatePath: string) => existingPaths.includes(candidatePath);
|
||||
}
|
||||
|
||||
describe('player.events Flatpak launch helpers', () => {
|
||||
it('detects Flatpak only on Linux when /.flatpak-info exists', () => {
|
||||
expect(isRunningInFlatpak(createPathExists(['/.flatpak-info']), 'linux')).toBe(
|
||||
true
|
||||
);
|
||||
expect(isRunningInFlatpak(createPathExists(['/.flatpak-info']), 'darwin')).toBe(
|
||||
false
|
||||
);
|
||||
expect(isRunningInFlatpak(createPathExists([]), 'linux')).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps direct Linux player launching outside Flatpak', () => {
|
||||
const launchContext = resolveExternalPlayerLaunchContext('mpv', undefined, {
|
||||
platform: 'linux',
|
||||
isFlatpak: false,
|
||||
pathExists: createPathExists(['/usr/local/bin/mpv']),
|
||||
});
|
||||
const spawnSpec = buildExternalPlayerSpawnSpec(launchContext, [
|
||||
'--ytdl=no',
|
||||
'https://example.com/stream.m3u8',
|
||||
]);
|
||||
|
||||
expect(launchContext).toEqual({
|
||||
mode: 'direct',
|
||||
playerPath: '/usr/local/bin/mpv',
|
||||
command: '/usr/local/bin/mpv',
|
||||
argsPrefix: [],
|
||||
});
|
||||
expect(spawnSpec).toEqual({
|
||||
mode: 'direct',
|
||||
playerPath: '/usr/local/bin/mpv',
|
||||
command: '/usr/local/bin/mpv',
|
||||
args: ['--ytdl=no', 'https://example.com/stream.m3u8'],
|
||||
});
|
||||
});
|
||||
|
||||
it('builds Flatpak host launches with bare player names by default', () => {
|
||||
const launchContext = resolveExternalPlayerLaunchContext('vlc', undefined, {
|
||||
platform: 'linux',
|
||||
isFlatpak: true,
|
||||
pathExists: createPathExists(['/usr/bin/vlc']),
|
||||
});
|
||||
const spawnSpec = buildExternalPlayerSpawnSpec(launchContext, [
|
||||
'--extraintf=rc',
|
||||
'https://example.com/stream.m3u8',
|
||||
]);
|
||||
|
||||
expect(launchContext).toEqual({
|
||||
mode: 'flatpak-host',
|
||||
playerPath: 'vlc',
|
||||
command: 'flatpak-spawn',
|
||||
argsPrefix: ['--host', '--watch-bus', 'vlc'],
|
||||
});
|
||||
expect(spawnSpec.args).toEqual([
|
||||
'--host',
|
||||
'--watch-bus',
|
||||
'vlc',
|
||||
'--extraintf=rc',
|
||||
'https://example.com/stream.m3u8',
|
||||
]);
|
||||
});
|
||||
|
||||
it('passes custom host player paths through Flatpak launching unchanged', () => {
|
||||
const launchContext = resolveExternalPlayerLaunchContext(
|
||||
'mpv',
|
||||
'/opt/mpv/bin/mpv',
|
||||
{
|
||||
platform: 'linux',
|
||||
isFlatpak: true,
|
||||
}
|
||||
);
|
||||
|
||||
expect(launchContext).toEqual({
|
||||
mode: 'flatpak-host',
|
||||
playerPath: '/opt/mpv/bin/mpv',
|
||||
command: 'flatpak-spawn',
|
||||
argsPrefix: ['--host', '--watch-bus', '/opt/mpv/bin/mpv'],
|
||||
});
|
||||
});
|
||||
|
||||
it('disables MPV reuse and socket bridging only in Flatpak', () => {
|
||||
expect(shouldReuseMpvInstance(true, true)).toBe(false);
|
||||
expect(shouldUseMpvSocketBridge(true)).toBe(false);
|
||||
|
||||
expect(shouldReuseMpvInstance(true, false)).toBe(true);
|
||||
expect(shouldReuseMpvInstance(false, false)).toBe(false);
|
||||
expect(shouldUseMpvSocketBridge(false)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import { ipcMain } from 'electron';
|
||||
import {
|
||||
CLOSE_EXTERNAL_PLAYER_SESSION,
|
||||
EXTERNAL_PLAYER_SESSION_UPDATE,
|
||||
ExternalPlayerName,
|
||||
ExternalPlayerSession,
|
||||
} from 'shared-interfaces';
|
||||
import App from '../app';
|
||||
@@ -40,6 +41,138 @@ const externalPlayerSessions = new ExternalPlayerSessionRegistry(
|
||||
sendExternalPlayerSessionUpdate
|
||||
);
|
||||
|
||||
type PathExists = (path: string) => boolean;
|
||||
|
||||
type ExternalPlayerLaunchMode = 'direct' | 'flatpak-host';
|
||||
|
||||
interface PlayerPathOptions {
|
||||
platform?: NodeJS.Platform;
|
||||
isFlatpak?: boolean;
|
||||
pathExists?: PathExists;
|
||||
}
|
||||
|
||||
interface ExternalPlayerLaunchContext {
|
||||
mode: ExternalPlayerLaunchMode;
|
||||
playerPath: string;
|
||||
command: string;
|
||||
argsPrefix: string[];
|
||||
}
|
||||
|
||||
interface ExternalPlayerSpawnSpec {
|
||||
mode: ExternalPlayerLaunchMode;
|
||||
playerPath: string;
|
||||
command: string;
|
||||
args: string[];
|
||||
}
|
||||
|
||||
export function isRunningInFlatpak(
|
||||
pathExists: PathExists = existsSync,
|
||||
platform: NodeJS.Platform = process.platform
|
||||
): boolean {
|
||||
return platform === 'linux' && pathExists('/.flatpak-info');
|
||||
}
|
||||
|
||||
function normalizeCustomPlayerPath(value: string | undefined): string | null {
|
||||
const trimmedValue = value?.trim();
|
||||
return trimmedValue ? trimmedValue : null;
|
||||
}
|
||||
|
||||
function getDefaultPlayerPath(
|
||||
player: ExternalPlayerName,
|
||||
options: PlayerPathOptions = {}
|
||||
): string {
|
||||
const {
|
||||
platform = process.platform,
|
||||
isFlatpak = isRunningInFlatpak(),
|
||||
pathExists = existsSync,
|
||||
} = options;
|
||||
|
||||
if (platform === 'linux' && isFlatpak) {
|
||||
return player;
|
||||
}
|
||||
|
||||
if (player === 'mpv') {
|
||||
return getDefaultMpvPath({ platform, isFlatpak, pathExists });
|
||||
}
|
||||
|
||||
return getDefaultVlcPath({ platform, isFlatpak, pathExists });
|
||||
}
|
||||
|
||||
export function resolveExternalPlayerLaunchContext(
|
||||
player: ExternalPlayerName,
|
||||
customPlayerPath?: string,
|
||||
options: PlayerPathOptions = {}
|
||||
): ExternalPlayerLaunchContext {
|
||||
const {
|
||||
platform = process.platform,
|
||||
isFlatpak = isRunningInFlatpak(),
|
||||
pathExists = existsSync,
|
||||
} = options;
|
||||
const playerPath =
|
||||
normalizeCustomPlayerPath(customPlayerPath) ??
|
||||
getDefaultPlayerPath(player, {
|
||||
platform,
|
||||
isFlatpak,
|
||||
pathExists,
|
||||
});
|
||||
|
||||
if (platform === 'linux' && isFlatpak) {
|
||||
return {
|
||||
mode: 'flatpak-host',
|
||||
playerPath,
|
||||
command: 'flatpak-spawn',
|
||||
argsPrefix: ['--host', '--watch-bus', playerPath],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
mode: 'direct',
|
||||
playerPath,
|
||||
command: playerPath,
|
||||
argsPrefix: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function buildExternalPlayerSpawnSpec(
|
||||
launchContext: ExternalPlayerLaunchContext,
|
||||
playerArgs: string[]
|
||||
): ExternalPlayerSpawnSpec {
|
||||
return {
|
||||
mode: launchContext.mode,
|
||||
playerPath: launchContext.playerPath,
|
||||
command: launchContext.command,
|
||||
args: [...launchContext.argsPrefix, ...playerArgs],
|
||||
};
|
||||
}
|
||||
|
||||
export function shouldReuseMpvInstance(
|
||||
requestedReuseInstance: boolean,
|
||||
isFlatpak: boolean = isRunningInFlatpak()
|
||||
): boolean {
|
||||
return !isFlatpak && requestedReuseInstance;
|
||||
}
|
||||
|
||||
export function shouldUseMpvSocketBridge(
|
||||
isFlatpak: boolean = isRunningInFlatpak()
|
||||
): boolean {
|
||||
return !isFlatpak;
|
||||
}
|
||||
|
||||
function buildPlayerStartError(
|
||||
player: 'MPV' | 'VLC',
|
||||
error: Error,
|
||||
launchContext: ExternalPlayerLaunchContext
|
||||
): Error {
|
||||
const guidance =
|
||||
launchContext.mode === 'flatpak-host'
|
||||
? `Make sure ${player} is installed on the host system and reachable via Flatpak host spawning at '${launchContext.playerPath}'.`
|
||||
: `Make sure ${player} is installed and the path '${launchContext.playerPath}' is correct.`;
|
||||
|
||||
return new Error(
|
||||
`Failed to start ${player} player: ${error.message}. ${guidance}`
|
||||
);
|
||||
}
|
||||
|
||||
// Helper function to send error notifications to the renderer
|
||||
function sendPlayerErrorNotification(player: 'MPV' | 'VLC', error: string) {
|
||||
if (App.mainWindow && !App.mainWindow.isDestroyed()) {
|
||||
@@ -383,8 +516,18 @@ ipcMain.handle(
|
||||
});
|
||||
|
||||
try {
|
||||
const mpvPath = getMpvPath();
|
||||
const reuseInstance = store.get(MPV_REUSE_INSTANCE, false);
|
||||
const isFlatpak = isRunningInFlatpak();
|
||||
const mpvLaunchContext = resolveExternalPlayerLaunchContext(
|
||||
'mpv',
|
||||
getMpvPath({ isFlatpak }),
|
||||
{ isFlatpak }
|
||||
);
|
||||
const requestedReuseInstance = store.get(MPV_REUSE_INSTANCE, false);
|
||||
const reuseInstance = shouldReuseMpvInstance(
|
||||
requestedReuseInstance,
|
||||
isFlatpak
|
||||
);
|
||||
const useMpvSocketBridge = shouldUseMpvSocketBridge(isFlatpak);
|
||||
const fallbackHeaders = getStalkerPlaybackContextHeaders(url) ?? {};
|
||||
const mergedHeaders = isStalkerDirectStreamProfile(fallbackHeaders)
|
||||
? fallbackHeaders
|
||||
@@ -413,7 +556,9 @@ ipcMain.handle(
|
||||
);
|
||||
|
||||
console.log('[MPV] Opening player', {
|
||||
path: mpvPath,
|
||||
path: mpvLaunchContext.playerPath,
|
||||
launchMode: mpvLaunchContext.mode,
|
||||
requestedReuseInstance,
|
||||
reuseInstance,
|
||||
stream: maskUrlForLogs(url),
|
||||
hasUserAgent: Boolean(effectiveUserAgent),
|
||||
@@ -520,19 +665,20 @@ ipcMain.handle(
|
||||
// Create new MPV process
|
||||
console.log('Creating new MPV instance');
|
||||
|
||||
// Generate unique socket path
|
||||
const socketPath =
|
||||
process.platform === 'win32'
|
||||
? `\\\\.\\pipe\\mpv-${Date.now()}`
|
||||
: `/tmp/mpvsocket-${Date.now()}`;
|
||||
let socketPath: string | null = null;
|
||||
const args: string[] = [];
|
||||
|
||||
const args = [
|
||||
`--input-ipc-server=${socketPath}`,
|
||||
'--idle=yes',
|
||||
// IPTV URLs often look like generic web pages to ytdl_hook and can fail with 403.
|
||||
// Force MPV to open them directly instead of probing via yt-dlp/youtube-dl.
|
||||
'--ytdl=no',
|
||||
];
|
||||
if (useMpvSocketBridge) {
|
||||
socketPath =
|
||||
process.platform === 'win32'
|
||||
? `\\\\.\\pipe\\mpv-${Date.now()}`
|
||||
: `/tmp/mpvsocket-${Date.now()}`;
|
||||
args.push(`--input-ipc-server=${socketPath}`, '--idle=yes');
|
||||
}
|
||||
|
||||
// IPTV URLs often look like generic web pages to ytdl_hook and can fail with 403.
|
||||
// Force MPV to open them directly instead of probing via yt-dlp/youtube-dl.
|
||||
args.push('--ytdl=no');
|
||||
|
||||
// Add user agent if provided
|
||||
if (effectiveUserAgent) {
|
||||
@@ -563,7 +709,11 @@ ipcMain.handle(
|
||||
|
||||
// Wrap spawn in a promise to catch startup errors
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const proc = spawn(mpvPath, args, {
|
||||
const spawnSpec = buildExternalPlayerSpawnSpec(
|
||||
mpvLaunchContext,
|
||||
args
|
||||
);
|
||||
const proc = spawn(spawnSpec.command, spawnSpec.args, {
|
||||
shell: false,
|
||||
detached: !reuseInstance,
|
||||
// Only pipe stdio when reusing instance; use 'ignore' for detached to allow clean shutdown
|
||||
@@ -644,11 +794,7 @@ ipcMain.handle(
|
||||
session.id,
|
||||
`Failed to start MPV player: ${err.message}`
|
||||
);
|
||||
reject(
|
||||
new Error(
|
||||
`Failed to start MPV player: ${err.message}. Make sure MPV is installed and the path '${mpvPath}' is correct.`
|
||||
)
|
||||
);
|
||||
reject(buildPlayerStartError('MPV', err, mpvLaunchContext));
|
||||
});
|
||||
|
||||
proc.on('exit', (code) => {
|
||||
@@ -677,7 +823,7 @@ ipcMain.handle(
|
||||
});
|
||||
|
||||
// Store the process reference if reuse is enabled
|
||||
if (reuseInstance) {
|
||||
if (reuseInstance && socketPath) {
|
||||
mpvProcess = proc;
|
||||
mpvSocketPath = socketPath;
|
||||
console.log(
|
||||
@@ -696,7 +842,7 @@ ipcMain.handle(
|
||||
});
|
||||
|
||||
// Start polling if content info is provided
|
||||
if (contentInfo) {
|
||||
if (useMpvSocketBridge && contentInfo && socketPath) {
|
||||
startPositionPolling(socketPath, contentInfo, session.id);
|
||||
}
|
||||
|
||||
@@ -765,7 +911,12 @@ ipcMain.handle(
|
||||
});
|
||||
|
||||
try {
|
||||
const vlcPath = getVlcPath();
|
||||
const isFlatpak = isRunningInFlatpak();
|
||||
const vlcLaunchContext = resolveExternalPlayerLaunchContext(
|
||||
'vlc',
|
||||
getVlcPath({ isFlatpak }),
|
||||
{ isFlatpak }
|
||||
);
|
||||
const fallbackHeaders = getStalkerPlaybackContextHeaders(url) ?? {};
|
||||
const mergedHeaders = isStalkerDirectStreamProfile(fallbackHeaders)
|
||||
? fallbackHeaders
|
||||
@@ -788,7 +939,8 @@ ipcMain.handle(
|
||||
mergedHeaders['User-Agent'] ??
|
||||
mergedHeaders['user-agent'] ??
|
||||
undefined;
|
||||
console.log('Opening VLC player with path:', vlcPath);
|
||||
console.log('Opening VLC player with path:', vlcLaunchContext.playerPath);
|
||||
console.log('VLC launch mode:', vlcLaunchContext.mode);
|
||||
console.log('URL:', url);
|
||||
// console.log('User-Agent:', userAgent);
|
||||
// console.log('Referer:', referer);
|
||||
@@ -854,8 +1006,12 @@ ipcMain.handle(
|
||||
|
||||
// Wrap spawn in a promise to catch startup errors
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const spawnVlc = (spawnArgs: string[], isRetry = false) => {
|
||||
const proc = spawn(vlcPath, spawnArgs, {
|
||||
const spawnVlc = (playerArgs: string[], isRetry = false) => {
|
||||
const spawnSpec = buildExternalPlayerSpawnSpec(
|
||||
vlcLaunchContext,
|
||||
playerArgs
|
||||
);
|
||||
const proc = spawn(spawnSpec.command, spawnSpec.args, {
|
||||
shell: false,
|
||||
detached: true,
|
||||
stdio: 'ignore', // Use 'ignore' for detached to allow clean shutdown
|
||||
@@ -897,7 +1053,7 @@ ipcMain.handle(
|
||||
if (!isRetry && rcPort > 0) {
|
||||
console.log('Retrying VLC without RC interface...');
|
||||
// Retry without RC args
|
||||
const retryArgs = spawnArgs.filter(
|
||||
const retryArgs = playerArgs.filter(
|
||||
(arg) =>
|
||||
!arg.includes('--extraintf') &&
|
||||
!arg.includes('--rc-host') &&
|
||||
@@ -910,8 +1066,10 @@ ipcMain.handle(
|
||||
`Failed to start VLC player: ${err.message}`
|
||||
);
|
||||
reject(
|
||||
new Error(
|
||||
`Failed to start VLC player: ${err.message}. Make sure VLC is installed and the path '${vlcPath}' is correct.`
|
||||
buildPlayerStartError(
|
||||
'VLC',
|
||||
err,
|
||||
vlcLaunchContext
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -925,7 +1083,7 @@ ipcMain.handle(
|
||||
'VLC exited with error, retrying without RC interface...'
|
||||
);
|
||||
stopVlcPositionPolling();
|
||||
const retryArgs = spawnArgs.filter(
|
||||
const retryArgs = playerArgs.filter(
|
||||
(arg) =>
|
||||
!arg.includes('--extraintf') &&
|
||||
!arg.includes('--rc-host') &&
|
||||
@@ -985,26 +1143,32 @@ ipcMain.handle(
|
||||
}
|
||||
);
|
||||
|
||||
function getMpvPath() {
|
||||
const customMpvPath = store.get(MPV_PLAYER_PATH);
|
||||
if (customMpvPath) {
|
||||
return customMpvPath;
|
||||
} else {
|
||||
return getDefaultMpvPath();
|
||||
}
|
||||
function getMpvPath(options: PlayerPathOptions = {}) {
|
||||
return (
|
||||
normalizeCustomPlayerPath(store.get(MPV_PLAYER_PATH)) ??
|
||||
getDefaultMpvPath(options)
|
||||
);
|
||||
}
|
||||
|
||||
function getVlcPath() {
|
||||
const customVlcPath = store.get(VLC_PLAYER_PATH);
|
||||
if (customVlcPath) {
|
||||
return customVlcPath;
|
||||
} else {
|
||||
return getDefaultVlcPath();
|
||||
}
|
||||
function getVlcPath(options: PlayerPathOptions = {}) {
|
||||
return (
|
||||
normalizeCustomPlayerPath(store.get(VLC_PLAYER_PATH)) ??
|
||||
getDefaultVlcPath(options)
|
||||
);
|
||||
}
|
||||
|
||||
function getDefaultMpvPath() {
|
||||
if (process.platform === 'win32') {
|
||||
function getDefaultMpvPath(options: PlayerPathOptions = {}) {
|
||||
const {
|
||||
platform = process.platform,
|
||||
isFlatpak = isRunningInFlatpak(),
|
||||
pathExists = existsSync,
|
||||
} = options;
|
||||
|
||||
if (platform === 'linux' && isFlatpak) {
|
||||
return 'mpv';
|
||||
}
|
||||
|
||||
if (platform === 'win32') {
|
||||
// Check multiple common Windows paths
|
||||
const windowsPaths = [
|
||||
path.join('C:', 'Program Files', 'mpv', 'mpv.exe'),
|
||||
@@ -1013,13 +1177,13 @@ function getDefaultMpvPath() {
|
||||
|
||||
// Check if any of the paths exist
|
||||
for (const mpvPath of windowsPaths) {
|
||||
if (existsSync(mpvPath)) {
|
||||
if (pathExists(mpvPath)) {
|
||||
return mpvPath;
|
||||
}
|
||||
}
|
||||
// Default to just 'mpv' if it's in PATH
|
||||
return 'mpv';
|
||||
} else if (process.platform === 'linux') {
|
||||
} else if (platform === 'linux') {
|
||||
// Check multiple common Linux paths
|
||||
const linuxPaths = [
|
||||
'/usr/bin/mpv',
|
||||
@@ -1028,12 +1192,12 @@ function getDefaultMpvPath() {
|
||||
];
|
||||
|
||||
for (const mpvPath of linuxPaths) {
|
||||
if (existsSync(mpvPath)) {
|
||||
if (pathExists(mpvPath)) {
|
||||
return mpvPath;
|
||||
}
|
||||
}
|
||||
return 'mpv';
|
||||
} else if (process.platform === 'darwin') {
|
||||
} else if (platform === 'darwin') {
|
||||
// Check multiple common macOS paths
|
||||
const macosPaths = [
|
||||
'/Applications/mpv.app/Contents/MacOS/mpv',
|
||||
@@ -1042,7 +1206,7 @@ function getDefaultMpvPath() {
|
||||
];
|
||||
|
||||
for (const mpvPath of macosPaths) {
|
||||
if (existsSync(mpvPath)) {
|
||||
if (pathExists(mpvPath)) {
|
||||
return mpvPath;
|
||||
}
|
||||
}
|
||||
@@ -1053,8 +1217,18 @@ function getDefaultMpvPath() {
|
||||
return 'mpv';
|
||||
}
|
||||
|
||||
function getDefaultVlcPath() {
|
||||
if (process.platform === 'win32') {
|
||||
function getDefaultVlcPath(options: PlayerPathOptions = {}) {
|
||||
const {
|
||||
platform = process.platform,
|
||||
isFlatpak = isRunningInFlatpak(),
|
||||
pathExists = existsSync,
|
||||
} = options;
|
||||
|
||||
if (platform === 'linux' && isFlatpak) {
|
||||
return 'vlc';
|
||||
}
|
||||
|
||||
if (platform === 'win32') {
|
||||
// Check multiple common Windows paths (64-bit and 32-bit)
|
||||
const windowsPaths = [
|
||||
path.join('C:', 'Program Files', 'VideoLAN', 'VLC', 'vlc.exe'),
|
||||
@@ -1068,12 +1242,12 @@ function getDefaultVlcPath() {
|
||||
];
|
||||
|
||||
for (const vlcPath of windowsPaths) {
|
||||
if (existsSync(vlcPath)) {
|
||||
if (pathExists(vlcPath)) {
|
||||
return vlcPath;
|
||||
}
|
||||
}
|
||||
return 'vlc';
|
||||
} else if (process.platform === 'linux') {
|
||||
} else if (platform === 'linux') {
|
||||
// Check multiple common Linux paths
|
||||
const linuxPaths = [
|
||||
'/usr/bin/vlc',
|
||||
@@ -1082,12 +1256,12 @@ function getDefaultVlcPath() {
|
||||
];
|
||||
|
||||
for (const vlcPath of linuxPaths) {
|
||||
if (existsSync(vlcPath)) {
|
||||
if (pathExists(vlcPath)) {
|
||||
return vlcPath;
|
||||
}
|
||||
}
|
||||
return 'vlc';
|
||||
} else if (process.platform === 'darwin') {
|
||||
} else if (platform === 'darwin') {
|
||||
// Check multiple common macOS paths
|
||||
const macosPaths = [
|
||||
'/Applications/VLC.app/Contents/MacOS/VLC',
|
||||
@@ -1095,7 +1269,7 @@ function getDefaultVlcPath() {
|
||||
];
|
||||
|
||||
for (const vlcPath of macosPaths) {
|
||||
if (existsSync(vlcPath)) {
|
||||
if (pathExists(vlcPath)) {
|
||||
return vlcPath;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ This document records the current contract for embedded playback in portal detai
|
||||
|
||||
- Embedded web players are `videojs`, `html5`, and `artplayer`.
|
||||
- External players are `mpv` and `vlc`.
|
||||
- Flatpak launches external players on the host via `flatpak-spawn --host`.
|
||||
- Live playback stays inline in dedicated live layouts.
|
||||
- VOD and series detail playback now also stays inline on canonical detail surfaces.
|
||||
- Material dialog playback remains only as a fallback for older non-detail callers.
|
||||
@@ -56,6 +57,19 @@ When a detail view starts playback:
|
||||
|
||||
The detail host owns inline state. `PlayerService` is no longer the primary owner of UI playback state for canonical VOD/series detail screens.
|
||||
|
||||
## Flatpak External Players
|
||||
|
||||
Flatpak cannot execute host-installed `mpv` or `vlc` binaries directly from the sandbox.
|
||||
|
||||
Current contract:
|
||||
|
||||
- Flatpak launches external players through `flatpak-spawn --host`.
|
||||
- AppImage, deb/rpm, snap, macOS, and Windows keep the existing direct process spawn flow.
|
||||
- VLC keeps the current external-session flow in Flatpak, including the RC port used for progress polling.
|
||||
- MPV is intentionally reduced in Flatpak: the app does not reuse an existing MPV instance there and does not open the Unix socket bridge used for non-Flatpak progress polling.
|
||||
|
||||
This keeps non-Flatpak behavior unchanged while allowing Flatpak builds to open host-installed external players.
|
||||
|
||||
## Typed Playback Payload
|
||||
|
||||
Shared playback payloads live in:
|
||||
|
||||
@@ -77,6 +77,17 @@
|
||||
"sdk": "org.freedesktop.Sdk",
|
||||
"base": "org.electronjs.Electron2.BaseApp",
|
||||
"baseVersion": "24.08",
|
||||
"finishArgs": [
|
||||
"--socket=wayland",
|
||||
"--socket=x11",
|
||||
"--share=ipc",
|
||||
"--device=dri",
|
||||
"--socket=pulseaudio",
|
||||
"--filesystem=home",
|
||||
"--share=network",
|
||||
"--talk-name=org.freedesktop.Notifications",
|
||||
"--talk-name=org.freedesktop.Flatpak"
|
||||
],
|
||||
"files": [
|
||||
[
|
||||
"apps/electron-backend/linux/com.fourgray.iptvnator.metainfo.xml",
|
||||
|
||||
@@ -27,6 +27,7 @@ const packageMetadata = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
const electronBuilderConfig = JSON.parse(
|
||||
fs.readFileSync(electronBuilderConfigPath, 'utf8')
|
||||
);
|
||||
const flatpakFinishArgs = electronBuilderConfig.flatpak?.finishArgs ?? [];
|
||||
const workerRelativeDir = path.join(
|
||||
'dist',
|
||||
'apps',
|
||||
@@ -172,6 +173,19 @@ function verifyLinuxLauncher(resourceDir, errors) {
|
||||
}
|
||||
}
|
||||
|
||||
function verifyFlatpakPermissions(errors) {
|
||||
if (!Array.isArray(flatpakFinishArgs)) {
|
||||
errors.push('Flatpak finishArgs must be configured as an array.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!flatpakFinishArgs.includes('--talk-name=org.freedesktop.Flatpak')) {
|
||||
errors.push(
|
||||
'Flatpak finishArgs must include --talk-name=org.freedesktop.Flatpak for host player launching.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function verifyResourceDir(resourceDir) {
|
||||
const missingWorkers = workerFiles.filter(
|
||||
(workerFile) =>
|
||||
@@ -207,6 +221,7 @@ function verifyResourceDir(resourceDir) {
|
||||
if (!fileExists(flatpakMetainfoPath)) {
|
||||
errors.push(`Missing Flatpak metainfo file: ${flatpakMetainfoPath}`);
|
||||
}
|
||||
verifyFlatpakPermissions(errors);
|
||||
verifyLinuxLauncher(resourceDir, errors);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user