Files
iptvnator/tools/packaging/electron-after-pack.cjs
T
4grayandClaude Fable 5 8eb0fe3261 feat(playback): embedded mpv pre-0.22 hardening, Linux parity, and localization (#1122)
* fix(playback): harden embedded mpv session handling and support detection

- guard the session controller against late startup rejections clobbering
  a newer session during fast channel zapping
- exclude the refresh timestamp from the session-update dedup key so idle
  sessions stop re-emitting IPC updates every 500 ms
- macOS: reconcile async loadfile replies by request id so a rejected
  seek/aid/speed on a live stream no longer flips the session to error
- append --ozone-platform=x11 on Linux in main.ts so direct binary and
  AppImage launches match the packaged .desktop launcher behavior
- return a sandbox-specific unsupported reason in Flatpak/Snap instead of
  asking the user to install mpv inside the sandbox
- update the stale "macOS only" embedded MPV claim in CLAUDE.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(playback): populate Linux audio tracks and fix ARM Linux packaging

- Linux poller now reads track-list/count each tick and walks the scalar
  track-list/N/* sub-properties when the count changes, so the audio-track
  menu is no longer empty; selection reconciles from the aid property
- afterPack replaces the x64 embedded_mpv.node with an
  embedded-mpv-unavailable.txt marker in arm64/armv7l Linux packages, and
  package-layout verification rejects foreign-architecture addons while
  requiring the marker
- extend native source invariants for the non-fatal async-reply rule
  (macOS) and the Linux track-list polling contract
- document the Linux track-list mechanics and ARM packaging behavior in
  docs/architecture/embedded-mpv-native.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(playback): embedded mpv player UX polish and full localization

- click on the video toggles pause (same action as Space) with a 250 ms
  grace period so double-click fullscreen cancels the pending pause; no
  DOM overlay is drawn — the dock transport icon is the feedback
- timeline scrubbing previews the drag position locally and commits a
  single seek on release instead of one IPC seek per drag pixel
- translate all player UI strings (controls, tooltips, aria-labels,
  status and recording messages) via new EMBEDDED_MPV.PLAYER.* keys,
  synced across en + 17 locales through the i18n-fill workflow
- replace the legacy @Output() EventEmitter with the signal output() API

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(playback): react to language changes and respect ozone platform hint

Address review feedback on #1122:
- add a translationsTick signal (onLangChange/onTranslationChange/
  onDefaultLangChange) read by every computed() and template helper that
  calls translate.instant(), so labels re-evaluate on a runtime language
  switch and when the translation file finishes loading after mount
- suppress the Linux --ozone-platform=x11 fallback when the user set
  ELECTRON_OZONE_PLATFORM_HINT, matching the existing respect for an
  explicit --ozone-platform switch

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 14:22:48 +02:00

133 lines
3.7 KiB
JavaScript

const linuxAfterPack = require('./linux-after-pack.cjs');
const {
isForeignLinuxEmbeddedMpvArch,
resolveElectronBuilderArchName,
validatePackagedEmbeddedMpv,
} = require('./embedded-mpv-packaging.cjs');
const fs = require('fs');
const path = require('path');
function log(message) {
console.log(` - ${message}`);
}
function isTruthy(value) {
return ['1', 'true', 'yes', 'on'].includes(
String(value ?? '')
.trim()
.toLowerCase()
);
}
function copyEmbeddedMpvNativeOutput(resourceDir, projectDir) {
const sourceDir = path.join(
projectDir,
'dist',
'apps',
'electron-backend',
'native'
);
if (!fs.existsSync(sourceDir)) {
return;
}
const destinationDir = path.join(
resourceDir,
'app.asar.unpacked',
'electron-backend',
'native'
);
fs.rmSync(destinationDir, { recursive: true, force: true });
fs.cpSync(sourceDir, destinationDir, { recursive: true });
}
function writeEmbeddedMpvUnavailableMarker(resourceDir, targetArch) {
const destinationDir = path.join(
resourceDir,
'app.asar.unpacked',
'electron-backend',
'native'
);
fs.rmSync(destinationDir, { recursive: true, force: true });
fs.mkdirSync(destinationDir, { recursive: true });
fs.writeFileSync(
path.join(destinationDir, 'embedded-mpv-unavailable.txt'),
`Embedded MPV is not bundled for ${targetArch} Linux builds yet. The built-in player and external MPV/VLC remain available.\n`
);
}
async function afterPackHook(params) {
await linuxAfterPack(params);
const requireEmbeddedMpv = isTruthy(
process.env.IPTVNATOR_REQUIRE_EMBEDDED_MPV
);
log(
requireEmbeddedMpv
? `validating required embedded MPV ${params.electronPlatformName} runtime`
: `validating optional embedded MPV ${params.electronPlatformName} runtime`
);
const resourceDir = getResourceDir(params);
const foreignArch = isForeignLinuxEmbeddedMpvArch(
params.electronPlatformName,
params.arch
);
if (foreignArch) {
const targetArch = resolveElectronBuilderArchName(params.arch);
log(
`embedded MPV addon is not built for ${targetArch}; packaging an unavailable marker instead`
);
writeEmbeddedMpvUnavailableMarker(resourceDir, targetArch);
} else {
copyEmbeddedMpvNativeOutput(
resourceDir,
params.packager.projectDir ?? process.cwd()
);
}
const errors = validatePackagedEmbeddedMpv(resourceDir, {
platform: params.electronPlatformName,
required: requireEmbeddedMpv,
foreignArch,
});
if (errors.length > 0) {
throw new Error(
[
`Embedded MPV ${params.electronPlatformName} package validation failed.`,
...errors.map((error) => `- ${error}`),
].join('\n')
);
}
log(`embedded MPV ${params.electronPlatformName} runtime validated`);
}
function getResourceDir(params) {
if (params.electronPlatformName !== 'darwin') {
return path.join(params.appOutDir, 'resources');
}
const appPath = params.appOutDir.endsWith('.app')
? params.appOutDir
: fs
.readdirSync(params.appOutDir)
.find((entry) => entry.endsWith('.app'));
return appPath
? path.join(
params.appOutDir.endsWith('.app')
? params.appOutDir
: path.join(params.appOutDir, appPath),
'Contents',
'Resources'
)
: params.appOutDir;
}
module.exports = afterPackHook;