Files
iptvnator/tools/packaging/embedded-mpv-arch.test.mjs
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

126 lines
4.2 KiB
JavaScript

import assert from 'node:assert/strict';
import fs from 'node:fs';
import { createRequire } from 'node:module';
import os from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
const require = createRequire(import.meta.url);
const {
isForeignLinuxEmbeddedMpvArch,
linuxUnpackedDirArch,
resolveElectronBuilderArchName,
validatePackagedEmbeddedMpv,
} = require('./embedded-mpv-packaging.cjs');
const X64_ADDON_ENV = { IPTVNATOR_EMBEDDED_MPV_ARCH: 'x64' };
test('resolveElectronBuilderArchName maps builder-util Arch enum values', () => {
assert.equal(resolveElectronBuilderArchName(0), 'ia32');
assert.equal(resolveElectronBuilderArchName(1), 'x64');
assert.equal(resolveElectronBuilderArchName(2), 'armv7l');
assert.equal(resolveElectronBuilderArchName(3), 'arm64');
assert.equal(resolveElectronBuilderArchName('arm64'), 'arm64');
assert.equal(resolveElectronBuilderArchName(99), null);
});
test('flags Linux packages whose arch differs from the built addon', () => {
assert.equal(isForeignLinuxEmbeddedMpvArch('linux', 3, X64_ADDON_ENV), true);
assert.equal(
isForeignLinuxEmbeddedMpvArch('linux', 'armv7l', X64_ADDON_ENV),
true
);
assert.equal(
isForeignLinuxEmbeddedMpvArch('linux', 'x64', X64_ADDON_ENV),
false
);
// Only Linux fans out foreign arches from one dist tree.
assert.equal(
isForeignLinuxEmbeddedMpvArch('darwin', 'arm64', X64_ADDON_ENV),
false
);
assert.equal(
isForeignLinuxEmbeddedMpvArch('win32', 'arm64', X64_ADDON_ENV),
false
);
});
test('derives package arch from electron-builder Linux output directory names', () => {
assert.equal(linuxUnpackedDirArch('linux-unpacked'), 'x64');
assert.equal(linuxUnpackedDirArch('linux-arm64-unpacked'), 'arm64');
assert.equal(linuxUnpackedDirArch('linux-armv7l-unpacked'), 'armv7l');
assert.equal(linuxUnpackedDirArch('mac-arm64'), null);
assert.equal(linuxUnpackedDirArch('win-unpacked'), null);
});
function createResourceDir(files) {
const resourceDir = fs.mkdtempSync(
join(os.tmpdir(), 'iptvnator-embedded-mpv-arch-')
);
const nativeDir = join(
resourceDir,
'app.asar.unpacked',
'electron-backend',
'native'
);
fs.mkdirSync(nativeDir, { recursive: true });
for (const [name, content] of Object.entries(files)) {
fs.writeFileSync(join(nativeDir, name), content);
}
return resourceDir;
}
test('foreign-arch validation requires the unavailable marker and forbids the addon', (t) => {
const withMarker = createResourceDir({
'embedded-mpv-unavailable.txt': 'not bundled for arm64\n',
});
const withAddon = createResourceDir({
'embedded-mpv-unavailable.txt': 'not bundled for arm64\n',
'embedded_mpv.node': 'x64 machine code',
});
const empty = createResourceDir({});
t.after(() => {
for (const dir of [withMarker, withAddon, empty]) {
fs.rmSync(dir, { recursive: true, force: true });
}
});
assert.deepEqual(
validatePackagedEmbeddedMpv(withMarker, {
platform: 'linux',
required: true,
foreignArch: true,
}),
[]
);
const addonErrors = validatePackagedEmbeddedMpv(withAddon, {
platform: 'linux',
required: true,
foreignArch: true,
});
assert.equal(addonErrors.length, 1);
assert.match(addonErrors[0], /must not ship in foreign-architecture/);
const markerErrors = validatePackagedEmbeddedMpv(empty, {
platform: 'linux',
required: true,
foreignArch: true,
});
assert.equal(markerErrors.length, 1);
assert.match(markerErrors[0], /unavailable marker/);
});
test('same-arch validation still requires the addon when embedded MPV is required', (t) => {
const empty = createResourceDir({});
t.after(() => fs.rmSync(empty, { recursive: true, force: true }));
const errors = validatePackagedEmbeddedMpv(empty, {
platform: 'linux',
required: true,
foreignArch: false,
});
assert.equal(errors.length, 1);
assert.match(errors[0], /Missing embedded MPV native addon/);
});