mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
feat(electron): add embedded mpv support for windows and linux (#1031)
Adds experimental embedded MPV support for Windows and Linux, expands packaging/runtime validation, and updates CI coverage for the cross-platform native addon.
This commit is contained in:
1 parent
77cf4065e0
commit
06700b318a
40 files changed
+2958
-257
No files matched your search
@@ -23,18 +23,33 @@ jobs:
|
||||
- os: macos
|
||||
runner: macos-15-intel
|
||||
arch: x64
|
||||
embedded_mpv_platform: darwin
|
||||
embedded_mpv_arch: x64
|
||||
embedded_mpv_build_runtime: true
|
||||
- os: macos
|
||||
runner: macos-latest
|
||||
arch: arm64
|
||||
embedded_mpv_platform: darwin
|
||||
embedded_mpv_arch: arm64
|
||||
embedded_mpv_build_runtime: true
|
||||
# Linux and Windows
|
||||
- os: linux
|
||||
runner: ubuntu-22.04
|
||||
linux_profile: standard
|
||||
embedded_mpv_platform: linux
|
||||
embedded_mpv_arch: x64
|
||||
embedded_mpv_build_runtime: false
|
||||
- os: linux
|
||||
runner: ubuntu-24.04
|
||||
linux_profile: flatpak
|
||||
embedded_mpv_platform: linux
|
||||
embedded_mpv_arch: x64
|
||||
embedded_mpv_build_runtime: false
|
||||
- os: windows
|
||||
runner: windows-latest
|
||||
embedded_mpv_platform: win32
|
||||
embedded_mpv_arch: x64
|
||||
embedded_mpv_build_runtime: false
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -53,7 +68,7 @@ jobs:
|
||||
if: matrix.os == 'linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --no-install-recommends -y rpm libarchive-tools flatpak flatpak-builder appstream
|
||||
sudo apt-get install --no-install-recommends -y rpm libarchive-tools flatpak flatpak-builder appstream libx11-dev libxext-dev
|
||||
|
||||
# Configure Flatpak
|
||||
# 1. Add the Flathub repository (source of runtimes)
|
||||
@@ -91,84 +106,115 @@ jobs:
|
||||
- name: Resolve embedded MPV runtime cache key
|
||||
# TEMPORARY ARTIFACT TEST: remove `|| github.event_name == 'pull_request' || github.ref == 'refs/heads/master'`
|
||||
# after the macOS Embedded MPV artifacts are built and manually tested.
|
||||
if: matrix.os == 'macos' && !startsWith(github.ref, 'refs/tags/v') && (github.event_name == 'pull_request' || github.ref == 'refs/heads/master')
|
||||
if: matrix.embedded_mpv_platform && !startsWith(github.ref, 'refs/tags/v') && (github.event_name == 'pull_request' || github.ref == 'refs/heads/master')
|
||||
id: embedded-mpv-runtime-cache-key
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
deployment_target="${MACOSX_DEPLOYMENT_TARGET:-11.0}"
|
||||
safe_deployment_target="$(printf '%s' "${deployment_target}" | tr -c 'A-Za-z0-9_.-' '-')"
|
||||
xcode_version="$(xcodebuild -version | tr '\n' ' ' | sed 's/[[:space:]]*$//')"
|
||||
xcode_hash="$(printf '%s' "${xcode_version}" | shasum -a 256 | awk '{print $1}')"
|
||||
# Runtime source pins, source URLs, git tags, and build flags
|
||||
# live in build-macos-runtime.mjs; staging layout and manifest
|
||||
# normalization live in stage-macos-runtime.mjs.
|
||||
source_hash="$(
|
||||
shasum -a 256 \
|
||||
tools/embedded-mpv/build-macos-runtime.mjs \
|
||||
tools/embedded-mpv/stage-macos-runtime.mjs |
|
||||
shasum -a 256 |
|
||||
awk '{print $1}'
|
||||
)"
|
||||
cache_key="embedded-mpv-runtime-v1-${RUNNER_OS}-${{ matrix.arch }}-macos${safe_deployment_target}-xcode${xcode_hash}-${source_hash}"
|
||||
node <<'NODE'
|
||||
const childProcess = require('child_process');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
|
||||
echo "deployment-target=${deployment_target}" >> "${GITHUB_OUTPUT}"
|
||||
echo "xcode-version=${xcode_version}" >> "${GITHUB_OUTPUT}"
|
||||
echo "key=${cache_key}" >> "${GITHUB_OUTPUT}"
|
||||
echo "Embedded MPV runtime cache key: ${cache_key}"
|
||||
echo "Xcode version: ${xcode_version}"
|
||||
const hash = (value) => crypto.createHash('sha256').update(value).digest('hex');
|
||||
const targetPlatform = '${{ matrix.embedded_mpv_platform }}';
|
||||
const targetArch = '${{ matrix.embedded_mpv_arch }}';
|
||||
const sourceHashFiles = [
|
||||
'tools/embedded-mpv/stage-runtime.mjs',
|
||||
];
|
||||
const cacheKeyParts = [
|
||||
'embedded-mpv-runtime-v2',
|
||||
targetPlatform,
|
||||
targetArch,
|
||||
process.env.RUNNER_OS,
|
||||
];
|
||||
let deploymentTarget = '';
|
||||
let xcodeVersion = 'none';
|
||||
if (targetPlatform === 'darwin') {
|
||||
deploymentTarget = process.env.MACOSX_DEPLOYMENT_TARGET || '11.0';
|
||||
const safeDeploymentTarget = deploymentTarget.replace(/[^A-Za-z0-9_.-]/g, '-');
|
||||
sourceHashFiles.push(
|
||||
'tools/embedded-mpv/build-macos-runtime.mjs',
|
||||
'tools/embedded-mpv/stage-macos-runtime.mjs'
|
||||
);
|
||||
try {
|
||||
xcodeVersion = childProcess
|
||||
.execSync('xcodebuild -version', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
|
||||
.replace(/\s+$/g, '')
|
||||
.replace(/\n/g, ' ');
|
||||
} catch {
|
||||
xcodeVersion = 'none';
|
||||
}
|
||||
cacheKeyParts.push(
|
||||
`macos${safeDeploymentTarget}`,
|
||||
`xcode${hash(xcodeVersion)}`
|
||||
);
|
||||
}
|
||||
const sourceHash = hash(
|
||||
sourceHashFiles.map((filePath) => fs.readFileSync(filePath)).join('\n')
|
||||
);
|
||||
cacheKeyParts.push(sourceHash);
|
||||
const cacheKey = cacheKeyParts.join('-');
|
||||
|
||||
fs.appendFileSync(process.env.GITHUB_OUTPUT, `deployment-target=${deploymentTarget}\n`);
|
||||
fs.appendFileSync(process.env.GITHUB_OUTPUT, `xcode-version=${xcodeVersion}\n`);
|
||||
fs.appendFileSync(process.env.GITHUB_OUTPUT, `key=${cacheKey}\n`);
|
||||
console.log(`Embedded MPV runtime cache key: ${cacheKey}`);
|
||||
console.log(`Xcode version: ${xcodeVersion}`);
|
||||
NODE
|
||||
|
||||
- name: Restore embedded MPV runtime cache
|
||||
# TEMPORARY ARTIFACT TEST: remove `|| github.event_name == 'pull_request' || github.ref == 'refs/heads/master'`
|
||||
# after the macOS Embedded MPV artifacts are built and manually tested.
|
||||
if: matrix.os == 'macos' && !startsWith(github.ref, 'refs/tags/v') && (github.event_name == 'pull_request' || github.ref == 'refs/heads/master')
|
||||
if: matrix.embedded_mpv_platform && !startsWith(github.ref, 'refs/tags/v') && (github.event_name == 'pull_request' || github.ref == 'refs/heads/master')
|
||||
id: embedded-mpv-runtime-cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: |
|
||||
vendor/embedded-mpv/darwin-${{ matrix.arch }}/include
|
||||
vendor/embedded-mpv/darwin-${{ matrix.arch }}/lib
|
||||
vendor/embedded-mpv/darwin-${{ matrix.arch }}/runtime-manifest.json
|
||||
vendor/embedded-mpv/${{ matrix.embedded_mpv_platform }}-${{ matrix.embedded_mpv_arch }}/include
|
||||
vendor/embedded-mpv/${{ matrix.embedded_mpv_platform }}-${{ matrix.embedded_mpv_arch }}/lib
|
||||
vendor/embedded-mpv/${{ matrix.embedded_mpv_platform }}-${{ matrix.embedded_mpv_arch }}/runtime-manifest.json
|
||||
key: ${{ steps.embedded-mpv-runtime-cache-key.outputs.key }}
|
||||
|
||||
- name: Clear stale embedded MPV runtime files
|
||||
# TEMPORARY ARTIFACT TEST: remove `|| github.event_name == 'pull_request' || github.ref == 'refs/heads/master'`
|
||||
# after the macOS Embedded MPV artifacts are built and manually tested.
|
||||
if: matrix.os == 'macos' && !startsWith(github.ref, 'refs/tags/v') && (github.event_name == 'pull_request' || github.ref == 'refs/heads/master') && steps.embedded-mpv-runtime-cache.outputs.cache-hit != 'true'
|
||||
if: matrix.embedded_mpv_platform && !startsWith(github.ref, 'refs/tags/v') && (github.event_name == 'pull_request' || github.ref == 'refs/heads/master') && steps.embedded-mpv-runtime-cache.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
rm -rf \
|
||||
"vendor/embedded-mpv/darwin-${{ matrix.arch }}/include" \
|
||||
"vendor/embedded-mpv/darwin-${{ matrix.arch }}/lib" \
|
||||
"vendor/embedded-mpv/darwin-${{ matrix.arch }}/runtime-manifest.json"
|
||||
"vendor/embedded-mpv/${{ matrix.embedded_mpv_platform }}-${{ matrix.embedded_mpv_arch }}/include" \
|
||||
"vendor/embedded-mpv/${{ matrix.embedded_mpv_platform }}-${{ matrix.embedded_mpv_arch }}/lib" \
|
||||
"vendor/embedded-mpv/${{ matrix.embedded_mpv_platform }}-${{ matrix.embedded_mpv_arch }}/runtime-manifest.json"
|
||||
|
||||
- name: Build embedded MPV runtime (macOS release)
|
||||
# TEMPORARY ARTIFACT TEST: remove `|| github.event_name == 'pull_request' || github.ref == 'refs/heads/master'`
|
||||
# after the macOS Embedded MPV artifacts are built and manually tested.
|
||||
if: matrix.os == 'macos' && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'pull_request' || github.ref == 'refs/heads/master') && steps.embedded-mpv-runtime-cache.outputs.cache-hit != 'true'
|
||||
if: matrix.embedded_mpv_build_runtime && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'pull_request' || github.ref == 'refs/heads/master') && steps.embedded-mpv-runtime-cache.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
brew install meson ninja pkg-config nasm autoconf automake libtool xz
|
||||
|
||||
RUNTIME_PREFIX="${RUNNER_TEMP}/embedded-mpv-runtime/${{ matrix.arch }}/prefix"
|
||||
pnpm embedded-mpv:build-runtime -- "${{ matrix.arch }}" "${RUNTIME_PREFIX}"
|
||||
pnpm embedded-mpv:stage-runtime -- "${{ matrix.arch }}" "${RUNTIME_PREFIX}"
|
||||
RUNTIME_PREFIX="${RUNNER_TEMP}/embedded-mpv-runtime/${{ matrix.embedded_mpv_arch }}/prefix"
|
||||
pnpm embedded-mpv:build-runtime -- "${{ matrix.embedded_mpv_arch }}" "${RUNTIME_PREFIX}"
|
||||
pnpm embedded-mpv:stage-runtime -- "${{ matrix.embedded_mpv_platform }}" "${{ matrix.embedded_mpv_arch }}" "${RUNTIME_PREFIX}"
|
||||
|
||||
- name: Build backend
|
||||
env:
|
||||
IPTVNATOR_EMBEDDED_MPV_ARCH: ${{ matrix.arch || '' }}
|
||||
IPTVNATOR_REQUIRE_EMBEDDED_MPV: ${{ matrix.os == 'macos' && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'pull_request' || github.ref == 'refs/heads/master') && '1' || '0' }}
|
||||
IPTVNATOR_EMBEDDED_MPV_PLATFORM: ${{ matrix.embedded_mpv_platform || '' }}
|
||||
IPTVNATOR_EMBEDDED_MPV_ARCH: ${{ matrix.embedded_mpv_arch || matrix.arch || '' }}
|
||||
IPTVNATOR_REQUIRE_EMBEDDED_MPV: ${{ ((matrix.os == 'macos' && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'pull_request' || github.ref == 'refs/heads/master')) || (steps.embedded-mpv-runtime-cache.outputs.cache-hit == 'true')) && '1' || '0' }}
|
||||
run: pnpm run build:backend
|
||||
|
||||
- name: Verify embedded MPV build output
|
||||
# TEMPORARY ARTIFACT TEST: remove `|| github.event_name == 'pull_request' || github.ref == 'refs/heads/master'`
|
||||
# after the macOS Embedded MPV artifacts are built and manually tested.
|
||||
if: matrix.os == 'macos' && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'pull_request' || github.ref == 'refs/heads/master')
|
||||
if: (matrix.os == 'macos' && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'pull_request' || github.ref == 'refs/heads/master')) || steps.embedded-mpv-runtime-cache.outputs.cache-hit == 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -184,7 +230,17 @@ jobs:
|
||||
test -f apps/electron-backend/native/build/Release/embedded_mpv.node
|
||||
test -f dist/apps/electron-backend/native/embedded_mpv.node
|
||||
test -f dist/apps/electron-backend/native/embedded-mpv-runtime.json
|
||||
test -f dist/apps/electron-backend/native/lib/libmpv.2.dylib
|
||||
case "${{ matrix.embedded_mpv_platform }}" in
|
||||
darwin)
|
||||
test -f dist/apps/electron-backend/native/lib/libmpv.2.dylib || test -f dist/apps/electron-backend/native/lib/libmpv.dylib
|
||||
;;
|
||||
win32)
|
||||
test -f dist/apps/electron-backend/native/lib/mpv-2.dll || test -f dist/apps/electron-backend/native/lib/mpv.dll
|
||||
;;
|
||||
linux)
|
||||
test -f dist/apps/electron-backend/native/lib/libmpv.so.2 || test -f dist/apps/electron-backend/native/lib/libmpv.so.1 || test -f dist/apps/electron-backend/native/lib/libmpv.so
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Validate AppStream metadata
|
||||
if: matrix.os == 'linux'
|
||||
@@ -305,7 +361,8 @@ jobs:
|
||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||
DEBUG: electron-builder,electron-notarize*
|
||||
IPTVNATOR_EMBEDDED_MPV_ARCH: ${{ matrix.arch }}
|
||||
IPTVNATOR_EMBEDDED_MPV_PLATFORM: ${{ matrix.embedded_mpv_platform }}
|
||||
IPTVNATOR_EMBEDDED_MPV_ARCH: ${{ matrix.embedded_mpv_arch }}
|
||||
# TEMPORARY MASTER ARTIFACT TEST: remove `|| github.ref == 'refs/heads/master'`
|
||||
# after the macOS Embedded MPV artifacts are built and manually tested.
|
||||
IPTVNATOR_REQUIRE_EMBEDDED_MPV: ${{ (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/master') && '1' || '0' }}
|
||||
@@ -363,10 +420,11 @@ jobs:
|
||||
- name: Make Electron app
|
||||
if: matrix.os != 'macos' || github.event_name == 'pull_request'
|
||||
env:
|
||||
IPTVNATOR_EMBEDDED_MPV_ARCH: ${{ matrix.arch || '' }}
|
||||
IPTVNATOR_EMBEDDED_MPV_PLATFORM: ${{ matrix.embedded_mpv_platform || '' }}
|
||||
IPTVNATOR_EMBEDDED_MPV_ARCH: ${{ matrix.embedded_mpv_arch || matrix.arch || '' }}
|
||||
# TEMPORARY PR TEST: change this back to '0' after manually
|
||||
# testing the macOS PR artifact with Embedded MPV included.
|
||||
IPTVNATOR_REQUIRE_EMBEDDED_MPV: ${{ matrix.os == 'macos' && github.event_name == 'pull_request' && '1' || '0' }}
|
||||
IPTVNATOR_REQUIRE_EMBEDDED_MPV: ${{ ((matrix.os == 'macos' && github.event_name == 'pull_request') || (steps.embedded-mpv-runtime-cache.outputs.cache-hit == 'true')) && '1' || '0' }}
|
||||
run: pnpm run make:app
|
||||
|
||||
- name: Verify packaged worker layout
|
||||
@@ -376,19 +434,19 @@ jobs:
|
||||
PACKAGE_ARCH: ${{ matrix.arch || '' }}
|
||||
# TEMPORARY ARTIFACT TEST: remove `|| github.event_name == 'pull_request' || github.ref == 'refs/heads/master'`
|
||||
# after the macOS Embedded MPV artifacts are built and manually tested.
|
||||
IPTVNATOR_REQUIRE_EMBEDDED_MPV: ${{ matrix.os == 'macos' && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'pull_request' || github.ref == 'refs/heads/master') && '1' || '0' }}
|
||||
IPTVNATOR_REQUIRE_EMBEDDED_MPV: ${{ ((matrix.os == 'macos' && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'pull_request' || github.ref == 'refs/heads/master')) || (steps.embedded-mpv-runtime-cache.outputs.cache-hit == 'true')) && '1' || '0' }}
|
||||
run: pnpm run verify:package-layout -- "$PACKAGE_OS" "$PACKAGE_ARCH"
|
||||
|
||||
- name: Save embedded MPV runtime cache
|
||||
# TEMPORARY ARTIFACT TEST: remove `|| github.ref == 'refs/heads/master'`
|
||||
# after the macOS Embedded MPV artifacts are built and manually tested.
|
||||
if: matrix.os == 'macos' && !startsWith(github.ref, 'refs/tags/v') && github.repository == '4gray/iptvnator' && github.event_name != 'pull_request' && github.ref == 'refs/heads/master' && steps.embedded-mpv-runtime-cache.outputs.cache-hit != 'true'
|
||||
if: matrix.embedded_mpv_build_runtime && !startsWith(github.ref, 'refs/tags/v') && github.repository == '4gray/iptvnator' && github.event_name != 'pull_request' && github.ref == 'refs/heads/master' && steps.embedded-mpv-runtime-cache.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: |
|
||||
vendor/embedded-mpv/darwin-${{ matrix.arch }}/include
|
||||
vendor/embedded-mpv/darwin-${{ matrix.arch }}/lib
|
||||
vendor/embedded-mpv/darwin-${{ matrix.arch }}/runtime-manifest.json
|
||||
vendor/embedded-mpv/${{ matrix.embedded_mpv_platform }}-${{ matrix.embedded_mpv_arch }}/include
|
||||
vendor/embedded-mpv/${{ matrix.embedded_mpv_platform }}-${{ matrix.embedded_mpv_arch }}/lib
|
||||
vendor/embedded-mpv/${{ matrix.embedded_mpv_platform }}-${{ matrix.embedded_mpv_arch }}/runtime-manifest.json
|
||||
key: ${{ steps.embedded-mpv-runtime-cache-key.outputs.key }}
|
||||
|
||||
- name: Smoke test packaged Flatpak launcher
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# Embedded MPV for Windows and Linux
|
||||
|
||||
## Summary
|
||||
|
||||
Add support through three implementation slices:
|
||||
|
||||
1. Foundation: extract the shared libmpv session/IPC core from the macOS-only native source and generalize build/runtime/package contracts without enabling new platforms.
|
||||
2. Windows: add a Windows x64 backend using `HWND` plus mpv `wid`, runtime staging, packaging, and CI verification.
|
||||
3. Linux: add a Linux x64 X11/Xwayland backend using an X11 `Window` plus mpv `wid`, runtime staging, packaging, and CI verification.
|
||||
|
||||
Reuse the current Angular UI, IPC bridge, `EmbeddedMpvSession` contract, Settings support probe, polling/session logic, recording path logic, and E2E infrastructure. The main work is native addon structure, runtime tooling, package verification, and GitHub Actions.
|
||||
|
||||
## Key Changes
|
||||
|
||||
- Split `apps/electron-backend/native/src/embedded_mpv.mm` into shared C++ session/event/command code plus platform backends.
|
||||
- Keep macOS render backend behavior unchanged.
|
||||
- Add Windows x64 and Linux x64 platform backends while preserving existing N-API exports.
|
||||
- Generalize `vendor/embedded-mpv/<platform>-<arch>/` beyond `darwin-*`, adding `win32-x64` and `linux-x64`.
|
||||
- Replace macOS-only build/stage helpers with cross-platform helpers, keeping macOS script names as compatibility wrappers.
|
||||
- Bundle only LGPL-compatible dynamic runtimes with manifests; release builds reject system-package/Homebrew-style runtime origins.
|
||||
- Update `electron-builder.json` to unpack/copy `.node`, `.dylib`, `.dll`, `.so`, `.so.*`, and runtime manifests.
|
||||
- Extend package layout verification for macOS, Windows, and Linux.
|
||||
- Keep existing renderer IPC shape; `EmbeddedMpvSupport.platform` may now report `win32` or `linux`.
|
||||
- Show Embedded MPV in Settings on Windows/Linux only when `getEmbeddedMpvSupport()` reports `supported: true`.
|
||||
- Extend `.github/workflows/build-and-make.yaml` with platform/arch-aware embedded MPV cache keys and runtime build/stage steps.
|
||||
|
||||
## Test Plan
|
||||
|
||||
- `pnpm nx test electron-backend`: platform support matrix, missing runtime reasons, addon candidate paths, recording/session behavior after native split.
|
||||
- `pnpm nx test web`: Settings option visibility for `darwin`, `win32`, `linux`, unsupported arch, and unsupported Wayland.
|
||||
- `pnpm nx test ui-playback`: embedded player capability-driven controls remain stable.
|
||||
- `pnpm nx test packaging`: `electron-builder.json`, `asarUnpack`, runtime manifest, and package layout checks for `.dll`/`.so`.
|
||||
- `pnpm nx run electron-backend:build-embedded-mpv` on macOS, Windows x64, and Linux x64.
|
||||
- `pnpm run build:backend`.
|
||||
- `pnpm run verify:package-layout -- macos <arch>`, `-- windows x64`, and `-- linux x64`.
|
||||
- Extend `electron-backend-e2e:e2e-ci--src/settings.e2e.ts`.
|
||||
- For supported OSes, run Electron with CDP port `9222` and verify with `agent-browser --cdp 9222 tab`, `snapshot -i`, and `screenshot`.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- v1 supports Windows x64 and Linux x64 X11/Xwayland only.
|
||||
- Linux native Wayland support is out of scope for these PRs because Electron and mpv embedding both have platform limitations there.
|
||||
- Existing macOS behavior must not regress.
|
||||
@@ -6,7 +6,7 @@ const {
|
||||
findLibMpv,
|
||||
patchAddonForBundledRuntime,
|
||||
validateNoForbiddenRuntimeLinks,
|
||||
} = require('../../tools/packaging/embedded-mpv-macos.cjs');
|
||||
} = require('../../tools/packaging/embedded-mpv-packaging.cjs');
|
||||
|
||||
const workspaceRoot = process.cwd();
|
||||
const addonRoot = path.join(workspaceRoot, 'apps', 'electron-backend', 'native');
|
||||
@@ -23,6 +23,8 @@ const distNativeDir = path.join(
|
||||
const unavailableMarkerFile = path.join(outputDir, 'embedded-mpv-unavailable.txt');
|
||||
const homebrewIncludeDir = '/opt/homebrew/include';
|
||||
const homebrewLibDir = '/opt/homebrew/lib';
|
||||
const targetPlatform =
|
||||
process.env.IPTVNATOR_EMBEDDED_MPV_PLATFORM || process.platform;
|
||||
const targetArch =
|
||||
process.env.IPTVNATOR_EMBEDDED_MPV_ARCH ||
|
||||
process.env.npm_config_arch ||
|
||||
@@ -31,10 +33,11 @@ const vendoredRuntimeRoot = path.join(
|
||||
workspaceRoot,
|
||||
'vendor',
|
||||
'embedded-mpv',
|
||||
`darwin-${targetArch}`
|
||||
`${targetPlatform}-${targetArch}`
|
||||
);
|
||||
const vendoredIncludeDir = path.join(vendoredRuntimeRoot, 'include');
|
||||
const vendoredLibDir = path.join(vendoredRuntimeRoot, 'lib');
|
||||
const vendoredBinDir = path.join(vendoredRuntimeRoot, 'bin');
|
||||
const homebrewFallbackEnabled =
|
||||
process.env.IPTVNATOR_EMBEDDED_MPV_ALLOW_HOMEBREW === '1';
|
||||
const embeddedMpvRequired = ['1', 'true', 'yes', 'on'].includes(
|
||||
@@ -48,6 +51,9 @@ function log(message) {
|
||||
function cleanOutput() {
|
||||
fs.rmSync(outputFile, { force: true });
|
||||
fs.rmSync(outputLibDir, { recursive: true, force: true });
|
||||
for (const windowsDllName of ['mpv-2.dll', 'mpv.dll']) {
|
||||
fs.rmSync(path.join(outputDir, windowsDllName), { force: true });
|
||||
}
|
||||
fs.rmSync(path.join(outputDir, '.deps'), { recursive: true, force: true });
|
||||
fs.rmSync(path.join(outputDir, 'obj.target'), {
|
||||
recursive: true,
|
||||
@@ -75,20 +81,113 @@ function readRuntimeManifest(runtimeRoot) {
|
||||
return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||
}
|
||||
|
||||
function fileExists(filePath) {
|
||||
return fs.existsSync(filePath) && fs.statSync(filePath).isFile();
|
||||
}
|
||||
|
||||
function listRuntimeFiles(runtimeDir, predicate) {
|
||||
if (!runtimeDir || !fs.existsSync(runtimeDir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return fs
|
||||
.readdirSync(runtimeDir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isFile() || entry.isSymbolicLink())
|
||||
.map((entry) => path.join(runtimeDir, entry.name))
|
||||
.filter((filePath) => {
|
||||
try {
|
||||
return fs.statSync(filePath).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.filter(predicate)
|
||||
.sort();
|
||||
}
|
||||
|
||||
function runtimeFilePredicate(filePath) {
|
||||
const fileName = path.basename(filePath);
|
||||
|
||||
switch (targetPlatform) {
|
||||
case 'darwin':
|
||||
return fileName.endsWith('.dylib');
|
||||
case 'win32':
|
||||
return fileName.endsWith('.dll') || fileName.endsWith('.lib');
|
||||
case 'linux':
|
||||
return /\.so(?:\.\d+)*$/.test(fileName);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function findWindowsImportLib(libDir) {
|
||||
for (const candidate of ['mpv.lib', 'mpv-2.lib', 'libmpv.dll.a']) {
|
||||
const candidatePath = path.join(libDir, candidate);
|
||||
if (fileExists(candidatePath)) {
|
||||
return candidatePath;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function findWindowsLibMpv(runtimeRoot) {
|
||||
for (const candidate of [
|
||||
path.join(runtimeRoot, 'lib', 'mpv-2.dll'),
|
||||
path.join(runtimeRoot, 'bin', 'mpv-2.dll'),
|
||||
path.join(runtimeRoot, 'lib', 'mpv.dll'),
|
||||
path.join(runtimeRoot, 'bin', 'mpv.dll'),
|
||||
]) {
|
||||
if (fileExists(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function findLinuxLibMpv(libDir) {
|
||||
for (const candidate of ['libmpv.so.2', 'libmpv.so.1', 'libmpv.so']) {
|
||||
const candidatePath = path.join(libDir, candidate);
|
||||
if (fileExists(candidatePath)) {
|
||||
return candidatePath;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveRuntime() {
|
||||
const vendoredLibMpv = findLibMpv(vendoredLibDir);
|
||||
const vendoredLibMpv =
|
||||
targetPlatform === 'darwin'
|
||||
? findLibMpv(vendoredLibDir)
|
||||
: targetPlatform === 'win32'
|
||||
? findWindowsLibMpv(vendoredRuntimeRoot)
|
||||
: targetPlatform === 'linux'
|
||||
? findLinuxLibMpv(vendoredLibDir)
|
||||
: null;
|
||||
const vendoredHeader = path.join(vendoredIncludeDir, 'mpv', 'client.h');
|
||||
|
||||
if (vendoredLibMpv && fs.existsSync(vendoredHeader)) {
|
||||
const windowsImportLib =
|
||||
targetPlatform === 'win32'
|
||||
? findWindowsImportLib(vendoredLibDir)
|
||||
: null;
|
||||
if (targetPlatform === 'win32' && !windowsImportLib) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
origin: 'vendored-lgpl',
|
||||
includeDir: vendoredIncludeDir,
|
||||
libDir: vendoredLibDir,
|
||||
binDir: vendoredBinDir,
|
||||
manifest: readRuntimeManifest(vendoredRuntimeRoot),
|
||||
windowsImportLib,
|
||||
};
|
||||
}
|
||||
|
||||
if (homebrewFallbackEnabled) {
|
||||
if (targetPlatform === 'darwin' && homebrewFallbackEnabled) {
|
||||
const homebrewLibMpv = findLibMpv(homebrewLibDir);
|
||||
const homebrewHeader = path.join(homebrewIncludeDir, 'mpv', 'client.h');
|
||||
if (homebrewLibMpv && fs.existsSync(homebrewHeader)) {
|
||||
@@ -99,10 +198,12 @@ function resolveRuntime() {
|
||||
origin: 'homebrew-dev',
|
||||
includeDir: homebrewIncludeDir,
|
||||
libDir: homebrewLibDir,
|
||||
binDir: undefined,
|
||||
manifest: {
|
||||
warning:
|
||||
'Development-only runtime. Do not ship this in release artifacts.',
|
||||
},
|
||||
windowsImportLib: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -110,6 +211,68 @@ function resolveRuntime() {
|
||||
return null;
|
||||
}
|
||||
|
||||
function copyFile(sourcePath, destinationPath) {
|
||||
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
||||
fs.copyFileSync(sourcePath, destinationPath);
|
||||
fs.chmodSync(destinationPath, 0o755);
|
||||
}
|
||||
|
||||
function copyGenericRuntimeToNativeBuild(runtime) {
|
||||
fs.rmSync(outputLibDir, { recursive: true, force: true });
|
||||
fs.mkdirSync(outputLibDir, { recursive: true });
|
||||
|
||||
const runtimeFiles = [
|
||||
...listRuntimeFiles(runtime.libDir, runtimeFilePredicate),
|
||||
...listRuntimeFiles(runtime.binDir, runtimeFilePredicate),
|
||||
];
|
||||
const copiedFiles = new Set();
|
||||
|
||||
for (const runtimeFile of runtimeFiles) {
|
||||
const fileName = path.basename(runtimeFile);
|
||||
if (copiedFiles.has(fileName)) {
|
||||
continue;
|
||||
}
|
||||
copyFile(runtimeFile, path.join(outputLibDir, fileName));
|
||||
copiedFiles.add(fileName);
|
||||
}
|
||||
|
||||
if (targetPlatform === 'win32') {
|
||||
for (const fileName of copiedFiles) {
|
||||
if (fileName.endsWith('.dll')) {
|
||||
copyFile(
|
||||
path.join(outputLibDir, fileName),
|
||||
path.join(outputDir, fileName)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (targetPlatform === 'linux') {
|
||||
const libMpvPath = findLinuxLibMpv(outputLibDir);
|
||||
if (libMpvPath && path.basename(libMpvPath) !== 'libmpv.so') {
|
||||
copyFile(libMpvPath, path.join(outputLibDir, 'libmpv.so'));
|
||||
copiedFiles.add('libmpv.so');
|
||||
}
|
||||
}
|
||||
|
||||
const manifest = {
|
||||
origin: runtime.origin,
|
||||
generatedAt: new Date().toISOString(),
|
||||
libDir: 'lib',
|
||||
runtimeFiles: [...copiedFiles].sort(),
|
||||
...runtime.manifest,
|
||||
platform: targetPlatform,
|
||||
targetArch,
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(outputDir, 'embedded-mpv-runtime.json'),
|
||||
`${JSON.stringify(manifest, null, 2)}\n`
|
||||
);
|
||||
|
||||
return manifest;
|
||||
}
|
||||
|
||||
function resolveElectronNodeGypBin() {
|
||||
const pnpmRoot = path.join(workspaceRoot, 'node_modules', '.pnpm');
|
||||
|
||||
@@ -167,12 +330,16 @@ function main() {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
cleanDistNativeOutput();
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
if (targetPlatform !== process.platform) {
|
||||
cleanOutput();
|
||||
if (embeddedMpvRequired) {
|
||||
throw new Error('Embedded MPV is required but this host is not macOS.');
|
||||
throw new Error(
|
||||
`Embedded MPV is required for ${targetPlatform}-${targetArch}, but this host is ${process.platform}-${process.arch}.`
|
||||
);
|
||||
}
|
||||
log('Skipping build on non-macOS host.');
|
||||
log(
|
||||
`Skipping build for ${targetPlatform}-${targetArch} on ${process.platform}-${process.arch}.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -180,9 +347,11 @@ function main() {
|
||||
if (!runtime) {
|
||||
cleanOutput();
|
||||
const message = [
|
||||
`Skipping build because no embedded MPV runtime was found for darwin-${targetArch}.`,
|
||||
`Skipping build because no embedded MPV runtime was found for ${targetPlatform}-${targetArch}.`,
|
||||
`Expected vendored runtime at ${vendoredRuntimeRoot}.`,
|
||||
'For local development only, set IPTVNATOR_EMBEDDED_MPV_ALLOW_HOMEBREW=1 to use Homebrew libmpv.',
|
||||
targetPlatform === 'darwin'
|
||||
? 'For local development only, set IPTVNATOR_EMBEDDED_MPV_ALLOW_HOMEBREW=1 to use Homebrew libmpv.'
|
||||
: 'Stage a vendored LGPL-compatible runtime before requiring Embedded MPV on this platform.',
|
||||
].join('\n');
|
||||
if (embeddedMpvRequired) {
|
||||
throw new Error(message);
|
||||
@@ -192,15 +361,18 @@ function main() {
|
||||
return;
|
||||
}
|
||||
|
||||
const runtimeManifest = copyRuntimeToNativeBuild({
|
||||
runtimeLibDir: runtime.libDir,
|
||||
outputLibDir,
|
||||
runtimeOrigin: runtime.origin,
|
||||
runtimeManifest: {
|
||||
targetArch,
|
||||
...runtime.manifest,
|
||||
},
|
||||
});
|
||||
const runtimeManifest =
|
||||
targetPlatform === 'darwin'
|
||||
? copyRuntimeToNativeBuild({
|
||||
runtimeLibDir: runtime.libDir,
|
||||
outputLibDir,
|
||||
runtimeOrigin: runtime.origin,
|
||||
runtimeManifest: {
|
||||
targetArch,
|
||||
...runtime.manifest,
|
||||
},
|
||||
})
|
||||
: copyGenericRuntimeToNativeBuild(runtime);
|
||||
fs.rmSync(unavailableMarkerFile, { force: true });
|
||||
|
||||
const electronPackageJson = require(path.join(
|
||||
@@ -220,10 +392,13 @@ function main() {
|
||||
npm_config_update_binary: 'false',
|
||||
LIBMPV_INCLUDE_DIR: runtime.includeDir,
|
||||
LIBMPV_LIBRARY_DIR: outputLibDir,
|
||||
...(runtime.windowsImportLib
|
||||
? { LIBMPV_IMPORT_LIB: path.join(outputLibDir, path.basename(runtime.windowsImportLib)) }
|
||||
: {}),
|
||||
};
|
||||
|
||||
log(
|
||||
`Building native addon against Electron ${electronVersion} using ${runtime.origin} runtime for darwin-${targetArch}...`
|
||||
`Building native addon against Electron ${electronVersion} using ${runtime.origin} runtime for ${targetPlatform}-${targetArch}...`
|
||||
);
|
||||
runNodeGyp('configure', env);
|
||||
runNodeGyp('build', env);
|
||||
@@ -232,13 +407,17 @@ function main() {
|
||||
throw new Error(`Build finished without producing ${outputFile}.`);
|
||||
}
|
||||
|
||||
patchAddonForBundledRuntime(outputFile, outputLibDir);
|
||||
const forbiddenLinkErrors = validateNoForbiddenRuntimeLinks([
|
||||
outputFile,
|
||||
...runtimeManifest.dylibs.map((dylib) => path.join(outputLibDir, dylib)),
|
||||
]);
|
||||
if (runtime.origin === 'vendored-lgpl' && forbiddenLinkErrors.length > 0) {
|
||||
throw new Error(forbiddenLinkErrors.join('\n'));
|
||||
if (targetPlatform === 'darwin') {
|
||||
patchAddonForBundledRuntime(outputFile, outputLibDir);
|
||||
const forbiddenLinkErrors = validateNoForbiddenRuntimeLinks([
|
||||
outputFile,
|
||||
...runtimeManifest.dylibs.map((dylib) =>
|
||||
path.join(outputLibDir, dylib)
|
||||
),
|
||||
]);
|
||||
if (runtime.origin === 'vendored-lgpl' && forbiddenLinkErrors.length > 0) {
|
||||
throw new Error(forbiddenLinkErrors.join('\n'));
|
||||
}
|
||||
}
|
||||
|
||||
log(`Built ${path.relative(workspaceRoot, outputFile)}.`);
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
"targets": [
|
||||
{
|
||||
"target_name": "embedded_mpv",
|
||||
"sources": [
|
||||
"src/embedded_mpv.mm"
|
||||
],
|
||||
"sources": [],
|
||||
"include_dirs": [
|
||||
"<(module_root_dir)/../../../node_modules/.pnpm/node_modules/node-addon-api",
|
||||
"<!(node -p \"process.env.LIBMPV_INCLUDE_DIR || '/opt/homebrew/include'\")"
|
||||
"<!(node -p \"process.env.LIBMPV_INCLUDE_DIR || '/opt/homebrew/include'\")",
|
||||
"src"
|
||||
],
|
||||
"defines": [
|
||||
"NAPI_CPP_EXCEPTIONS"
|
||||
@@ -16,21 +15,65 @@
|
||||
"-fexceptions",
|
||||
"-std=c++20"
|
||||
],
|
||||
"xcode_settings": {
|
||||
"CLANG_CXX_LANGUAGE_STANDARD": "c++20",
|
||||
"CLANG_ENABLE_OBJC_ARC": "YES",
|
||||
"GCC_ENABLE_CPP_EXCEPTIONS": "YES",
|
||||
"MACOSX_DEPLOYMENT_TARGET": "10.15",
|
||||
"OTHER_LDFLAGS": [
|
||||
"-Wl,-rpath,@loader_path/lib"
|
||||
"conditions": [
|
||||
[
|
||||
"OS==\"mac\"",
|
||||
{
|
||||
"sources": [
|
||||
"src/embedded_mpv.mm"
|
||||
],
|
||||
"xcode_settings": {
|
||||
"CLANG_CXX_LANGUAGE_STANDARD": "c++20",
|
||||
"CLANG_ENABLE_OBJC_ARC": "YES",
|
||||
"GCC_ENABLE_CPP_EXCEPTIONS": "YES",
|
||||
"MACOSX_DEPLOYMENT_TARGET": "10.15",
|
||||
"OTHER_LDFLAGS": [
|
||||
"-Wl,-rpath,@loader_path/lib"
|
||||
]
|
||||
},
|
||||
"libraries": [
|
||||
"-framework AppKit",
|
||||
"-framework CoreVideo",
|
||||
"-framework Foundation",
|
||||
"-framework OpenGL",
|
||||
"<!(node -e \"const path = require('path'); const dir = process.env.LIBMPV_LIBRARY_DIR || '/opt/homebrew/lib'; process.stdout.write(path.join(dir, 'libmpv.2.dylib'))\")"
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
"OS==\"win\"",
|
||||
{
|
||||
"sources": [
|
||||
"src/embedded_mpv_win32.cc"
|
||||
],
|
||||
"defines": [
|
||||
"WIN32_LEAN_AND_MEAN",
|
||||
"NOMINMAX"
|
||||
],
|
||||
"libraries": [
|
||||
"<!(node -e \"const path = require('path'); const dir = process.env.LIBMPV_LIBRARY_DIR || process.cwd(); const lib = process.env.LIBMPV_IMPORT_LIB || path.join(dir, 'mpv.lib'); process.stdout.write(lib.replace(/\\\\\\\\/g, '/'))\")",
|
||||
"user32.lib",
|
||||
"gdi32.lib"
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
"OS==\"linux\"",
|
||||
{
|
||||
"sources": [
|
||||
"src/embedded_mpv_linux.cc"
|
||||
],
|
||||
"libraries": [
|
||||
"-L<!(node -p \"process.env.LIBMPV_LIBRARY_DIR || '/usr/lib'\")",
|
||||
"-lmpv",
|
||||
"-lX11",
|
||||
"-lXext"
|
||||
],
|
||||
"ldflags": [
|
||||
"-Wl,-rpath,\\$$ORIGIN/lib"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"libraries": [
|
||||
"-framework AppKit",
|
||||
"-framework CoreVideo",
|
||||
"-framework Foundation",
|
||||
"-framework OpenGL",
|
||||
"<!(node -e \"const path = require('path'); const dir = process.env.LIBMPV_LIBRARY_DIR || '/opt/homebrew/lib'; process.stdout.write(path.join(dir, 'libmpv.2.dylib'))\")"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/extensions/shape.h>
|
||||
|
||||
#include "embedded_mpv_wid_types.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstdint>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
class NativeVideoHost {
|
||||
public:
|
||||
static bool isAvailable()
|
||||
{
|
||||
return std::getenv("DISPLAY") != nullptr;
|
||||
}
|
||||
|
||||
static std::string lastError()
|
||||
{
|
||||
return lastError_;
|
||||
}
|
||||
|
||||
bool create(uintptr_t parentHandle, const Bounds& bounds)
|
||||
{
|
||||
if (!isAvailable()) {
|
||||
lastError_ =
|
||||
"Embedded MPV on Linux requires X11 or Xwayland; DISPLAY is not set.";
|
||||
return false;
|
||||
}
|
||||
|
||||
display_ = XOpenDisplay(nullptr);
|
||||
if (!display_) {
|
||||
lastError_ = "Unable to open the X11 display for embedded MPV.";
|
||||
return false;
|
||||
}
|
||||
|
||||
parentWindow_ = static_cast<Window>(parentHandle);
|
||||
if (!parentWindow_) {
|
||||
lastError_ = "Unable to resolve Electron X11 window handle.";
|
||||
destroy();
|
||||
return false;
|
||||
}
|
||||
|
||||
XSetWindowAttributes attributes{};
|
||||
attributes.background_pixel = BlackPixel(display_, DefaultScreen(display_));
|
||||
attributes.event_mask = ExposureMask | StructureNotifyMask;
|
||||
|
||||
window_ = XCreateWindow(
|
||||
display_,
|
||||
parentWindow_,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
CopyFromParent,
|
||||
InputOutput,
|
||||
CopyFromParent,
|
||||
CWBackPixel | CWEventMask,
|
||||
&attributes
|
||||
);
|
||||
if (!window_) {
|
||||
lastError_ = "Failed to create embedded MPV X11 child window.";
|
||||
destroy();
|
||||
return false;
|
||||
}
|
||||
|
||||
clearInputShape();
|
||||
XMapWindow(display_, window_);
|
||||
setBounds(bounds);
|
||||
XFlush(display_);
|
||||
return true;
|
||||
}
|
||||
|
||||
void setBounds(const Bounds& bounds)
|
||||
{
|
||||
if (!display_ || !window_) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int x = static_cast<int>(std::lround(bounds.x));
|
||||
const int y = static_cast<int>(std::lround(bounds.y));
|
||||
const unsigned int width = static_cast<unsigned int>(
|
||||
std::max(1, static_cast<int>(std::lround(bounds.width)))
|
||||
);
|
||||
const unsigned int height = static_cast<unsigned int>(
|
||||
std::max(1, static_cast<int>(std::lround(bounds.height)))
|
||||
);
|
||||
|
||||
XMoveResizeWindow(display_, window_, x, y, width, height);
|
||||
XRaiseWindow(display_, window_);
|
||||
XFlush(display_);
|
||||
drainEvents();
|
||||
}
|
||||
|
||||
std::string wid() const
|
||||
{
|
||||
std::ostringstream stream;
|
||||
stream << static_cast<unsigned long>(window_);
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
void destroy()
|
||||
{
|
||||
if (display_ && window_) {
|
||||
XDestroyWindow(display_, window_);
|
||||
window_ = 0;
|
||||
}
|
||||
if (display_) {
|
||||
XCloseDisplay(display_);
|
||||
display_ = nullptr;
|
||||
}
|
||||
parentWindow_ = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
void drainEvents()
|
||||
{
|
||||
if (!display_) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (XPending(display_) > 0) {
|
||||
XEvent event{};
|
||||
XNextEvent(display_, &event);
|
||||
}
|
||||
}
|
||||
|
||||
void clearInputShape()
|
||||
{
|
||||
if (!display_ || !window_) {
|
||||
return;
|
||||
}
|
||||
|
||||
int eventBase = 0;
|
||||
int errorBase = 0;
|
||||
if (!XShapeQueryExtension(display_, &eventBase, &errorBase)) {
|
||||
return;
|
||||
}
|
||||
|
||||
XShapeCombineRectangles(
|
||||
display_,
|
||||
window_,
|
||||
ShapeInput,
|
||||
0,
|
||||
0,
|
||||
nullptr,
|
||||
0,
|
||||
ShapeSet,
|
||||
Unsorted
|
||||
);
|
||||
}
|
||||
|
||||
Display* display_ = nullptr;
|
||||
Window parentWindow_ = 0;
|
||||
Window window_ = 0;
|
||||
static inline std::string lastError_ =
|
||||
"Failed to create embedded MPV X11 child window.";
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
#include "embedded_mpv_wid_common.h"
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
struct Bounds {
|
||||
double x = 0.0;
|
||||
double y = 0.0;
|
||||
double width = 1.0;
|
||||
double height = 1.0;
|
||||
};
|
||||
@@ -0,0 +1,203 @@
|
||||
#include <windows.h>
|
||||
|
||||
#include "embedded_mpv_wid_types.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
class NativeVideoHost {
|
||||
public:
|
||||
static bool isAvailable()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
static std::string lastError()
|
||||
{
|
||||
if (!lastErrorMessage_.empty()) {
|
||||
return lastErrorMessage_;
|
||||
}
|
||||
|
||||
return formatLastError(
|
||||
"Failed to create embedded MPV child window."
|
||||
);
|
||||
}
|
||||
|
||||
static std::string formatLastError(const char* fallbackMessage)
|
||||
{
|
||||
DWORD error = GetLastError();
|
||||
if (error == 0) {
|
||||
return fallbackMessage;
|
||||
}
|
||||
|
||||
LPSTR messageBuffer = nullptr;
|
||||
const DWORD length = FormatMessageA(
|
||||
FORMAT_MESSAGE_ALLOCATE_BUFFER |
|
||||
FORMAT_MESSAGE_FROM_SYSTEM |
|
||||
FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
nullptr,
|
||||
error,
|
||||
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
|
||||
reinterpret_cast<LPSTR>(&messageBuffer),
|
||||
0,
|
||||
nullptr
|
||||
);
|
||||
|
||||
std::string message = fallbackMessage;
|
||||
if (length > 0 && messageBuffer) {
|
||||
message += " ";
|
||||
message += messageBuffer;
|
||||
}
|
||||
if (messageBuffer) {
|
||||
LocalFree(messageBuffer);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
bool create(uintptr_t parentHandle, const Bounds& bounds)
|
||||
{
|
||||
lastErrorMessage_.clear();
|
||||
parentWindow_ = reinterpret_cast<HWND>(parentHandle);
|
||||
if (!parentWindow_ || !IsWindow(parentWindow_)) {
|
||||
lastErrorMessage_ =
|
||||
"Unable to resolve Electron native window handle.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!registerWindowClass()) {
|
||||
return false;
|
||||
}
|
||||
window_ = CreateWindowExW(
|
||||
WS_EX_TRANSPARENT,
|
||||
windowClassName(),
|
||||
L"IPTVnator Embedded MPV",
|
||||
WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
parentWindow_,
|
||||
nullptr,
|
||||
GetModuleHandleW(nullptr),
|
||||
nullptr
|
||||
);
|
||||
if (!window_) {
|
||||
lastErrorMessage_ = formatLastError(
|
||||
"Failed to create embedded MPV child window."
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
SetWindowLongPtrW(
|
||||
window_,
|
||||
GWLP_USERDATA,
|
||||
reinterpret_cast<LONG_PTR>(this)
|
||||
);
|
||||
setBounds(bounds);
|
||||
return true;
|
||||
}
|
||||
|
||||
void setBounds(const Bounds& bounds)
|
||||
{
|
||||
if (!window_) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int x = static_cast<int>(std::lround(bounds.x));
|
||||
const int y = static_cast<int>(std::lround(bounds.y));
|
||||
const int width = std::max(1, static_cast<int>(std::lround(bounds.width)));
|
||||
const int height = std::max(1, static_cast<int>(std::lround(bounds.height)));
|
||||
|
||||
SetWindowPos(
|
||||
window_,
|
||||
HWND_TOP,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
SWP_NOACTIVATE | SWP_SHOWWINDOW
|
||||
);
|
||||
}
|
||||
|
||||
std::string wid() const
|
||||
{
|
||||
std::ostringstream stream;
|
||||
stream << reinterpret_cast<uintptr_t>(window_);
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
void destroy()
|
||||
{
|
||||
if (window_) {
|
||||
DestroyWindow(window_);
|
||||
window_ = nullptr;
|
||||
}
|
||||
parentWindow_ = nullptr;
|
||||
}
|
||||
|
||||
private:
|
||||
static const wchar_t* windowClassName()
|
||||
{
|
||||
return L"IPTVnatorEmbeddedMpvHostWindow";
|
||||
}
|
||||
|
||||
static bool registerWindowClass()
|
||||
{
|
||||
static bool registered = false;
|
||||
if (registered) {
|
||||
return true;
|
||||
}
|
||||
|
||||
WNDCLASSEXW windowClass{};
|
||||
windowClass.cbSize = sizeof(windowClass);
|
||||
windowClass.lpfnWndProc = &NativeVideoHost::windowProc;
|
||||
windowClass.hInstance = GetModuleHandleW(nullptr);
|
||||
windowClass.lpszClassName = windowClassName();
|
||||
windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW);
|
||||
windowClass.hbrBackground =
|
||||
reinterpret_cast<HBRUSH>(GetStockObject(BLACK_BRUSH));
|
||||
const ATOM classAtom = RegisterClassExW(&windowClass);
|
||||
if (classAtom == 0) {
|
||||
const DWORD error = GetLastError();
|
||||
if (error != ERROR_CLASS_ALREADY_EXISTS) {
|
||||
lastErrorMessage_ = formatLastError(
|
||||
"Failed to register embedded MPV child window class."
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
registered = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
static LRESULT CALLBACK windowProc(
|
||||
HWND window,
|
||||
UINT message,
|
||||
WPARAM wParam,
|
||||
LPARAM lParam)
|
||||
{
|
||||
if (message == WM_NCHITTEST) {
|
||||
return HTTRANSPARENT;
|
||||
}
|
||||
|
||||
if (message == WM_ERASEBKGND) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return DefWindowProcW(window, message, wParam, lParam);
|
||||
}
|
||||
|
||||
HWND parentWindow_ = nullptr;
|
||||
HWND window_ = nullptr;
|
||||
static inline std::string lastErrorMessage_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
#include "embedded_mpv_wid_common.h"
|
||||
@@ -6,6 +6,29 @@ describe('Embedded MPV native source recording invariants', () => {
|
||||
path.resolve(__dirname, '../../../native/src/embedded_mpv.mm'),
|
||||
'utf8'
|
||||
);
|
||||
const widCommonSource = readFileSync(
|
||||
path.resolve(
|
||||
__dirname,
|
||||
'../../../native/src/embedded_mpv_wid_common.h'
|
||||
),
|
||||
'utf8'
|
||||
);
|
||||
const win32Source = readFileSync(
|
||||
path.resolve(__dirname, '../../../native/src/embedded_mpv_win32.cc'),
|
||||
'utf8'
|
||||
);
|
||||
const linuxSource = readFileSync(
|
||||
path.resolve(__dirname, '../../../native/src/embedded_mpv_linux.cc'),
|
||||
'utf8'
|
||||
);
|
||||
const buildScriptSource = readFileSync(
|
||||
path.resolve(__dirname, '../../../build-embedded-mpv.js'),
|
||||
'utf8'
|
||||
);
|
||||
const buildAndMakeWorkflowSource = readFileSync(
|
||||
path.resolve(__dirname, '../../../../../.github/workflows/build-and-make.yaml'),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
function functionBody(name: string): string {
|
||||
const start = nativeSource.indexOf(`Napi::Value ${name}(`);
|
||||
@@ -67,4 +90,117 @@ describe('Embedded MPV native source recording invariants', () => {
|
||||
expect(endFileCase).toContain('SessionStatus::Ended');
|
||||
expect(endFileCase).toContain('MPV_END_FILE_REASON_EOF');
|
||||
});
|
||||
|
||||
it('keeps Windows/Linux non-load async MPV command failures non-fatal', () => {
|
||||
expect(widCommonSource).toContain('pendingPlaybackLoadRequestId');
|
||||
expect(widCommonSource).toContain('reconcilePlaybackLoadReply');
|
||||
expect(widCommonSource).toContain(
|
||||
'session->snapshot.error = mpv_error_string(event->error);'
|
||||
);
|
||||
expect(widCommonSource).not.toContain(
|
||||
'if (event->error < 0) {\n' +
|
||||
' session->snapshot.status = SessionStatus::Error;'
|
||||
);
|
||||
});
|
||||
|
||||
it('clears transient Windows/Linux MPV operation errors after healthy playback states', () => {
|
||||
const fileLoadedCaseStart = widCommonSource.indexOf(
|
||||
'case MPV_EVENT_FILE_LOADED:'
|
||||
);
|
||||
expect(fileLoadedCaseStart).toBeGreaterThanOrEqual(0);
|
||||
const endFileCaseStart = widCommonSource.indexOf(
|
||||
'case MPV_EVENT_END_FILE:',
|
||||
fileLoadedCaseStart
|
||||
);
|
||||
expect(endFileCaseStart).toBeGreaterThan(fileLoadedCaseStart);
|
||||
const fileLoadedCase = widCommonSource.slice(
|
||||
fileLoadedCaseStart,
|
||||
endFileCaseStart
|
||||
);
|
||||
|
||||
expect(fileLoadedCase).toContain('session->snapshot.error.clear();');
|
||||
|
||||
const pausePropertyStart = widCommonSource.indexOf(
|
||||
'const bool paused = *static_cast<int*>(property->data) != 0;'
|
||||
);
|
||||
expect(pausePropertyStart).toBeGreaterThanOrEqual(0);
|
||||
const volumePropertyStart = widCommonSource.indexOf(
|
||||
'} else if (name == "volume"',
|
||||
pausePropertyStart
|
||||
);
|
||||
expect(volumePropertyStart).toBeGreaterThan(pausePropertyStart);
|
||||
const pausePropertyBranch = widCommonSource.slice(
|
||||
pausePropertyStart,
|
||||
volumePropertyStart
|
||||
);
|
||||
|
||||
expect(pausePropertyBranch).toContain(
|
||||
'session->snapshot.error.clear();'
|
||||
);
|
||||
});
|
||||
|
||||
it('copies Windows runtime DLLs next to the addon for Windows loader lookup', () => {
|
||||
expect(buildScriptSource).toContain(
|
||||
"for (const windowsDllName of ['mpv-2.dll', 'mpv.dll'])"
|
||||
);
|
||||
expect(buildScriptSource).toContain(
|
||||
'path.join(outputDir, windowsDllName)'
|
||||
);
|
||||
expect(buildScriptSource).toContain("fileName.endsWith('.dll')");
|
||||
expect(buildScriptSource).toContain('path.join(outputDir, fileName)');
|
||||
});
|
||||
|
||||
it('checks Win32 window class registration failures explicitly', () => {
|
||||
expect(win32Source).toContain('const ATOM classAtom = RegisterClassExW');
|
||||
expect(win32Source).toContain('ERROR_CLASS_ALREADY_EXISTS');
|
||||
expect(win32Source).toContain(
|
||||
'Failed to register embedded MPV child window class.'
|
||||
);
|
||||
});
|
||||
|
||||
it('drains Linux X11 events after resizing the embedded window', () => {
|
||||
expect(linuxSource).toContain('void drainEvents()');
|
||||
expect(linuxSource).toContain('XPending(display_)');
|
||||
expect(linuxSource).toContain('XNextEvent(display_, &event)');
|
||||
expect(linuxSource).toContain('drainEvents();');
|
||||
});
|
||||
|
||||
it('uses platform-specific embedded MPV runtime cache key inputs in CI', () => {
|
||||
expect(buildAndMakeWorkflowSource).toContain(
|
||||
"const targetPlatform = '${{ matrix.embedded_mpv_platform }}';"
|
||||
);
|
||||
expect(buildAndMakeWorkflowSource).toContain(
|
||||
"if (targetPlatform === 'darwin')"
|
||||
);
|
||||
expect(buildAndMakeWorkflowSource).toContain(
|
||||
"'tools/embedded-mpv/build-macos-runtime.mjs'"
|
||||
);
|
||||
expect(buildAndMakeWorkflowSource).toContain(
|
||||
"'tools/embedded-mpv/stage-runtime.mjs'"
|
||||
);
|
||||
expect(buildAndMakeWorkflowSource).not.toContain(
|
||||
'`macos${safeDeploymentTarget}`,\n' +
|
||||
' `xcode${xcodeHash}`,'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Embedded MPV native build configuration', () => {
|
||||
const bindingGyp = JSON.parse(
|
||||
readFileSync(
|
||||
path.resolve(__dirname, '../../../native/binding.gyp'),
|
||||
'utf8'
|
||||
)
|
||||
);
|
||||
const target = bindingGyp.targets.find(
|
||||
(candidate: { target_name?: string }) =>
|
||||
candidate.target_name === 'embedded_mpv'
|
||||
);
|
||||
|
||||
it('declares platform-specific native sources for macOS, Windows, and Linux', () => {
|
||||
expect(target).toBeDefined();
|
||||
expect(JSON.stringify(target)).toContain('src/embedded_mpv.mm');
|
||||
expect(JSON.stringify(target)).toContain('src/embedded_mpv_win32.cc');
|
||||
expect(JSON.stringify(target)).toContain('src/embedded_mpv_linux.cc');
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import type {
|
||||
EmbeddedMpvSessionStatus,
|
||||
ResolvedPortalPlayback,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'fs';
|
||||
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import path from 'path';
|
||||
import type { EmbeddedMpvNativeService as EmbeddedMpvNativeServiceType } from './embedded-mpv-native.service';
|
||||
@@ -259,6 +259,66 @@ describe('EmbeddedMpvNativeService power blocker', () => {
|
||||
expect(service.getSupport().capabilities?.recording).toBe(true);
|
||||
});
|
||||
|
||||
it.each<NodeJS.Platform>(['darwin', 'win32', 'linux'])(
|
||||
'reports embedded MPV support on %s when the addon is already loaded',
|
||||
(platform) => {
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: platform,
|
||||
});
|
||||
|
||||
expect(service.getSupport()).toEqual(
|
||||
expect.objectContaining({
|
||||
supported: true,
|
||||
platform,
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
it.each([
|
||||
{
|
||||
platform: 'darwin' as NodeJS.Platform,
|
||||
runtimeFile: path.join('lib', 'libmpv.2.dylib'),
|
||||
},
|
||||
{
|
||||
platform: 'win32' as NodeJS.Platform,
|
||||
runtimeFile: path.join('lib', 'mpv-2.dll'),
|
||||
},
|
||||
{
|
||||
platform: 'linux' as NodeJS.Platform,
|
||||
runtimeFile: path.join('lib', 'libmpv.so.2'),
|
||||
},
|
||||
])(
|
||||
'loads the addon after validating the $platform runtime file exists',
|
||||
({ platform, runtimeFile }) => {
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: platform,
|
||||
});
|
||||
const nativeDir = createTempDir();
|
||||
const addonPath = path.join(nativeDir, 'embedded_mpv.node');
|
||||
const runtimePath = path.join(nativeDir, runtimeFile);
|
||||
mkdirSync(path.dirname(runtimePath), { recursive: true });
|
||||
writeFileSync(addonPath, '');
|
||||
writeFileSync(runtimePath, '');
|
||||
const loadAddonModule = jest.fn().mockReturnValue(addon);
|
||||
|
||||
Object.assign(service as unknown as Record<string, unknown>, {
|
||||
addon: null,
|
||||
addonLoadError: null,
|
||||
loadAddonModule,
|
||||
getAddonCandidatePaths: () => [addonPath],
|
||||
});
|
||||
|
||||
expect(service.getSupport()).toEqual(
|
||||
expect.objectContaining({
|
||||
supported: true,
|
||||
platform,
|
||||
})
|
||||
);
|
||||
expect(loadAddonModule).toHaveBeenCalledWith(addonPath);
|
||||
}
|
||||
);
|
||||
|
||||
it('loads the addon before reporting support capabilities', () => {
|
||||
const loadAddonModule = jest.fn().mockReturnValue(addon);
|
||||
Object.assign(service as unknown as Record<string, unknown>, {
|
||||
|
||||
@@ -76,6 +76,11 @@ interface EmbeddedMpvRuntimeSession {
|
||||
}
|
||||
|
||||
const EMBEDDED_MPV_EXPERIMENT_ENV = 'IPTVNATOR_ENABLE_EMBEDDED_MPV_EXPERIMENT';
|
||||
const SUPPORTED_EMBEDDED_MPV_PLATFORMS = new Set<NodeJS.Platform>([
|
||||
'darwin',
|
||||
'win32',
|
||||
'linux',
|
||||
]);
|
||||
|
||||
function dedupePaths(paths: Array<string | undefined>): string[] {
|
||||
return [
|
||||
@@ -105,11 +110,21 @@ export class EmbeddedMpvNativeService {
|
||||
}
|
||||
|
||||
getSupport(): EmbeddedMpvSupport {
|
||||
if (process.platform !== 'darwin') {
|
||||
if (!SUPPORTED_EMBEDDED_MPV_PLATFORMS.has(process.platform)) {
|
||||
return {
|
||||
supported: false,
|
||||
platform: process.platform,
|
||||
reason: 'Embedded MPV is currently available on macOS only.',
|
||||
reason:
|
||||
'Embedded MPV is currently available on macOS, Windows, and Linux only.',
|
||||
};
|
||||
}
|
||||
|
||||
if (this.isUnsupportedLinuxDisplayServer()) {
|
||||
return {
|
||||
supported: false,
|
||||
platform: process.platform,
|
||||
reason:
|
||||
'Embedded MPV on Linux currently requires X11 or Xwayland. Native Wayland embedding is not supported yet.',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -117,7 +132,7 @@ export class EmbeddedMpvNativeService {
|
||||
return {
|
||||
supported: false,
|
||||
platform: process.platform,
|
||||
reason: `Embedded MPV is a macOS-only experimental player. Set ${EMBEDDED_MPV_EXPERIMENT_ENV}=1 to enable it for local development builds.`,
|
||||
reason: `Embedded MPV is an experimental desktop player. Set ${EMBEDDED_MPV_EXPERIMENT_ENV}=1 to enable it for local development builds, or use a packaged build with the bundled runtime.`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -718,11 +733,19 @@ export class EmbeddedMpvNativeService {
|
||||
private assertEmbeddedMpvEnabled(): void {
|
||||
if (!this.isEmbeddedMpvEnabled()) {
|
||||
throw new Error(
|
||||
`Embedded MPV is disabled. Set ${EMBEDDED_MPV_EXPERIMENT_ENV}=1 to enable the local macOS harness, or use a packaged macOS build with the bundled runtime.`
|
||||
`Embedded MPV is disabled. Set ${EMBEDDED_MPV_EXPERIMENT_ENV}=1 to enable the local desktop harness, or use a packaged build with the bundled runtime.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private isUnsupportedLinuxDisplayServer(): boolean {
|
||||
return (
|
||||
process.platform === 'linux' &&
|
||||
Boolean(process.env.WAYLAND_DISPLAY) &&
|
||||
!process.env.DISPLAY
|
||||
);
|
||||
}
|
||||
|
||||
private getAddon(): NativeEmbeddedMpvAddon {
|
||||
if (this.addon) {
|
||||
return this.addon;
|
||||
@@ -836,14 +859,42 @@ export class EmbeddedMpvNativeService {
|
||||
|
||||
private getMissingRuntimeReason(addonPath: string): string | null {
|
||||
const nativeDir = path.dirname(addonPath);
|
||||
const libMpvPath = path.join(nativeDir, 'lib', 'libmpv.2.dylib');
|
||||
const runtimeCandidates = this.getRuntimeLibraryCandidates(nativeDir);
|
||||
|
||||
if (!existsSync(libMpvPath)) {
|
||||
return `Embedded MPV runtime is incomplete. Missing ${libMpvPath}.`;
|
||||
if (!runtimeCandidates.some((candidate) => existsSync(candidate))) {
|
||||
return [
|
||||
'Embedded MPV runtime is incomplete. Missing one of:',
|
||||
...runtimeCandidates.map((candidate) => `- ${candidate}`),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private getRuntimeLibraryCandidates(nativeDir: string): string[] {
|
||||
switch (process.platform) {
|
||||
case 'darwin':
|
||||
return [
|
||||
path.join(nativeDir, 'lib', 'libmpv.2.dylib'),
|
||||
path.join(nativeDir, 'lib', 'libmpv.dylib'),
|
||||
];
|
||||
case 'win32':
|
||||
return [
|
||||
path.join(nativeDir, 'lib', 'mpv-2.dll'),
|
||||
path.join(nativeDir, 'mpv-2.dll'),
|
||||
path.join(nativeDir, 'lib', 'mpv.dll'),
|
||||
path.join(nativeDir, 'mpv.dll'),
|
||||
];
|
||||
case 'linux':
|
||||
return [
|
||||
path.join(nativeDir, 'lib', 'libmpv.so.2'),
|
||||
path.join(nativeDir, 'lib', 'libmpv.so.1'),
|
||||
path.join(nativeDir, 'lib', 'libmpv.so'),
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const embeddedMpvNativeService = new EmbeddedMpvNativeService();
|
||||
@@ -471,24 +471,29 @@ describe('SettingsComponent', () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('shows the embedded mpv option when the desktop support probe reports supported', async () => {
|
||||
window.electron = {
|
||||
...window.electron,
|
||||
getEmbeddedMpvSupport: jest.fn().mockResolvedValue({
|
||||
supported: true,
|
||||
platform: 'darwin',
|
||||
}),
|
||||
} as unknown as typeof window.electron;
|
||||
it.each(['darwin', 'win32', 'linux'] as const)(
|
||||
'shows the embedded mpv option when the %s desktop support probe reports supported',
|
||||
async (platform) => {
|
||||
window.electron = {
|
||||
...window.electron,
|
||||
getEmbeddedMpvSupport: jest.fn().mockResolvedValue({
|
||||
supported: true,
|
||||
platform,
|
||||
}),
|
||||
} as unknown as typeof window.electron;
|
||||
|
||||
await component.ngOnInit();
|
||||
await fixture.whenStable();
|
||||
await component.ngOnInit();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(
|
||||
component
|
||||
.players()
|
||||
.some((player) => player.id === VideoPlayer.EmbeddedMpv)
|
||||
).toBe(true);
|
||||
});
|
||||
expect(
|
||||
component
|
||||
.players()
|
||||
.some(
|
||||
(player) => player.id === VideoPlayer.EmbeddedMpv
|
||||
)
|
||||
).toBe(true);
|
||||
}
|
||||
);
|
||||
|
||||
it('hides external player path settings when the Electron bridge is incomplete', async () => {
|
||||
fixture.destroy();
|
||||
|
||||
@@ -249,7 +249,7 @@
|
||||
"PLAYER_HTML5": "مشغل فيديو HTML5",
|
||||
"PLAYER_VIDEOJS": "مشغل Video.js",
|
||||
"PLAYER_ARTPLAYER": "ArtPlayer",
|
||||
"PLAYER_EMBEDDED_MPV": "MPV المضمّن (تجريبي، macOS)",
|
||||
"PLAYER_EMBEDDED_MPV": "MPV المضمّن (تجريبي)",
|
||||
"PLAYER_MPV": "مشغل MPV",
|
||||
"PLAYER_VLC": "VLC",
|
||||
"VIDEO_PLAYER_PLACEHOLDER": "اختر خيارًا",
|
||||
@@ -296,7 +296,7 @@
|
||||
"VERSION_DESCRIPTION": "الإصدار الحالي للتطبيق",
|
||||
"ABOUT_SUBTITLE": "الإصدار وروابط المشروع وطرق دعم IPTVnator.",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "بيئة تشغيل MPV المضمّنة",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "يمكن أن تتضمن إصدارات macOS مكتبتي libmpv و FFmpeg المرفقتين لمشغل MPV المضمّن التجريبي. لا يحتاج المستخدمون إلى تثبيت تطبيق MPV بشكل منفصل؛ ويبقى MPV و VLC الخارجيان متاحين كمشغلين احتياطيين.",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "يمكن أن تتضمن إصدارات سطح المكتب مكتبتي libmpv و FFmpeg المرفقتين لمشغل MPV المضمّن التجريبي. لا يحتاج المستخدمون إلى تثبيت تطبيق MPV بشكل منفصل؛ ويبقى MPV و VLC الخارجيان متاحين كمشغلين احتياطيين.",
|
||||
"COMMUNITY": "المشروع والمجتمع",
|
||||
"COMMUNITY_DESCRIPTION": "ضع نجمة على المستودع، أو اقرأ المدونة، أو انضم إلى مجموعة Telegram للحصول على التحديثات والمساعدة.",
|
||||
"LINK_BLOG": "المدونة",
|
||||
|
||||
@@ -249,7 +249,7 @@
|
||||
"PLAYER_HTML5": "مشغل فيديو HTML5",
|
||||
"PLAYER_VIDEOJS": "مشغل Video.js",
|
||||
"PLAYER_ARTPLAYER": "ArtPlayer",
|
||||
"PLAYER_EMBEDDED_MPV": "MPV مدمج (تجريبي، macOS)",
|
||||
"PLAYER_EMBEDDED_MPV": "MPV مدمج (تجريبي)",
|
||||
"PLAYER_MPV": "مشغل MPV",
|
||||
"PLAYER_VLC": "VLC",
|
||||
"VIDEO_PLAYER_PLACEHOLDER": "اختار خيار",
|
||||
@@ -296,7 +296,7 @@
|
||||
"VERSION_DESCRIPTION": "الإصدار الحالي ديال التطبيق",
|
||||
"ABOUT_SUBTITLE": "الإصدار، روابط المشروع، وطرق دعم IPTVnator.",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "وقت تشغيل MPV المدمج",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "إصدارات macOS تقدر تتضمن مكتبات libmpv و FFmpeg المدمجة لمشغل MPV المدمج التجريبي. المستخدمين ما محتاجينش يثبتو تطبيق MPV بشكل منفصل؛ MPV الخارجي و VLC مازالو متوفرين كمشغلات احتياطية.",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "إصدارات سطح المكتب تقدر تتضمن مكتبات libmpv و FFmpeg المدمجة لمشغل MPV المدمج التجريبي. المستخدمين ما محتاجينش يثبتو تطبيق MPV بشكل منفصل؛ MPV الخارجي و VLC مازالو متوفرين كمشغلات احتياطية.",
|
||||
"COMMUNITY": "المشروع والمجتمع",
|
||||
"COMMUNITY_DESCRIPTION": "حط نجمة على المستودع، قرا المدونة، ولا انضم لجروب Telegram للتحديثات والمساعدة.",
|
||||
"LINK_BLOG": "المدونة",
|
||||
|
||||
@@ -249,7 +249,7 @@
|
||||
"PLAYER_HTML5": "HTML5 відэапрайгравальнік",
|
||||
"PLAYER_VIDEOJS": "Прайгравальнік Video.js",
|
||||
"PLAYER_ARTPLAYER": "ArtPlayer",
|
||||
"PLAYER_EMBEDDED_MPV": "Убудаваны MPV (эксперыментальна, macOS)",
|
||||
"PLAYER_EMBEDDED_MPV": "Убудаваны MPV (эксперыментальна)",
|
||||
"PLAYER_MPV": "Прайгравальнік MPV",
|
||||
"PLAYER_VLC": "VLC",
|
||||
"VIDEO_PLAYER_PLACEHOLDER": "Выберыце са спісу",
|
||||
@@ -296,7 +296,7 @@
|
||||
"VERSION_DESCRIPTION": "Бягучая версія праграмы",
|
||||
"ABOUT_SUBTITLE": "Версія, спасылкі праекта і спосабы падтрымаць IPTVnator.",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "Асяроддзе ўбудаванага MPV",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "Зборкі для macOS могуць уключаць убудаваныя бібліятэкі libmpv і FFmpeg для эксперыментальнага ўбудаванага прайгравальніка MPV. Карыстальнікам не трэба ўсталёўваць праграму MPV асобна; знешнія MPV і VLC застаюцца даступнымі ў якасці запасных прайгравальнікаў.",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "Зборкі для настольных платформаў могуць уключаць убудаваныя бібліятэкі libmpv і FFmpeg для эксперыментальнага ўбудаванага прайгравальніка MPV. Карыстальнікам не трэба ўсталёўваць праграму MPV асобна; знешнія MPV і VLC застаюцца даступнымі ў якасці запасных прайгравальнікаў.",
|
||||
"COMMUNITY": "Праект і супольнасць",
|
||||
"COMMUNITY_DESCRIPTION": "Пастаўце зорку рэпазіторыю, чытайце блог або далучайцеся да Telegram-групы для абнаўленняў і дапамогі.",
|
||||
"LINK_BLOG": "Блог",
|
||||
|
||||
@@ -249,7 +249,7 @@
|
||||
"PLAYER_HTML5": "HTML5-Videoplayer",
|
||||
"PLAYER_VIDEOJS": "Video.js-Player",
|
||||
"PLAYER_ARTPLAYER": "ArtPlayer",
|
||||
"PLAYER_EMBEDDED_MPV": "Eingebettetes MPV (experimentell, macOS)",
|
||||
"PLAYER_EMBEDDED_MPV": "Eingebettetes MPV (experimentell)",
|
||||
"PLAYER_MPV": "MPV-Player",
|
||||
"PLAYER_VLC": "VLC",
|
||||
"VIDEO_PLAYER_PLACEHOLDER": "Option auswählen",
|
||||
@@ -296,7 +296,7 @@
|
||||
"VERSION_DESCRIPTION": "Aktuelle Version der Anwendung",
|
||||
"ABOUT_SUBTITLE": "Version, Projekt-Links und Möglichkeiten, IPTVnator zu unterstützen.",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "Eingebettete MPV-Laufzeit",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "macOS-Builds können gebündelte libmpv- und FFmpeg-Bibliotheken für den experimentellen eingebetteten MPV-Player enthalten. Eine separate Installation der MPV-App ist nicht erforderlich; externes MPV und VLC bleiben weiterhin als Fallback-Player verfügbar.",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "Desktop-Builds können gebündelte libmpv- und FFmpeg-Bibliotheken für den experimentellen eingebetteten MPV-Player enthalten. Eine separate Installation der MPV-App ist nicht erforderlich; externes MPV und VLC bleiben weiterhin als Fallback-Player verfügbar.",
|
||||
"COMMUNITY": "Projekt & Community",
|
||||
"COMMUNITY_DESCRIPTION": "Gib dem Repository einen Stern, lies den Blog oder tritt der Telegram-Gruppe für Updates und Hilfe bei.",
|
||||
"LINK_BLOG": "Blog",
|
||||
|
||||
@@ -249,7 +249,7 @@
|
||||
"PLAYER_HTML5": "Πρόγραμμα αναπαραγωγής HTML5",
|
||||
"PLAYER_VIDEOJS": "Πρόγραμμα αναπαραγωγής Video.js",
|
||||
"PLAYER_ARTPLAYER": "ArtPlayer",
|
||||
"PLAYER_EMBEDDED_MPV": "Ενσωματωμένο MPV (πειραματικό, macOS)",
|
||||
"PLAYER_EMBEDDED_MPV": "Ενσωματωμένο MPV (πειραματικό)",
|
||||
"PLAYER_MPV": "MPV player",
|
||||
"PLAYER_VLC": "VLC",
|
||||
"VIDEO_PLAYER_PLACEHOLDER": "Επιλέξτε μια επιλογή",
|
||||
@@ -296,7 +296,7 @@
|
||||
"VERSION_DESCRIPTION": "Τρέχουσα έκδοση της εφαρμογής",
|
||||
"ABOUT_SUBTITLE": "Έκδοση, σύνδεσμοι έργου και τρόποι υποστήριξης του IPTVnator.",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "Ενσωματωμένος χρόνος εκτέλεσης MPV",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "Οι εκδόσεις macOS μπορούν να περιλαμβάνουν τις ενσωματωμένες βιβλιοθήκες libmpv και FFmpeg για το πειραματικό ενσωματωμένο πρόγραμμα αναπαραγωγής MPV. Οι χρήστες δεν χρειάζεται να εγκαταστήσουν ξεχωριστά την εφαρμογή MPV· τα εξωτερικά MPV και VLC παραμένουν διαθέσιμα ως εναλλακτικά προγράμματα αναπαραγωγής.",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "Οι εκδόσεις desktop μπορούν να περιλαμβάνουν τις ενσωματωμένες βιβλιοθήκες libmpv και FFmpeg για το πειραματικό ενσωματωμένο πρόγραμμα αναπαραγωγής MPV. Οι χρήστες δεν χρειάζεται να εγκαταστήσουν ξεχωριστά την εφαρμογή MPV· τα εξωτερικά MPV και VLC παραμένουν διαθέσιμα ως εναλλακτικά προγράμματα αναπαραγωγής.",
|
||||
"COMMUNITY": "Έργο και κοινότητα",
|
||||
"COMMUNITY_DESCRIPTION": "Δώστε αστέρι στο αποθετήριο, διαβάστε το blog ή συμμετέχετε στην ομάδα Telegram για ενημερώσεις και βοήθεια.",
|
||||
"LINK_BLOG": "Blog",
|
||||
|
||||
@@ -249,7 +249,7 @@
|
||||
"PLAYER_HTML5": "HTML5 video player",
|
||||
"PLAYER_VIDEOJS": "Video.js player",
|
||||
"PLAYER_ARTPLAYER": "ArtPlayer",
|
||||
"PLAYER_EMBEDDED_MPV": "Embedded MPV (Experimental, macOS)",
|
||||
"PLAYER_EMBEDDED_MPV": "Embedded MPV (Experimental)",
|
||||
"PLAYER_MPV": "MPV player",
|
||||
"PLAYER_VLC": "VLC",
|
||||
"VIDEO_PLAYER_PLACEHOLDER": "Select an option",
|
||||
@@ -296,7 +296,7 @@
|
||||
"VERSION_DESCRIPTION": "Current version of the application",
|
||||
"ABOUT_SUBTITLE": "Version, project links and ways to support IPTVnator.",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "Embedded MPV runtime",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "macOS builds can include bundled libmpv and FFmpeg libraries for the experimental Embedded MPV player. Users do not need to install the MPV app separately; external MPV and VLC remain available as fallback players.",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "Desktop builds can include bundled libmpv and FFmpeg libraries for the experimental Embedded MPV player. Users do not need to install the MPV app separately; external MPV and VLC remain available as fallback players.",
|
||||
"COMMUNITY": "Project & community",
|
||||
"COMMUNITY_DESCRIPTION": "Star the repository, read the blog or join the Telegram group for updates and help.",
|
||||
"LINK_BLOG": "Blog",
|
||||
|
||||
@@ -249,7 +249,7 @@
|
||||
"PLAYER_HTML5": "Reproductor HTML5",
|
||||
"PLAYER_VIDEOJS": "Reproductor Video.js",
|
||||
"PLAYER_ARTPLAYER": "ArtPlayer",
|
||||
"PLAYER_EMBEDDED_MPV": "MPV integrado (experimental, macOS)",
|
||||
"PLAYER_EMBEDDED_MPV": "MPV integrado (experimental)",
|
||||
"PLAYER_MPV": "Reproductor MPV",
|
||||
"PLAYER_VLC": "VLC",
|
||||
"VIDEO_PLAYER_PLACEHOLDER": "Selecciona una opción",
|
||||
@@ -296,7 +296,7 @@
|
||||
"VERSION_DESCRIPTION": "Versión actual de la aplicación.",
|
||||
"ABOUT_SUBTITLE": "Versión, enlaces del proyecto y formas de apoyar a IPTVnator.",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "Runtime de MPV integrado",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "Las compilaciones de macOS pueden incluir las bibliotecas libmpv y FFmpeg integradas para el reproductor MPV integrado experimental. Los usuarios no necesitan instalar la app MPV por separado; MPV externo y VLC siguen disponibles como reproductores alternativos.",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "Las compilaciones de escritorio pueden incluir las bibliotecas libmpv y FFmpeg integradas para el reproductor MPV integrado experimental. Los usuarios no necesitan instalar la app MPV por separado; MPV externo y VLC siguen disponibles como reproductores alternativos.",
|
||||
"COMMUNITY": "Proyecto y comunidad",
|
||||
"COMMUNITY_DESCRIPTION": "Da una estrella al repositorio, lee el blog o únete al grupo de Telegram para novedades y ayuda.",
|
||||
"LINK_BLOG": "Blog",
|
||||
|
||||
@@ -249,7 +249,7 @@
|
||||
"PLAYER_HTML5": "Lecteur vidéo HTML5",
|
||||
"PLAYER_VIDEOJS": "Lecteur Video.js",
|
||||
"PLAYER_ARTPLAYER": "ArtPlayer",
|
||||
"PLAYER_EMBEDDED_MPV": "MPV intégré (expérimental, macOS)",
|
||||
"PLAYER_EMBEDDED_MPV": "MPV intégré (expérimental)",
|
||||
"PLAYER_MPV": "Lecteur MPV",
|
||||
"PLAYER_VLC": "VLC",
|
||||
"VIDEO_PLAYER_PLACEHOLDER": "Choisir une option",
|
||||
@@ -296,7 +296,7 @@
|
||||
"VERSION_DESCRIPTION": "Version actuelle de l'application",
|
||||
"ABOUT_SUBTITLE": "Version, liens du projet et façons de soutenir IPTVnator.",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "Runtime MPV intégré",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "Les versions macOS peuvent inclure les bibliothèques libmpv et FFmpeg empaquetées pour le lecteur MPV intégré expérimental. Les utilisateurs n'ont pas besoin d'installer l'application MPV séparément ; les lecteurs externes MPV et VLC restent disponibles comme lecteurs de secours.",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "Les versions desktop peuvent inclure les bibliothèques libmpv et FFmpeg empaquetées pour le lecteur MPV intégré expérimental. Les utilisateurs n'ont pas besoin d'installer l'application MPV séparément ; les lecteurs externes MPV et VLC restent disponibles comme lecteurs de secours.",
|
||||
"COMMUNITY": "Projet et communauté",
|
||||
"COMMUNITY_DESCRIPTION": "Mettez une étoile au dépôt, lisez le blog ou rejoignez le groupe Telegram pour les actualités et l'aide.",
|
||||
"LINK_BLOG": "Blog",
|
||||
|
||||
@@ -249,7 +249,7 @@
|
||||
"PLAYER_HTML5": "Lettore HTML5",
|
||||
"PLAYER_VIDEOJS": "Lettore Video.js",
|
||||
"PLAYER_ARTPLAYER": "ArtPlayer",
|
||||
"PLAYER_EMBEDDED_MPV": "MPV integrato (sperimentale, macOS)",
|
||||
"PLAYER_EMBEDDED_MPV": "MPV integrato (sperimentale)",
|
||||
"PLAYER_MPV": "Lettore MPV",
|
||||
"PLAYER_VLC": "VLC",
|
||||
"VIDEO_PLAYER_PLACEHOLDER": "Seleziona una opzione",
|
||||
@@ -296,7 +296,7 @@
|
||||
"VERSION_DESCRIPTION": "Versione attuale dell'applicazione",
|
||||
"ABOUT_SUBTITLE": "Versione, link al progetto e modi per supportare IPTVnator.",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "Runtime MPV integrato",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "Le build per macOS possono includere le librerie libmpv e FFmpeg in bundle per il lettore MPV integrato sperimentale. Gli utenti non devono installare l'app MPV separatamente; MPV e VLC esterni restano disponibili come lettori di riserva.",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "Le build desktop possono includere le librerie libmpv e FFmpeg in bundle per il lettore MPV integrato sperimentale. Gli utenti non devono installare l'app MPV separatamente; MPV e VLC esterni restano disponibili come lettori di riserva.",
|
||||
"COMMUNITY": "Progetto e community",
|
||||
"COMMUNITY_DESCRIPTION": "Metti una stella al repository, leggi il blog o unisciti al gruppo Telegram per aggiornamenti e supporto.",
|
||||
"LINK_BLOG": "Blog",
|
||||
|
||||
@@ -249,7 +249,7 @@
|
||||
"PLAYER_HTML5": "HTML5ビデオプレーヤー",
|
||||
"PLAYER_VIDEOJS": "Video.jsプレーヤー",
|
||||
"PLAYER_ARTPLAYER": "ArtPlayer",
|
||||
"PLAYER_EMBEDDED_MPV": "組み込みMPV(実験的、macOS)",
|
||||
"PLAYER_EMBEDDED_MPV": "組み込みMPV(実験的)",
|
||||
"PLAYER_MPV": "MPVプレーヤー",
|
||||
"PLAYER_VLC": "VLC",
|
||||
"VIDEO_PLAYER_PLACEHOLDER": "オプションを選択",
|
||||
@@ -296,7 +296,7 @@
|
||||
"VERSION_DESCRIPTION": "アプリケーションの現在のバージョン",
|
||||
"ABOUT_SUBTITLE": "バージョン、プロジェクトのリンク、IPTVnatorをサポートする方法。",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "組み込みMPVランタイム",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "macOS版のビルドには、実験的な組み込みMPVプレーヤー用のlibmpvおよびFFmpegライブラリを同梱できます。MPVアプリを別途インストールする必要はありません。フォールバックとして外部MPVおよびVLCも引き続き利用できます。",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "デスクトップ版のビルドには、実験的な組み込みMPVプレーヤー用のlibmpvおよびFFmpegライブラリを同梱できます。MPVアプリを別途インストールする必要はありません。フォールバックとして外部MPVおよびVLCも引き続き利用できます。",
|
||||
"COMMUNITY": "プロジェクトとコミュニティ",
|
||||
"COMMUNITY_DESCRIPTION": "リポジトリにスターを付けたり、ブログを読んだり、Telegramグループに参加して最新情報やサポートを受け取れます。",
|
||||
"LINK_BLOG": "ブログ",
|
||||
|
||||
@@ -249,7 +249,7 @@
|
||||
"PLAYER_HTML5": "HTML5 비디오 플레이어",
|
||||
"PLAYER_VIDEOJS": "Video.js 플레이어",
|
||||
"PLAYER_ARTPLAYER": "ArtPlayer",
|
||||
"PLAYER_EMBEDDED_MPV": "내장 MPV (실험적, macOS)",
|
||||
"PLAYER_EMBEDDED_MPV": "내장 MPV (실험적)",
|
||||
"PLAYER_MPV": "MPV 플레이어",
|
||||
"PLAYER_VLC": "VLC",
|
||||
"VIDEO_PLAYER_PLACEHOLDER": "옵션을 선택하세요",
|
||||
@@ -296,7 +296,7 @@
|
||||
"VERSION_DESCRIPTION": "애플리케이션의 현재 버전",
|
||||
"ABOUT_SUBTITLE": "버전, 프로젝트 링크 및 IPTVnator를 후원하는 방법.",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "내장 MPV 런타임",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "macOS 빌드에는 실험적인 내장 MPV 플레이어를 위해 번들된 libmpv 및 FFmpeg 라이브러리가 포함될 수 있습니다. 사용자는 MPV 앱을 별도로 설치할 필요가 없으며, 외부 MPV와 VLC는 폴백 플레이어로 계속 사용할 수 있습니다.",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "데스크톱 빌드에는 실험적인 내장 MPV 플레이어를 위해 번들된 libmpv 및 FFmpeg 라이브러리가 포함될 수 있습니다. 사용자는 MPV 앱을 별도로 설치할 필요가 없으며, 외부 MPV와 VLC는 폴백 플레이어로 계속 사용할 수 있습니다.",
|
||||
"COMMUNITY": "프로젝트 및 커뮤니티",
|
||||
"COMMUNITY_DESCRIPTION": "저장소에 별표를 누르거나, 블로그를 읽거나, Telegram 그룹에 참여하여 업데이트와 도움을 받으세요.",
|
||||
"LINK_BLOG": "블로그",
|
||||
|
||||
@@ -249,7 +249,7 @@
|
||||
"PLAYER_HTML5": "HTML5-videospeler",
|
||||
"PLAYER_VIDEOJS": "Video.js-speler",
|
||||
"PLAYER_ARTPLAYER": "ArtPlayer",
|
||||
"PLAYER_EMBEDDED_MPV": "Ingebedde MPV (experimenteel, macOS)",
|
||||
"PLAYER_EMBEDDED_MPV": "Ingebedde MPV (experimenteel)",
|
||||
"PLAYER_MPV": "MPV-speler",
|
||||
"PLAYER_VLC": "VLC",
|
||||
"VIDEO_PLAYER_PLACEHOLDER": "Selecteer een optie",
|
||||
@@ -296,7 +296,7 @@
|
||||
"VERSION_DESCRIPTION": "Huidige versie van de applicatie",
|
||||
"ABOUT_SUBTITLE": "Versie, projectlinks en manieren om IPTVnator te ondersteunen.",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "Ingebedde MPV-runtime",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "macOS-builds kunnen meegeleverde libmpv- en FFmpeg-bibliotheken bevatten voor de experimentele Embedded MPV-speler. Gebruikers hoeven de MPV-app niet apart te installeren; externe MPV en VLC blijven beschikbaar als terugvalspelers.",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "Desktop-builds kunnen meegeleverde libmpv- en FFmpeg-bibliotheken bevatten voor de experimentele Embedded MPV-speler. Gebruikers hoeven de MPV-app niet apart te installeren; externe MPV en VLC blijven beschikbaar als terugvalspelers.",
|
||||
"COMMUNITY": "Project & community",
|
||||
"COMMUNITY_DESCRIPTION": "Geef de repository een ster, lees de blog of word lid van de Telegram-groep voor updates en hulp.",
|
||||
"LINK_BLOG": "Blog",
|
||||
|
||||
@@ -249,7 +249,7 @@
|
||||
"PLAYER_HTML5": "Odtwarzacz wideo HTML5",
|
||||
"PLAYER_VIDEOJS": "Odtwarzacz Video.js",
|
||||
"PLAYER_ARTPLAYER": "ArtPlayer",
|
||||
"PLAYER_EMBEDDED_MPV": "Wbudowany MPV (eksperymentalny, macOS)",
|
||||
"PLAYER_EMBEDDED_MPV": "Wbudowany MPV (eksperymentalny)",
|
||||
"PLAYER_MPV": "Odtwarzacz MPV",
|
||||
"PLAYER_VLC": "VLC",
|
||||
"VIDEO_PLAYER_PLACEHOLDER": "Wybierz opcję",
|
||||
@@ -296,7 +296,7 @@
|
||||
"VERSION_DESCRIPTION": "Aktualna wersja aplikacji",
|
||||
"ABOUT_SUBTITLE": "Wersja, linki do projektu i sposoby wsparcia IPTVnator.",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "Środowisko wbudowanego MPV",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "Wersje dla macOS mogą zawierać dołączone biblioteki libmpv i FFmpeg dla eksperymentalnego wbudowanego odtwarzacza MPV. Użytkownicy nie muszą oddzielnie instalować aplikacji MPV; zewnętrzne MPV i VLC pozostają dostępne jako odtwarzacze zapasowe.",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "Wersje desktopowe mogą zawierać dołączone biblioteki libmpv i FFmpeg dla eksperymentalnego wbudowanego odtwarzacza MPV. Użytkownicy nie muszą oddzielnie instalować aplikacji MPV; zewnętrzne MPV i VLC pozostają dostępne jako odtwarzacze zapasowe.",
|
||||
"COMMUNITY": "Projekt i społeczność",
|
||||
"COMMUNITY_DESCRIPTION": "Daj gwiazdkę repozytorium, czytaj bloga lub dołącz do grupy Telegram, aby otrzymywać aktualizacje i pomoc.",
|
||||
"LINK_BLOG": "Blog",
|
||||
|
||||
@@ -249,7 +249,7 @@
|
||||
"PLAYER_HTML5": "Reprodutor de vídeo HTML5",
|
||||
"PLAYER_VIDEOJS": "Reprodutor Video.js",
|
||||
"PLAYER_ARTPLAYER": "ArtPlayer",
|
||||
"PLAYER_EMBEDDED_MPV": "MPV integrado (experimental, macOS)",
|
||||
"PLAYER_EMBEDDED_MPV": "MPV integrado (experimental)",
|
||||
"PLAYER_MPV": "Reprodutor MPV",
|
||||
"PLAYER_VLC": "VLC",
|
||||
"VIDEO_PLAYER_PLACEHOLDER": "Selecione uma opção",
|
||||
@@ -296,7 +296,7 @@
|
||||
"VERSION_DESCRIPTION": "Versão atual do aplicativo",
|
||||
"ABOUT_SUBTITLE": "Versão, links do projeto e formas de apoiar o IPTVnator.",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "Runtime MPV integrado",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "Builds para macOS podem incluir as bibliotecas libmpv e FFmpeg empacotadas para o reprodutor MPV integrado experimental. Os usuários não precisam instalar o aplicativo MPV separadamente; MPV externo e VLC permanecem disponíveis como reprodutores alternativos.",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "Builds desktop podem incluir as bibliotecas libmpv e FFmpeg empacotadas para o reprodutor MPV integrado experimental. Os usuários não precisam instalar o aplicativo MPV separadamente; MPV externo e VLC permanecem disponíveis como reprodutores alternativos.",
|
||||
"COMMUNITY": "Projeto e comunidade",
|
||||
"COMMUNITY_DESCRIPTION": "Marque o repositório com estrela, leia o blog ou junte-se ao grupo do Telegram para atualizações e ajuda.",
|
||||
"LINK_BLOG": "Blog",
|
||||
|
||||
@@ -249,7 +249,7 @@
|
||||
"PLAYER_HTML5": "HTML5-плеер",
|
||||
"PLAYER_VIDEOJS": "Video.js-плеер",
|
||||
"PLAYER_ARTPLAYER": "ArtPlayer",
|
||||
"PLAYER_EMBEDDED_MPV": "Встроенный MPV (экспериментальный, macOS)",
|
||||
"PLAYER_EMBEDDED_MPV": "Встроенный MPV (экспериментальный)",
|
||||
"PLAYER_MPV": "MPV-плеер",
|
||||
"PLAYER_VLC": "VLC",
|
||||
"VIDEO_PLAYER_PLACEHOLDER": "Выберите из списка",
|
||||
@@ -296,7 +296,7 @@
|
||||
"VERSION_DESCRIPTION": "Текущая версия приложения",
|
||||
"ABOUT_SUBTITLE": "Версия, ссылки на проект и способы поддержать IPTVnator.",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "Среда выполнения встроенного MPV",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "Сборки для macOS могут включать в себя библиотеки libmpv и FFmpeg для экспериментального встроенного MPV-проигрывателя. Установка отдельного приложения MPV не требуется; внешние MPV и VLC по-прежнему доступны как запасные проигрыватели.",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "Сборки для настольных платформ могут включать библиотеки libmpv и FFmpeg для экспериментального встроенного MPV-проигрывателя. Установка отдельного приложения MPV не требуется; внешние MPV и VLC по-прежнему доступны как запасные проигрыватели.",
|
||||
"COMMUNITY": "Проект и сообщество",
|
||||
"COMMUNITY_DESCRIPTION": "Поставьте звезду репозиторию, читайте блог или присоединяйтесь к Telegram-группе для новостей и поддержки.",
|
||||
"LINK_BLOG": "Блог",
|
||||
|
||||
@@ -249,7 +249,7 @@
|
||||
"PLAYER_HTML5": "HTML5 video oynatıcı",
|
||||
"PLAYER_VIDEOJS": "Video.js oynatıcı",
|
||||
"PLAYER_ARTPLAYER": "ArtPlayer",
|
||||
"PLAYER_EMBEDDED_MPV": "Gömülü MPV (Deneysel, macOS)",
|
||||
"PLAYER_EMBEDDED_MPV": "Gömülü MPV (Deneysel)",
|
||||
"PLAYER_MPV": "MPV oynatıcı",
|
||||
"PLAYER_VLC": "VLC",
|
||||
"VIDEO_PLAYER_PLACEHOLDER": "Bir seçenek belirleyin",
|
||||
@@ -296,7 +296,7 @@
|
||||
"VERSION_DESCRIPTION": "Uygulamanın geçerli sürümü",
|
||||
"ABOUT_SUBTITLE": "Sürüm, proje bağlantıları ve IPTVnator'u desteklemenin yolları.",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "Gömülü MPV çalışma zamanı",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "macOS sürümleri, deneysel Gömülü MPV oynatıcı için paketlenmiş libmpv ve FFmpeg kitaplıklarını içerebilir. Kullanıcıların MPV uygulamasını ayrıca kurmasına gerek yoktur; harici MPV ve VLC, yedek oynatıcı olarak kullanılabilir kalır.",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "Masaüstü sürümleri, deneysel Gömülü MPV oynatıcı için paketlenmiş libmpv ve FFmpeg kitaplıklarını içerebilir. Kullanıcıların MPV uygulamasını ayrıca kurmasına gerek yoktur; harici MPV ve VLC, yedek oynatıcı olarak kullanılabilir kalır.",
|
||||
"COMMUNITY": "Proje ve topluluk",
|
||||
"COMMUNITY_DESCRIPTION": "Depoyu yıldızlayın, blogu okuyun veya güncellemeler ve yardım için Telegram grubuna katılın.",
|
||||
"LINK_BLOG": "Blog",
|
||||
|
||||
@@ -249,7 +249,7 @@
|
||||
"PLAYER_HTML5": "HTML5 视频播放器",
|
||||
"PLAYER_VIDEOJS": "Video.js 播放器",
|
||||
"PLAYER_ARTPLAYER": "ArtPlayer",
|
||||
"PLAYER_EMBEDDED_MPV": "嵌入式 MPV(实验性,macOS)",
|
||||
"PLAYER_EMBEDDED_MPV": "嵌入式 MPV(实验性)",
|
||||
"PLAYER_MPV": "MPV 播放器",
|
||||
"PLAYER_VLC": "VLC",
|
||||
"VIDEO_PLAYER_PLACEHOLDER": "选择一个选项",
|
||||
@@ -296,7 +296,7 @@
|
||||
"VERSION_DESCRIPTION": "应用程序的当前版本",
|
||||
"ABOUT_SUBTITLE": "版本、项目链接以及支持 IPTVnator 的方式。",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "嵌入式 MPV 运行时",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "macOS 构建版本可包含捆绑的 libmpv 和 FFmpeg 库,用于实验性的嵌入式 MPV 播放器。用户无需单独安装 MPV 应用;外部 MPV 和 VLC 仍可作为备用播放器使用。",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "桌面构建版本可包含捆绑的 libmpv 和 FFmpeg 库,用于实验性的嵌入式 MPV 播放器。用户无需单独安装 MPV 应用;外部 MPV 和 VLC 仍可作为备用播放器使用。",
|
||||
"COMMUNITY": "项目与社区",
|
||||
"COMMUNITY_DESCRIPTION": "为仓库点星、阅读博客或加入 Telegram 群组,以获取更新和帮助。",
|
||||
"LINK_BLOG": "博客",
|
||||
|
||||
@@ -249,7 +249,7 @@
|
||||
"PLAYER_HTML5": "HTML5 影片播放器",
|
||||
"PLAYER_VIDEOJS": "Video.js 播放器",
|
||||
"PLAYER_ARTPLAYER": "ArtPlayer",
|
||||
"PLAYER_EMBEDDED_MPV": "內嵌 MPV(實驗性,macOS)",
|
||||
"PLAYER_EMBEDDED_MPV": "內嵌 MPV(實驗性)",
|
||||
"PLAYER_MPV": "MPV 播放器",
|
||||
"PLAYER_VLC": "VLC",
|
||||
"VIDEO_PLAYER_PLACEHOLDER": "選擇一個選項",
|
||||
@@ -296,7 +296,7 @@
|
||||
"VERSION_DESCRIPTION": "應用程式目前版本",
|
||||
"ABOUT_SUBTITLE": "版本、專案連結,以及支持 IPTVnator 的方式。",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "內嵌 MPV 執行環境",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "macOS 版本可內含 libmpv 和 FFmpeg 程式庫,用於實驗性的內嵌 MPV 播放器。使用者無需另行安裝 MPV 應用程式;外部 MPV 與 VLC 仍可作為備援播放器使用。",
|
||||
"EMBEDDED_MPV_NOTICE_DESCRIPTION": "桌面版本可內含 libmpv 和 FFmpeg 程式庫,用於實驗性的內嵌 MPV 播放器。使用者無需另行安裝 MPV 應用程式;外部 MPV 與 VLC 仍可作為備援播放器使用。",
|
||||
"COMMUNITY": "專案與社群",
|
||||
"COMMUNITY_DESCRIPTION": "為儲存庫加上星標、閱讀部落格,或加入 Telegram 群組以取得最新資訊與協助。",
|
||||
"LINK_BLOG": "部落格",
|
||||
|
||||
@@ -5,7 +5,7 @@ This document records the current contract for embedded playback in portal detai
|
||||
## Summary
|
||||
|
||||
- Embedded web players are `videojs`, `html5`, and `artplayer`.
|
||||
- `embedded-mpv` exists as a hidden macOS-only feasibility harness backed by a native `libmpv` addon.
|
||||
- `embedded-mpv` exists as a hidden desktop experimental harness backed by a native `libmpv` addon.
|
||||
- Controlled external players are `mpv` and `vlc`.
|
||||
- macOS `.app` bundle paths are resolved only for real MPV/VLC apps. IINA may
|
||||
launch through the MPV path field when the user supplies an executable path
|
||||
@@ -50,12 +50,13 @@ The repository now contains a first-pass native embedded MPV harness for Electro
|
||||
|
||||
Current contract:
|
||||
|
||||
- macOS only
|
||||
- desktop only: macOS, Windows x64, and Linux x64 under X11/Xwayland
|
||||
- experimental opt-in
|
||||
- enabled in local development only when `IPTVNATOR_ENABLE_EMBEDDED_MPV_EXPERIMENT=1`
|
||||
- enabled in packaged macOS builds only when the bundled native addon and `vendored-lgpl` libmpv runtime load successfully
|
||||
- enabled in packaged desktop builds only when the bundled native addon and `vendored-lgpl` libmpv runtime load successfully
|
||||
- uses IPTVnator-owned controls and `ResolvedPortalPlayback` payloads
|
||||
- uses the libmpv render API on macOS and renders through an IPTVnator-owned native `NSView`
|
||||
- uses mpv `wid` embedding on Windows and Linux through IPTVnator-owned native child windows
|
||||
- defaults to libmpv's OpenGL render backend with `hwdec=auto-safe`
|
||||
- keeps the previous software renderer as a debug fallback via `IPTVNATOR_EMBEDDED_MPV_RENDERER=sw`
|
||||
- emits lightweight render diagnostics when `IPTVNATOR_TRACE_EMBEDDED_MPV=1` is set
|
||||
@@ -68,8 +69,10 @@ Current contract:
|
||||
|
||||
Current limitation:
|
||||
|
||||
- the current feasibility harness is still experimental and macOS-specific
|
||||
- the current feasibility harness is still experimental and platform-specific
|
||||
- the original macOS `wid` embedding path produced audio with a black video surface inside Electron, so the harness now avoids foreign-window embedding on macOS
|
||||
- Windows and Linux use the mpv `wid` path, require staged LGPL runtime files, and still need OS-native smoke coverage before public exposure
|
||||
- Linux native Wayland is not supported in this implementation; the Electron process must have `DISPLAY` through X11 or Xwayland
|
||||
- the OpenGL render path avoids the old per-frame `CGImage` copy path, but it still needs broader interaction, resize, and packaging coverage
|
||||
- startup deadlocks seen during early macOS playback bring-up are mitigated, but the feature is still kept behind the explicit experiment flag until more interaction and packaging coverage is proven
|
||||
- because of that, the setting is auto-sanitized back to the default inline player unless support detection reports that the experimental runtime is available
|
||||
@@ -145,7 +148,7 @@ The diagnostics remain client-only:
|
||||
- no ffprobe or server-side probing
|
||||
- no extra manifest fetch beyond the active player
|
||||
- no automatic failover to an external player
|
||||
- no embedded MPV macOS diagnostics
|
||||
- no embedded MPV diagnostics
|
||||
|
||||
Supported diagnostic codes are:
|
||||
|
||||
|
||||
@@ -6,9 +6,12 @@ This document explains how IPTVnator embeds MPV inside the Electron app, which f
|
||||
|
||||
Source files for the embedded MPV integration:
|
||||
|
||||
- `apps/electron-backend/build-embedded-mpv.js` builds the native addon for the current Electron runtime.
|
||||
- `apps/electron-backend/build-embedded-mpv.js` builds the native addon for the target Electron runtime.
|
||||
- `apps/electron-backend/native/binding.gyp` defines the native addon build.
|
||||
- `apps/electron-backend/native/src/embedded_mpv.mm` owns the macOS `libmpv` render integration.
|
||||
- `apps/electron-backend/native/src/embedded_mpv_win32.cc` owns the Windows `HWND` + mpv `wid` backend.
|
||||
- `apps/electron-backend/native/src/embedded_mpv_linux.cc` owns the Linux X11/Xwayland `Window` + mpv `wid` backend.
|
||||
- `apps/electron-backend/native/src/embedded_mpv_wid_common.h` owns the shared Windows/Linux libmpv session surface.
|
||||
- `apps/electron-backend/src/app/services/embedded-mpv-native.service.ts` owns Electron main-process session lifecycle and support detection.
|
||||
- `apps/electron-backend/src/app/events/embedded-mpv.events.ts` registers the IPC contract.
|
||||
- `apps/electron-backend/src/app/api/main.preload.ts` exposes the preload bridge to the renderer.
|
||||
@@ -23,19 +26,19 @@ The build directory contains files such as `Makefile`, `binding.Makefile`, `conf
|
||||
|
||||
## How It Is Embedded
|
||||
|
||||
The embedded player does not spawn the normal `mpv` application and does not use `--wid` window reparenting. IPTVnator loads `libmpv` through a native Node addon and renders MPV frames into an app-owned native macOS view.
|
||||
The embedded player does not spawn the normal `mpv` application. IPTVnator loads `libmpv` through a native Node addon and renders MPV frames into an app-owned native video surface. macOS uses the libmpv render API in an `NSOpenGLView` because the mpv `wid` path produced a black video surface inside Electron. Windows and Linux use mpv's `wid` option against IPTVnator-owned child windows (`HWND` on Windows, X11 `Window` on Linux/Xwayland).
|
||||
|
||||
The flow is:
|
||||
|
||||
1. Angular receives a `ResolvedPortalPlayback` payload and renders `EmbeddedMpvPlayerComponent`.
|
||||
2. The component paints a loading state before requesting native startup work.
|
||||
3. If available, the preload API asks the main process to prepare the embedded MPV addon. This loads `embedded_mpv.node` and its dylibs, but does not create a native view or MPV playback session.
|
||||
3. If available, the preload API asks the main process to prepare the embedded MPV addon. This loads `embedded_mpv.node` and its platform runtime files, but does not create a native view or MPV playback session.
|
||||
4. The component asks the preload API to create an embedded MPV session with the current viewport bounds and initial volume.
|
||||
5. The Electron preload forwards calls through IPC to the main process.
|
||||
6. `EmbeddedMpvNativeService` owns sessions, polls snapshots, and emits session updates to the renderer.
|
||||
7. The native addon creates an `mpv_handle`, configures `vo=libmpv`, disables MPV's own OSC/input handling, and creates an app-owned `NSOpenGLView` inside the Electron window content view.
|
||||
8. The addon creates a `mpv_render_context` and uses MPV's render API to draw frames into the OpenGL surface.
|
||||
9. Resize, scroll, and fullscreen changes are measured in Angular and sent back to the addon as native bounds so the `NSView` stays aligned with the Angular layout.
|
||||
7. The native addon creates an `mpv_handle`, disables MPV's own OSC/input handling, and creates an app-owned platform video host inside the Electron window.
|
||||
8. On macOS the addon configures `vo=libmpv`, creates a `mpv_render_context`, and draws into the OpenGL surface. On Windows/Linux it passes the child-window id to mpv through `wid` and uses `vo=gpu`.
|
||||
9. Resize, scroll, and fullscreen changes are measured in Angular and sent back to the addon as native bounds so the platform video host stays aligned with the Angular layout.
|
||||
10. Playback controls remain IPTVnator-owned Angular UI. MPV receives commands only through the controlled IPC surface.
|
||||
|
||||
The renderer never gets direct native-module access. It can only call the preload contract:
|
||||
@@ -53,11 +56,11 @@ The renderer never gets direct native-module access. It can only call the preloa
|
||||
- dispose session
|
||||
- subscribe to session updates
|
||||
|
||||
Settings uses the preload support API only as a lightweight availability check. That check verifies platform, experiment gating, addon presence, and bundled `libmpv.2.dylib` presence without `require()`-loading `embedded_mpv.node`. This avoids blocking Settings navigation on macOS `dlopen` and code-signing work.
|
||||
Settings uses the preload support API only as a lightweight availability check. That check verifies platform, experiment gating, addon presence, and bundled platform runtime presence without `require()`-loading `embedded_mpv.node`. This avoids blocking Settings navigation on synchronous native-addon loading and code-signing or dynamic-linker work.
|
||||
|
||||
When `embedded-mpv` is the saved player, the settings store schedules an idle `prepareEmbeddedMpv()` call. This intentionally moves the first native addon load away from the click-to-play path. It can still block the Electron main process briefly because Node native addon loading is synchronous, but doing it during idle is less visible than doing it when the user clicks a video. Actual MPV session creation still happens on playback because it needs the current Electron window handle and viewport bounds.
|
||||
|
||||
The MPV video surface is a native AppKit/OpenGL view, not a normal DOM element. Do not place critical Angular overlays on top of the video viewport and expect CSS `z-index` to win. The embedded MPV controls use a compositor-safe control dock below the native viewport instead of a true overlay on top of the OpenGL surface.
|
||||
The MPV video surface is a native platform view/window, not a normal DOM element. Do not place critical Angular overlays on top of the video viewport and expect CSS `z-index` to win. The embedded MPV controls use a compositor-safe control dock below the native viewport instead of a true overlay on top of the native video surface.
|
||||
|
||||
The dock has a stable reserved height while embedded controls are enabled. Controls fade in and out inside that fixed dock, so normal show/hide behavior does not resize the native MPV viewport or make the video jump. Volume and audio-track panels replace the default transport controls inside the same dock and provide a back button to return to the default controls. Popovers and menus must stay inside that dock unless the native layering strategy changes. The native MPV view deliberately ignores hit testing so mouse movement passes through to Chromium and can reveal Angular controls even when the pointer moves quickly across the video area.
|
||||
|
||||
@@ -110,16 +113,16 @@ The Angular side of the embedded MPV player is intentionally split so the player
|
||||
|
||||
- `embedded-mpv-format.utils.ts` — pure helpers (`formatTime`, `audioTrackLabel`, `subtitleTrackLabel`, `speedLabel`, `aspectLabel`, `volumeIcon`, `volumeLabel`, `readStoredVolume`, `persistVolume`, `measureBounds`) and preset constants (`SPEED_PRESETS`, `ASPECT_PRESETS`, `HIDDEN_BOUNDS`, `MENU_OPEN_BOTTOM_CUTOUT_PX`).
|
||||
- `embedded-mpv-shortcuts.ts` — `EmbeddedMpvShortcuts` class with `attach(handlers)` / `detach()`. Owns the document keydown listener and routes through a callback interface; the component supplies the callbacks. Listens for Space/K (toggle), F (fullscreen), arrow keys (seek/volume), M (mute), Escape (close popovers).
|
||||
- `embedded-mpv-overlay-visibility.service.ts` — singleton service that exposes `overlayActive: signal<boolean>`. Tracks `MatDialog.afterOpened`/`afterAllClosed` for dialog-shaped overlays and falls back to a `MutationObserver` on the CDK overlay container for any remaining backdrop-bearing CDK overlays. The MPV NSView is hidden off-screen while a modal is open so DOM dialogs can paint above it.
|
||||
- `embedded-mpv-overlay-visibility.service.ts` — singleton service that exposes `overlayActive: signal<boolean>`. Tracks `MatDialog.afterOpened`/`afterAllClosed` for dialog-shaped overlays and falls back to a `MutationObserver` on the CDK overlay container for any remaining backdrop-bearing CDK overlays. The native MPV video host is hidden off-screen while a modal is open so DOM dialogs can paint above it.
|
||||
- `embedded-mpv-ui-state.ts` — `EmbeddedMpvMenuState` (single-open popover state machine with `volumeOpen`, `audioOpen`, `subtitleOpen`, `speedOpen`, `aspectOpen` signals plus `anyOpen` computed; `toggle`/`open`/`close`/`closeAll` helpers) and `EmbeddedMpvFeedback` (transient overlay that auto-clears after a configurable delay; used for keypress feedback).
|
||||
- `embedded-mpv-session-controller.ts` — component-scoped `Injectable` service that owns the `support`, `session`, `sessionId`, `stalled`, and `retryToken` signals. Subscribes to `onEmbeddedMpvSessionUpdate`, runs the polling-driven `stalled` timer, owns bounds-sync (resize, scroll, overlay state), and exposes the imperative IPC surface (`startSession`, `togglePaused`, `seekBy`/`seekTo`, `applyVolume`, `setAudioTrack`, `setSubtitleTrack`, `setSpeed`, `setAspect`, `startRecording`, `stopRecording`, `retry`).
|
||||
- `embedded-mpv-player.component.ts` — view-only shell. Holds view children, derived `computed` signals, DOM event listeners (pointermove, pointerdown, fullscreenchange, dblclick), and three `effect()`s.
|
||||
|
||||
### Bounds compositing strategy
|
||||
|
||||
The native NSView paints over the WebContents layer, so any DOM region it covers cannot receive pointer events and any CSS `z-index` competition is unwinnable. The component compensates with a single `boundsProvider(host)` closure on the controller that returns one of three bound shapes, evaluated each time the active bounds-sync runs:
|
||||
The native video host paints outside the normal DOM stacking model, so any DOM region it covers cannot reliably receive pointer events and any CSS `z-index` competition is unwinnable. The component compensates with a single `boundsProvider(host)` closure on the controller that returns one of three bound shapes, evaluated each time the active bounds-sync runs:
|
||||
|
||||
- **Modal overlay open** (any MatDialog, including the command palette) → `HIDDEN_BOUNDS`. The MPV view moves off-screen so the dialog has the full window.
|
||||
- **Modal overlay open** (any MatDialog, including the command palette) → `HIDDEN_BOUNDS`. The MPV video host moves off-screen so the dialog has the full window.
|
||||
- **Control popover open** (any of the menu states above) → host bounds with `MENU_OPEN_BOTTOM_CUTOUT_PX` (300 px) removed from the bottom. The popover region becomes DOM-receiving while video keeps playing in the upper region.
|
||||
- **Idle** → full host bounds.
|
||||
|
||||
@@ -156,26 +159,28 @@ The Electron main process holds an `electron.powerSaveBlocker` of type `prevent-
|
||||
|
||||
Current development behavior:
|
||||
|
||||
- The addon build is macOS-only.
|
||||
- The build script first looks for a staged runtime at `vendor/embedded-mpv/darwin-<arch>/`.
|
||||
- The staged runtime must contain `include/mpv/client.h`, `lib/*.dylib`, and `runtime-manifest.json`.
|
||||
- The addon build supports `darwin`, `win32`, and `linux`; Windows and Linux builds require running on that target OS.
|
||||
- The build script first looks for a staged runtime at `vendor/embedded-mpv/<platform>-<arch>/`.
|
||||
- The staged runtime must contain `include/mpv/client.h`, platform runtime files, and `runtime-manifest.json`.
|
||||
- The compiled `.node` addon is copied into `dist/apps/electron-backend/native/embedded_mpv.node`.
|
||||
- Bundled runtime files are copied into `dist/apps/electron-backend/native/lib/`. Most are `.dylib` files, but some Homebrew-linked runtimes expose non-`.dylib` Mach-O files such as a framework `Python` binary.
|
||||
- macOS `afterPack` copies `dist/apps/electron-backend/native/` into `app.asar.unpacked/electron-backend/native/` so the addon, manifest, dylibs, and non-`.dylib` Mach-O runtime files are filesystem-addressable.
|
||||
- Linux and Windows packaging do not include the Embedded MPV native directory.
|
||||
- Bundled runtime files are copied into `dist/apps/electron-backend/native/lib/`. macOS copies `.dylib` and non-`.dylib` Mach-O dependencies, Windows copies `mpv-2.dll`/`mpv.dll` and import libraries, and Linux copies `libmpv.so*`.
|
||||
- `afterPack` copies `dist/apps/electron-backend/native/` into `app.asar.unpacked/electron-backend/native/` on macOS, Windows, and Linux so the addon, manifest, and runtime libraries are filesystem-addressable.
|
||||
|
||||
Current release caveat:
|
||||
|
||||
- Release packaging requires a `vendored-lgpl` runtime manifest.
|
||||
- Release packaging rejects embedded MPV binaries linked to `/opt/homebrew` or `/usr/local`.
|
||||
- macOS release packaging rejects embedded MPV binaries linked to `/opt/homebrew` or `/usr/local`.
|
||||
- Windows and Linux release packaging verifies that the platform runtime file is present when Embedded MPV is required.
|
||||
- Local development can opt into Homebrew `libmpv` only by setting `IPTVNATOR_EMBEDDED_MPV_ALLOW_HOMEBREW=1`; packaged release validation rejects that runtime origin.
|
||||
|
||||
Before public release, packaging must:
|
||||
|
||||
- stage an LGPL-compatible `libmpv` and required dylibs for `darwin-arm64` and `darwin-x64`
|
||||
- collect indirect dependencies expressed as absolute paths, `@loader_path`, or `@rpath`
|
||||
- rewrite install names and dependency paths to app-relative paths such as `@loader_path`
|
||||
- code-sign and notarize the full dependency set
|
||||
- stage an LGPL-compatible `libmpv` runtime for each release platform/architecture
|
||||
- collect indirect macOS dependencies expressed as absolute paths, `@loader_path`, or `@rpath`
|
||||
- rewrite macOS install names and dependency paths to app-relative paths such as `@loader_path`
|
||||
- code-sign and notarize the full macOS dependency set
|
||||
- ensure Windows runtime staging includes both the DLL and the import library used by `node-gyp`
|
||||
- ensure Linux runtime staging includes a runtime SONAME (`libmpv.so.2`, `libmpv.so.1`, or `libmpv.so`) and an unversioned `libmpv.so` linker name for local native builds
|
||||
- publish the corresponding FFmpeg/libmpv source and build metadata
|
||||
|
||||
Users do not need the MPV GUI application for this architecture. IPTVnator bundles `libmpv` for release builds. If the bundled runtime is missing or fails to load, embedded MPV is hidden/unsupported and the existing inline/external players remain available.
|
||||
@@ -193,25 +198,27 @@ Release runtime policy:
|
||||
- mpv must be built with `-Dlibmpv=true` and `-Dgpl=false`.
|
||||
- The runtime must be dynamically linked and shipped with license/source-distribution notices.
|
||||
|
||||
After building an LGPL-compatible prefix for an architecture:
|
||||
After building an LGPL-compatible prefix for a platform/architecture:
|
||||
|
||||
```bash
|
||||
node tools/embedded-mpv/stage-macos-runtime.mjs arm64 /path/to/lgpl-prefix
|
||||
node tools/embedded-mpv/stage-macos-runtime.mjs x64 /path/to/lgpl-prefix
|
||||
pnpm embedded-mpv:stage-runtime -- darwin arm64 /path/to/lgpl-prefix
|
||||
pnpm embedded-mpv:stage-runtime -- darwin x64 /path/to/lgpl-prefix
|
||||
pnpm embedded-mpv:stage-runtime -- win32 x64 /path/to/lgpl-prefix
|
||||
pnpm embedded-mpv:stage-runtime -- linux x64 /path/to/lgpl-prefix
|
||||
```
|
||||
|
||||
Tagged macOS release CI builds that prefix from pinned source archives first. The workflow can temporarily run the same path for macOS PR artifacts while the bundled runtime is being tested:
|
||||
|
||||
```bash
|
||||
pnpm embedded-mpv:build-runtime -- arm64 /tmp/embedded-mpv-prefix
|
||||
pnpm embedded-mpv:stage-runtime -- arm64 /tmp/embedded-mpv-prefix
|
||||
pnpm embedded-mpv:stage-runtime -- darwin arm64 /tmp/embedded-mpv-prefix
|
||||
```
|
||||
|
||||
During temporary PR and `master` artifact testing, CI can restore an exact-keyed GitHub Actions cache for the staged `vendor/embedded-mpv/darwin-<arch>/` runtime and skip the expensive source build. The cache only contains `include/`, `lib/`, and `runtime-manifest.json`; it never contains the compiled `embedded_mpv.node` addon because that target depends on Electron headers, ABI, architecture, and build environment. Runtime cache entries are saved only from trusted repository refs, and tagged public release builds continue to rebuild from pinned sources until a dedicated signed and attested runtime artifact flow exists.
|
||||
During temporary PR and `master` artifact testing, CI can restore an exact-keyed GitHub Actions cache for the staged `vendor/embedded-mpv/<platform>-<arch>/` runtime and skip the expensive source build where a source builder exists. The cache only contains `include/`, `lib/`, and `runtime-manifest.json`; it never contains the compiled `embedded_mpv.node` addon because that target depends on Electron headers, ABI, architecture, and build environment. Runtime cache entries are saved only from trusted repository refs, and tagged public macOS release builds continue to rebuild from pinned sources until a dedicated signed and attested runtime artifact flow exists. Windows and Linux currently use staged runtime cache inputs only; adding pinned source builders for those platforms is a separate release-hardening task.
|
||||
|
||||
The CI builder pins FFmpeg `8.1`, mpv `0.41.0`, libplacebo `7.360.1`, libass `0.17.3`, FreeType `2.13.3`, FriBidi `1.0.16`, and HarfBuzz `8.5.0`. FFmpeg disables autodetected external libraries so Homebrew libraries cannot silently enter the runtime. Libplacebo is checked out from git with the submodules required by its Meson build because the generated GitHub archive does not include submodule contents. Even with Vulkan disabled, libplacebo still compiles Vulkan stubs and needs `3rdparty/Vulkan-Headers`. The generated manifest records source URLs, archive SHA-256 values where applicable, libplacebo git commit/submodule metadata, FFmpeg configure flags, and mpv Meson flags. The staging step normalizes that manifest to `origin: vendored-lgpl`, which release package validation requires.
|
||||
|
||||
The Electron backend build consumes the staged runtime, copies Mach-O runtime files into the native build output, and rewrites Mach-O paths so `embedded_mpv.node` loads `@loader_path/lib/libmpv.2.dylib` instead of a machine-local Homebrew path. After `install_name_tool` rewrites any addon or runtime binary, the build re-signs that binary with an ad-hoc signature for local development. Release packaging still performs the normal app signing and notarization later.
|
||||
The Electron backend build consumes the staged runtime and copies platform runtime files into the native build output. macOS additionally rewrites Mach-O paths so `embedded_mpv.node` loads `@loader_path/lib/libmpv.2.dylib` instead of a machine-local Homebrew path. After `install_name_tool` rewrites any addon or runtime binary, the build re-signs that binary with an ad-hoc signature for local development. Release packaging still performs the normal app signing and notarization later.
|
||||
|
||||
For local development before the vendored runtime exists, Homebrew can be used explicitly:
|
||||
|
||||
@@ -219,9 +226,9 @@ For local development before the vendored runtime exists, Homebrew can be used e
|
||||
pnpm run serve:backend:embedded-mpv
|
||||
```
|
||||
|
||||
That script first runs the local native build with `IPTVNATOR_EMBEDDED_MPV_ALLOW_HOMEBREW=1`, then starts Electron with `IPTVNATOR_ENABLE_EMBEDDED_MPV_EXPERIMENT=1`. This path is intentionally development-only. Packaged macOS builds reject `homebrew-dev` manifests and any `/opt/homebrew` or `/usr/local` embedded MPV links.
|
||||
That script first runs the local native build with `IPTVNATOR_EMBEDDED_MPV_ALLOW_HOMEBREW=1`, then starts Electron with `IPTVNATOR_ENABLE_EMBEDDED_MPV_EXPERIMENT=1`. This path is intentionally macOS development-only. Packaged builds reject `homebrew-dev` manifests and macOS packages reject any `/opt/homebrew` or `/usr/local` embedded MPV links.
|
||||
|
||||
If the settings page does not show `Embedded MPV (Experimental, macOS)` after starting with those flags, check the native build output:
|
||||
If the settings page does not show `Embedded MPV (Experimental)` after starting with those flags, check the native build output:
|
||||
|
||||
```bash
|
||||
ls apps/electron-backend/native/build/Release/embedded_mpv.node
|
||||
@@ -237,40 +244,46 @@ codesign --verify --verbose=2 apps/electron-backend/native/build/Release/embedde
|
||||
codesign --verify --verbose=2 apps/electron-backend/native/build/Release/lib/libmpv.2.dylib
|
||||
```
|
||||
|
||||
If support detection reports a missing `@rpath/...` dependency, the dependency collector missed an indirect runtime file. The packaging helper must copy that file into `native/lib/`, rewrite the dependency to `@loader_path/<name>`, and include non-`.dylib` Mach-O files in the asset copy glob.
|
||||
If macOS support detection reports a missing `@rpath/...` dependency, the dependency collector missed an indirect runtime file. The packaging helper must copy that file into `native/lib/`, rewrite the dependency to `@loader_path/<name>`, and include non-`.dylib` Mach-O files in the asset copy glob.
|
||||
|
||||
## Same-Version macOS Release Gate
|
||||
## Same-Version Desktop Release Gate
|
||||
|
||||
The normal release tag can produce Linux, Windows, and macOS artifacts from the same source version while only macOS carries the bundled Embedded MPV runtime.
|
||||
The normal release tag can produce Linux, Windows, and macOS artifacts from the same source version. Embedded MPV is required only for jobs where `IPTVNATOR_REQUIRE_EMBEDDED_MPV=1`; otherwise package validators still reject a present but invalid runtime while allowing the addon to be absent.
|
||||
|
||||
For tagged macOS builds, CI must:
|
||||
|
||||
- build the pinned LGPL-compatible runtime for the matrix architecture
|
||||
- stage it into `vendor/embedded-mpv/darwin-${arch}` before `pnpm run build:backend`
|
||||
- set `IPTVNATOR_EMBEDDED_MPV_PLATFORM=darwin`
|
||||
- set `IPTVNATOR_EMBEDDED_MPV_ARCH=${arch}` for backend build and packaging
|
||||
- set `IPTVNATOR_REQUIRE_EMBEDDED_MPV=1` for packaging and package-layout verification
|
||||
|
||||
During the temporary macOS artifact tests, CI also sets `IPTVNATOR_REQUIRE_EMBEDDED_MPV=1` for macOS PR and `master` push backend build, packaging, and package-layout verification. After the artifacts are manually validated, remove the workflow's temporary `pull_request` and `refs/heads/master` conditions so PR, development, Linux, and Windows packaging leave `IPTVNATOR_REQUIRE_EMBEDDED_MPV` unset or `0`. In that normal mode the package validators still reject a present but invalid Embedded MPV runtime, but they do not require the addon to exist. This keeps the native feature in-tree without making non-macOS or non-release builds depend on macOS runtime artifacts.
|
||||
For Windows and Linux builds, CI must set `IPTVNATOR_EMBEDDED_MPV_PLATFORM`/`IPTVNATOR_EMBEDDED_MPV_ARCH` for native build and package verification. When an exact-keyed staged runtime cache is restored, the job can set `IPTVNATOR_REQUIRE_EMBEDDED_MPV=1` and verify the packaged `.dll` or `.so` runtime. Without a staged runtime, the build script writes an unavailable marker and Settings hides Embedded MPV.
|
||||
|
||||
During temporary artifact tests, CI may also set `IPTVNATOR_REQUIRE_EMBEDDED_MPV=1` for PR and `master` push jobs where a runtime is known to exist. After the artifacts are manually validated, remove temporary conditions so ordinary development builds leave `IPTVNATOR_REQUIRE_EMBEDDED_MPV` unset or `0`. This keeps the native feature in-tree without making every non-release build depend on runtime artifacts.
|
||||
|
||||
## Release Safety
|
||||
|
||||
The feature is still experimental. The largest risks are native-process risks, not normal Angular UI risks:
|
||||
|
||||
- a bad native addon or `libmpv` crash can crash the Electron main process
|
||||
- packaging can fail if `libmpv` or one of its dylib dependencies is missing, unsigned, or linked to the wrong runtime path
|
||||
- packaging can fail if `libmpv` or one of its platform runtime dependencies is missing, unsigned where signing applies, or linked to the wrong runtime path
|
||||
- macOS graphics behavior can vary across Intel, Apple Silicon, external displays, fullscreen transitions, and hardware decoding paths
|
||||
- Windows `HWND` and Linux X11/Xwayland embedding need packaged-app smoke coverage for focus, resize, and fullscreen behavior
|
||||
- Linux native Wayland is unsupported until a dedicated Wayland embedding path exists
|
||||
- Homebrew `libmpv` builds can target a newer macOS version than IPTVnator's declared deployment target
|
||||
|
||||
It is reasonable to ship the code in-tree behind the current experiment flag. It is not yet safe to make it the default player. It can be exposed as macOS-only experimental if support detection is strict, the UI clearly labels it experimental, and fallback to Video.js or external MPV/VLC stays available.
|
||||
It is reasonable to ship the code in-tree behind the current experiment flag. It is not yet safe to make it the default player. It can be exposed as desktop experimental if support detection is strict, the UI clearly labels it experimental, and fallback to Video.js or external MPV/VLC stays available.
|
||||
|
||||
If an embedded session fails to initialize, the app should keep the user in control by preserving the normal player setting choices. If a native crash occurs, normal settings fallback cannot intercept that crash, so broader macOS smoke testing and packaged-app testing are required before broad release.
|
||||
If an embedded session fails to initialize, the app should keep the user in control by preserving the normal player setting choices. If a native crash occurs, normal settings fallback cannot intercept that crash, so broader OS-specific smoke testing and packaged-app testing are required before broad release.
|
||||
|
||||
## Suggested Release Gate
|
||||
|
||||
Do not expose embedded MPV broadly until these pass on both Apple Silicon and Intel macOS:
|
||||
Do not expose embedded MPV broadly until these pass on every supported target:
|
||||
|
||||
- packaged `.app` starts without Homebrew installed
|
||||
- bundled `libmpv` and dependent dylibs pass code signing and notarization
|
||||
- packaged app starts without system `mpv` installed
|
||||
- bundled `libmpv` and dependent runtime files pass platform package validation
|
||||
- macOS bundled `libmpv` and dependent dylibs pass code signing and notarization
|
||||
- VOD resume starts near the saved offset
|
||||
- series EOF emits `ended` and embedded MPV auto-continues only inside the current season
|
||||
- live HLS, MPEG-TS, MP4/VOD, headers, referrer, volume, seek, fullscreen, route changes, and cleanup work
|
||||
|
||||
@@ -21,7 +21,11 @@
|
||||
"**/bindings/**",
|
||||
"**/file-uri-to-path/**",
|
||||
"**/*.node",
|
||||
"**/*.dylib"
|
||||
"**/*.dylib",
|
||||
"**/*.dll",
|
||||
"**/*.so",
|
||||
"**/*.so.*",
|
||||
"**/embedded-mpv-runtime.json"
|
||||
],
|
||||
"extraMetadata": {
|
||||
"name": "iptvnator",
|
||||
|
||||
+2
-1
@@ -45,7 +45,8 @@
|
||||
"verify:package-layout": "node tools/packaging/verify-electron-package-layout.mjs",
|
||||
"embedded-mpv:build-native:homebrew": "IPTVNATOR_EMBEDDED_MPV_ALLOW_HOMEBREW=1 nx run electron-backend:build-embedded-mpv",
|
||||
"embedded-mpv:build-runtime": "node tools/embedded-mpv/build-macos-runtime.mjs",
|
||||
"embedded-mpv:stage-runtime": "node tools/embedded-mpv/stage-macos-runtime.mjs",
|
||||
"embedded-mpv:stage-runtime": "node tools/embedded-mpv/stage-runtime.mjs",
|
||||
"embedded-mpv:stage-runtime:macos": "node tools/embedded-mpv/stage-macos-runtime.mjs",
|
||||
"serve:backend:embedded-mpv": "pnpm embedded-mpv:build-native:homebrew && IPTVNATOR_EMBEDDED_MPV_ALLOW_HOMEBREW=1 IPTVNATOR_ENABLE_EMBEDDED_MPV_EXPERIMENT=1 pnpm serve:backend",
|
||||
"package:app": "nx run electron-backend:make --prepackageOnly",
|
||||
"make:app": "nx run electron-backend:make",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Embedded MPV macOS Runtime
|
||||
# Embedded MPV Runtime
|
||||
|
||||
This folder contains tooling for preparing the macOS `libmpv` runtime that is bundled with IPTVnator's experimental embedded MPV player.
|
||||
This folder contains tooling for preparing the `libmpv` runtime that is bundled with IPTVnator's experimental embedded MPV player.
|
||||
|
||||
## Runtime Policy
|
||||
|
||||
@@ -27,20 +27,43 @@ vendor/embedded-mpv/
|
||||
include/mpv/client.h
|
||||
lib/*.dylib
|
||||
runtime-manifest.json
|
||||
win32-x64/
|
||||
include/mpv/client.h
|
||||
lib/mpv-2.dll
|
||||
lib/mpv.lib
|
||||
runtime-manifest.json
|
||||
linux-x64/
|
||||
include/mpv/client.h
|
||||
lib/libmpv.so.2
|
||||
lib/libmpv.so
|
||||
runtime-manifest.json
|
||||
```
|
||||
|
||||
The generated `lib/` and `include/` directories are release inputs, not source files. They are ignored by git by default.
|
||||
|
||||
## Staging A Built Runtime
|
||||
|
||||
After building an LGPL-compatible prefix for one architecture, stage it with:
|
||||
After building an LGPL-compatible prefix for one platform/architecture, stage it with:
|
||||
|
||||
```bash
|
||||
node tools/embedded-mpv/stage-macos-runtime.mjs arm64 /path/to/lgpl-prefix
|
||||
node tools/embedded-mpv/stage-macos-runtime.mjs x64 /path/to/lgpl-prefix
|
||||
pnpm embedded-mpv:stage-runtime -- darwin arm64 /path/to/lgpl-prefix
|
||||
pnpm embedded-mpv:stage-runtime -- darwin x64 /path/to/lgpl-prefix
|
||||
pnpm embedded-mpv:stage-runtime -- win32 x64 /path/to/lgpl-prefix
|
||||
pnpm embedded-mpv:stage-runtime -- linux x64 /path/to/lgpl-prefix
|
||||
```
|
||||
|
||||
The prefix must contain `include/mpv/client.h`, `lib/libmpv.2.dylib` or `lib/libmpv.dylib`, and all non-system dylib dependencies required by `libmpv`.
|
||||
For compatibility, the legacy macOS-only staging command is still available:
|
||||
|
||||
```bash
|
||||
pnpm embedded-mpv:stage-runtime:macos -- arm64 /path/to/lgpl-prefix
|
||||
pnpm embedded-mpv:stage-runtime:macos -- x64 /path/to/lgpl-prefix
|
||||
```
|
||||
|
||||
The prefix must contain `include/mpv/client.h` and the platform runtime files:
|
||||
|
||||
- macOS: `lib/libmpv.2.dylib` or `lib/libmpv.dylib` plus all non-system dylib dependencies
|
||||
- Windows: `lib/mpv.lib` or `lib/mpv-2.lib`, and `bin/mpv-2.dll` or `lib/mpv-2.dll`
|
||||
- Linux: `lib/libmpv.so.2`, `lib/libmpv.so.1`, or `lib/libmpv.so`; include a `libmpv.so` linker name when building locally
|
||||
|
||||
If the prefix contains `runtime-manifest.json`, the staging script copies its build metadata into the vendored manifest. At minimum, record:
|
||||
|
||||
@@ -54,10 +77,10 @@ Tagged macOS release builds build the runtime from pinned source archives before
|
||||
|
||||
```bash
|
||||
pnpm embedded-mpv:build-runtime -- arm64 /tmp/embedded-mpv-prefix
|
||||
pnpm embedded-mpv:stage-runtime -- arm64 /tmp/embedded-mpv-prefix
|
||||
pnpm embedded-mpv:stage-runtime -- darwin arm64 /tmp/embedded-mpv-prefix
|
||||
```
|
||||
|
||||
During temporary PR and `master` artifact testing, CI restores an exact-keyed GitHub Actions cache for the staged `vendor/embedded-mpv/darwin-<arch>/` runtime before falling back to the source build. The cache key includes the target architecture, macOS deployment target, Xcode version, and hashes of the runtime build/staging scripts. Cache entries are saved only from trusted repository refs and are treated strictly as a speed optimization; tagged release builds continue to rebuild from pinned sources unless a future signed and attested runtime artifact flow is introduced.
|
||||
During temporary PR and `master` artifact testing, CI restores an exact-keyed GitHub Actions cache for the staged `vendor/embedded-mpv/<platform>-<arch>/` runtime before falling back to the macOS source build where available. The cache key includes the target platform, architecture, macOS deployment target, Xcode version when available, and hashes of the runtime build/staging scripts. Cache entries are saved only from trusted repository refs and are treated strictly as a speed optimization; tagged macOS release builds continue to rebuild from pinned sources unless a future signed and attested runtime artifact flow is introduced.
|
||||
|
||||
The builder currently pins:
|
||||
|
||||
@@ -70,7 +93,7 @@ The build manifest records source URLs, downloaded archive SHA-256 values where
|
||||
|
||||
## Build Integration
|
||||
|
||||
`apps/electron-backend/build-embedded-mpv.js` links the native addon against the staged runtime, copies dylibs into `apps/electron-backend/native/build/Release/lib/`, rewrites Mach-O paths to `@loader_path`, and writes `embedded-mpv-runtime.json`.
|
||||
`apps/electron-backend/build-embedded-mpv.js` links the native addon against the staged runtime, copies runtime libraries into `apps/electron-backend/native/build/Release/lib/`, rewrites macOS Mach-O paths to `@loader_path`, and writes `embedded-mpv-runtime.json`.
|
||||
|
||||
For local macOS development with Homebrew `mpv`, use:
|
||||
|
||||
@@ -80,8 +103,14 @@ pnpm run serve:backend:embedded-mpv
|
||||
|
||||
The script rebuilds the native addon with `IPTVNATOR_EMBEDDED_MPV_ALLOW_HOMEBREW=1` before starting Electron with the experimental player enabled. Use this only for local testing; release packaging rejects the resulting `homebrew-dev` runtime manifest.
|
||||
|
||||
The macOS `afterPack` hook copies `dist/apps/electron-backend/native/` into `app.asar.unpacked/electron-backend/native/` so the addon, runtime manifest, dylibs, and non-`.dylib` Mach-O runtime files are available as real files. Linux and Windows artifacts do not include that native directory.
|
||||
The `afterPack` hook copies `dist/apps/electron-backend/native/` into `app.asar.unpacked/electron-backend/native/` so the addon, runtime manifest, and runtime libraries are available as real files on macOS, Windows, and Linux.
|
||||
|
||||
During release packaging, `tools/packaging/electron-after-pack.cjs` verifies that the packaged app uses a `vendored-lgpl` runtime and has no `/opt/homebrew` or `/usr/local` dynamic links for embedded MPV.
|
||||
During release packaging, `tools/packaging/electron-after-pack.cjs` verifies that the packaged app uses a `vendored-lgpl` runtime. macOS artifacts additionally verify that Mach-O dependencies have no `/opt/homebrew` or `/usr/local` dynamic links for embedded MPV.
|
||||
|
||||
Set `IPTVNATOR_REQUIRE_EMBEDDED_MPV=1` when packaging a macOS release artifact that must include Embedded MPV. The same variable is temporarily enabled for macOS PR and `master` push artifacts while the bundled runtime is being tested. After manual artifact validation, remove the workflow's PR and `refs/heads/master` conditions so PR and non-tag development builds leave the variable unset or `0` and can package without a staged runtime while Settings keeps Embedded MPV hidden.
|
||||
Set `IPTVNATOR_REQUIRE_EMBEDDED_MPV=1` when packaging a release artifact that must include Embedded MPV. The same variable is temporarily enabled for macOS PR and `master` push artifacts while the bundled runtime is being tested. Windows and Linux CI packaging requires Embedded MPV when an exact-keyed staged runtime cache is restored; otherwise those jobs build without the native addon and Settings keeps Embedded MPV hidden.
|
||||
|
||||
## Platform Notes
|
||||
|
||||
- macOS keeps the existing libmpv render-context backend because mpv `wid` stays black inside Electron on macOS.
|
||||
- Windows uses an embedded child `HWND` and passes it to mpv through `wid`.
|
||||
- Linux uses an X11 child window and passes it to mpv through `wid`. Native Wayland is not supported in v1; run under X11/Xwayland so `DISPLAY` is set.
|
||||
@@ -0,0 +1,228 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const rawArgs = process.argv.slice(2);
|
||||
const args = rawArgs[0] === '--' ? rawArgs.slice(1) : rawArgs;
|
||||
const [platform, arch, sourcePrefix] = args;
|
||||
const workspaceRoot = process.cwd();
|
||||
const validTargets = new Set([
|
||||
'darwin-arm64',
|
||||
'darwin-x64',
|
||||
'win32-x64',
|
||||
'linux-x64',
|
||||
]);
|
||||
|
||||
if (!validTargets.has(`${platform}-${arch}`) || !sourcePrefix) {
|
||||
console.error(
|
||||
[
|
||||
'Usage: node tools/embedded-mpv/stage-runtime.mjs <darwin|win32|linux> <arch> <lgpl-runtime-prefix>',
|
||||
'',
|
||||
'Supported targets:',
|
||||
'- darwin arm64',
|
||||
'- darwin x64',
|
||||
'- win32 x64',
|
||||
'- linux x64',
|
||||
'',
|
||||
'The prefix must contain include/mpv/client.h and dynamic libmpv runtime files.',
|
||||
].join('\n')
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const normalizedPrefix = path.resolve(sourcePrefix);
|
||||
const destinationRoot = path.join(
|
||||
workspaceRoot,
|
||||
'vendor',
|
||||
'embedded-mpv',
|
||||
`${platform}-${arch}`
|
||||
);
|
||||
const destinationIncludeDir = path.join(destinationRoot, 'include');
|
||||
const destinationLibDir = path.join(destinationRoot, 'lib');
|
||||
const sourceIncludeDir = path.join(normalizedPrefix, 'include');
|
||||
const sourceLibDir = path.join(normalizedPrefix, 'lib');
|
||||
const sourceBinDir = path.join(normalizedPrefix, 'bin');
|
||||
|
||||
function assertExists(filePath, message) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`${message}: ${filePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
function isFileLike(sourcePath, entry) {
|
||||
if (entry.isFile()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!entry.isSymbolicLink()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return fs.statSync(sourcePath).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function copyDirectory(sourceDir, destinationDir, filter) {
|
||||
if (!fs.existsSync(sourceDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
fs.mkdirSync(destinationDir, { recursive: true });
|
||||
|
||||
for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
|
||||
const sourcePath = path.join(sourceDir, entry.name);
|
||||
const destinationPath = path.join(destinationDir, entry.name);
|
||||
|
||||
if (filter && !filter(sourcePath, entry)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
copyDirectory(sourcePath, destinationPath, filter);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isFileLike(sourcePath, entry)) {
|
||||
fs.copyFileSync(sourcePath, destinationPath);
|
||||
fs.chmodSync(destinationPath, 0o755);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findRuntimeFile(libDir) {
|
||||
const candidatesByPlatform = {
|
||||
darwin: ['libmpv.2.dylib', 'libmpv.dylib'],
|
||||
win32: ['mpv-2.dll', 'mpv.dll'],
|
||||
linux: ['libmpv.so.2', 'libmpv.so.1', 'libmpv.so'],
|
||||
};
|
||||
const candidates = candidatesByPlatform[platform] ?? [];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
for (const directory of [libDir, sourceBinDir]) {
|
||||
const candidatePath = path.join(directory, candidate);
|
||||
if (fs.existsSync(candidatePath)) {
|
||||
return candidatePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function hasWindowsImportLibrary(libDir) {
|
||||
return ['mpv.lib', 'mpv-2.lib', 'libmpv.dll.a'].some((candidate) =>
|
||||
fs.existsSync(path.join(libDir, candidate))
|
||||
);
|
||||
}
|
||||
|
||||
function runtimeFileFilter(_sourcePath, entry) {
|
||||
if (entry.isDirectory()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (platform === 'darwin') {
|
||||
return entry.name.endsWith('.dylib');
|
||||
}
|
||||
|
||||
if (platform === 'win32') {
|
||||
return entry.name.endsWith('.dll') || entry.name.endsWith('.lib');
|
||||
}
|
||||
|
||||
if (platform === 'linux') {
|
||||
return /\.so(?:\.\d+)*$/.test(entry.name);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function listRuntimeFiles(libDir) {
|
||||
if (!fs.existsSync(libDir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return fs
|
||||
.readdirSync(libDir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isFile() || entry.isSymbolicLink())
|
||||
.map((entry) => entry.name)
|
||||
.sort();
|
||||
}
|
||||
|
||||
function readJsonIfExists(filePath) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
}
|
||||
|
||||
try {
|
||||
assertExists(
|
||||
path.join(sourceIncludeDir, 'mpv', 'client.h'),
|
||||
'Missing libmpv header'
|
||||
);
|
||||
if (!findRuntimeFile(sourceLibDir)) {
|
||||
throw new Error(`Missing libmpv runtime for ${platform} in ${sourceLibDir}`);
|
||||
}
|
||||
if (platform === 'win32' && !hasWindowsImportLibrary(sourceLibDir)) {
|
||||
throw new Error(`Missing Windows libmpv import library in ${sourceLibDir}`);
|
||||
}
|
||||
|
||||
fs.rmSync(destinationIncludeDir, { recursive: true, force: true });
|
||||
fs.rmSync(destinationLibDir, { recursive: true, force: true });
|
||||
fs.mkdirSync(destinationRoot, { recursive: true });
|
||||
|
||||
copyDirectory(
|
||||
path.join(sourceIncludeDir, 'mpv'),
|
||||
path.join(destinationIncludeDir, 'mpv')
|
||||
);
|
||||
copyDirectory(sourceLibDir, destinationLibDir, runtimeFileFilter);
|
||||
if (platform === 'win32') {
|
||||
copyDirectory(sourceBinDir, destinationLibDir, runtimeFileFilter);
|
||||
}
|
||||
|
||||
const externalManifest =
|
||||
readJsonIfExists(path.join(normalizedPrefix, 'runtime-manifest.json')) ??
|
||||
{};
|
||||
const manifest = {
|
||||
...externalManifest,
|
||||
origin: 'vendored-lgpl',
|
||||
platform,
|
||||
arch,
|
||||
stagedAt: new Date().toISOString(),
|
||||
runtimeFiles: listRuntimeFiles(destinationLibDir),
|
||||
ffmpeg: {
|
||||
licensePolicy: 'LGPL, built without --enable-gpl and --enable-nonfree',
|
||||
...externalManifest.ffmpeg,
|
||||
configureFlags:
|
||||
externalManifest.ffmpeg?.configureFlags ??
|
||||
'Record the exact FFmpeg configure flags used to build this runtime.',
|
||||
},
|
||||
mpv: {
|
||||
licensePolicy: 'LGPL-compatible libmpv, built with -Dlibmpv=true -Dgpl=false',
|
||||
...externalManifest.mpv,
|
||||
mesonFlags:
|
||||
externalManifest.mpv?.mesonFlags ??
|
||||
'Record the exact mpv Meson flags used to build this runtime.',
|
||||
},
|
||||
sourceDistribution:
|
||||
externalManifest.sourceDistribution ??
|
||||
`Publish exact source archives and local patches with the ${platform}-${arch} binary release.`,
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(destinationRoot, 'runtime-manifest.json'),
|
||||
`${JSON.stringify(manifest, null, 2)}\n`
|
||||
);
|
||||
|
||||
console.log(
|
||||
`Staged embedded MPV runtime for ${platform}-${arch} at ${path.relative(
|
||||
workspaceRoot,
|
||||
destinationRoot
|
||||
)}`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
const linuxAfterPack = require('./linux-after-pack.cjs');
|
||||
const {
|
||||
validatePackagedEmbeddedMpv,
|
||||
} = require('./embedded-mpv-macos.cjs');
|
||||
} = require('./embedded-mpv-packaging.cjs');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
@@ -44,24 +44,50 @@ function copyEmbeddedMpvNativeOutput(resourceDir, projectDir) {
|
||||
async function afterPackHook(params) {
|
||||
await linuxAfterPack(params);
|
||||
|
||||
if (params.electronPlatformName !== 'darwin') {
|
||||
return;
|
||||
}
|
||||
|
||||
const requireEmbeddedMpv = isTruthy(
|
||||
process.env.IPTVNATOR_REQUIRE_EMBEDDED_MPV
|
||||
);
|
||||
log(
|
||||
requireEmbeddedMpv
|
||||
? 'validating required embedded MPV macOS runtime links'
|
||||
: 'validating optional embedded MPV macOS runtime links'
|
||||
? `validating required embedded MPV ${params.electronPlatformName} runtime`
|
||||
: `validating optional embedded MPV ${params.electronPlatformName} runtime`
|
||||
);
|
||||
const resourceDir = getResourceDir(params);
|
||||
|
||||
copyEmbeddedMpvNativeOutput(
|
||||
resourceDir,
|
||||
params.packager.projectDir ?? process.cwd()
|
||||
);
|
||||
|
||||
const errors = validatePackagedEmbeddedMpv(resourceDir, {
|
||||
platform: params.electronPlatformName,
|
||||
required: requireEmbeddedMpv,
|
||||
});
|
||||
|
||||
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'));
|
||||
const resourceDir = appPath
|
||||
|
||||
return appPath
|
||||
? path.join(
|
||||
params.appOutDir.endsWith('.app')
|
||||
? params.appOutDir
|
||||
@@ -70,26 +96,6 @@ async function afterPackHook(params) {
|
||||
'Resources'
|
||||
)
|
||||
: params.appOutDir;
|
||||
|
||||
copyEmbeddedMpvNativeOutput(
|
||||
resourceDir,
|
||||
params.packager.projectDir ?? process.cwd()
|
||||
);
|
||||
|
||||
const errors = validatePackagedEmbeddedMpv(resourceDir, {
|
||||
required: requireEmbeddedMpv,
|
||||
});
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(
|
||||
[
|
||||
'Embedded MPV macOS package validation failed.',
|
||||
...errors.map((error) => `- ${error}`),
|
||||
].join('\n')
|
||||
);
|
||||
}
|
||||
|
||||
log('embedded MPV macOS runtime links validated');
|
||||
}
|
||||
|
||||
module.exports = afterPackHook;
|
||||
@@ -1,6 +1,7 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import { createRequire } from 'node:module';
|
||||
import os from 'node:os';
|
||||
import { dirname, join } from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -44,6 +45,13 @@ const packageLayoutVerifier = fs.readFileSync(
|
||||
join(currentDir, 'verify-electron-package-layout.mjs'),
|
||||
'utf8'
|
||||
);
|
||||
const electronAfterPackSource = fs.readFileSync(
|
||||
join(currentDir, 'electron-after-pack.cjs'),
|
||||
'utf8'
|
||||
);
|
||||
const {
|
||||
validatePackagedEmbeddedMpv,
|
||||
} = require('./embedded-mpv-packaging.cjs');
|
||||
|
||||
test('Linux package identity does not expose the internal Electron backend project name', () => {
|
||||
assert.equal(electronBuilderConfig.productName, 'IPTVnator');
|
||||
@@ -162,3 +170,97 @@ test('nx-electron packaging does not copy duplicate root package metadata', () =
|
||||
/filter:\s*\[['"]index\.js['"],\s*['"]package\.json['"]\]/
|
||||
);
|
||||
});
|
||||
|
||||
test('embedded MPV runtime binaries are unpacked on every supported desktop platform', () => {
|
||||
const asarUnpack = electronBuilderConfig.asarUnpack ?? [];
|
||||
|
||||
for (const requiredPattern of [
|
||||
'**/*.node',
|
||||
'**/*.dylib',
|
||||
'**/*.dll',
|
||||
'**/*.so',
|
||||
'**/*.so.*',
|
||||
'**/embedded-mpv-runtime.json',
|
||||
]) {
|
||||
assert.ok(
|
||||
asarUnpack.includes(requiredPattern),
|
||||
`electron-builder asarUnpack must include ${requiredPattern}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('embedded MPV package validation accepts Windows and Linux runtime files', () => {
|
||||
const tempDir = fs.mkdtempSync(join(os.tmpdir(), 'iptvnator-mpv-package-'));
|
||||
|
||||
try {
|
||||
for (const [platform, runtimeFile] of [
|
||||
['windows', 'mpv-2.dll'],
|
||||
['windows', join('lib', 'mpv.dll')],
|
||||
['linux', join('lib', 'libmpv.so.2')],
|
||||
]) {
|
||||
const resourceDir = join(tempDir, platform);
|
||||
const nativeDir = join(
|
||||
resourceDir,
|
||||
'app.asar.unpacked',
|
||||
'electron-backend',
|
||||
'native'
|
||||
);
|
||||
fs.mkdirSync(join(nativeDir, 'lib'), { recursive: true });
|
||||
fs.writeFileSync(join(nativeDir, 'embedded_mpv.node'), '');
|
||||
fs.writeFileSync(
|
||||
join(nativeDir, 'embedded-mpv-runtime.json'),
|
||||
JSON.stringify({ origin: 'vendored-lgpl' })
|
||||
);
|
||||
fs.writeFileSync(join(nativeDir, runtimeFile), '');
|
||||
|
||||
assert.deepEqual(
|
||||
validatePackagedEmbeddedMpv(resourceDir, {
|
||||
platform,
|
||||
required: true,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('embedded MPV packaging helpers use a cross-platform module name', () => {
|
||||
assert.match(
|
||||
electronAfterPackSource,
|
||||
/require\(['"]\.\/embedded-mpv-packaging\.cjs['"]\)/
|
||||
);
|
||||
assert.match(
|
||||
packageLayoutVerifier,
|
||||
/require\(['"]\.\/embedded-mpv-packaging\.cjs['"]\)/
|
||||
);
|
||||
});
|
||||
|
||||
test('embedded MPV package validation rejects missing required Windows runtime', () => {
|
||||
const tempDir = fs.mkdtempSync(join(os.tmpdir(), 'iptvnator-mpv-package-'));
|
||||
|
||||
try {
|
||||
const nativeDir = join(
|
||||
tempDir,
|
||||
'app.asar.unpacked',
|
||||
'electron-backend',
|
||||
'native'
|
||||
);
|
||||
fs.mkdirSync(join(nativeDir, 'lib'), { recursive: true });
|
||||
fs.writeFileSync(join(nativeDir, 'embedded_mpv.node'), '');
|
||||
fs.writeFileSync(
|
||||
join(nativeDir, 'embedded-mpv-runtime.json'),
|
||||
JSON.stringify({ origin: 'vendored-lgpl' })
|
||||
);
|
||||
|
||||
const errors = validatePackagedEmbeddedMpv(tempDir, {
|
||||
platform: 'windows',
|
||||
required: true,
|
||||
});
|
||||
|
||||
assert.match(errors.join('\n'), /Missing bundled embedded MPV runtime/);
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
+67
-12
@@ -399,15 +399,43 @@ function validateNoForbiddenRuntimeLinks(binaryPaths) {
|
||||
return errors;
|
||||
}
|
||||
|
||||
function getPackagedRuntimeCandidates(libDir, platform, nativeDir) {
|
||||
switch (platform) {
|
||||
case 'darwin':
|
||||
return [
|
||||
path.join(libDir, 'libmpv.2.dylib'),
|
||||
path.join(libDir, 'libmpv.dylib'),
|
||||
];
|
||||
case 'win32':
|
||||
return [
|
||||
nativeDir ? path.join(nativeDir, 'mpv-2.dll') : null,
|
||||
nativeDir ? path.join(nativeDir, 'mpv.dll') : null,
|
||||
path.join(libDir, 'mpv-2.dll'),
|
||||
path.join(libDir, 'mpv.dll'),
|
||||
].filter(Boolean);
|
||||
case 'linux':
|
||||
return [
|
||||
path.join(libDir, 'libmpv.so.2'),
|
||||
path.join(libDir, 'libmpv.so.1'),
|
||||
path.join(libDir, 'libmpv.so'),
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeEmbeddedMpvPlatform(value) {
|
||||
if (value === 'macos') {
|
||||
return 'darwin';
|
||||
}
|
||||
if (value === 'windows') {
|
||||
return 'win32';
|
||||
}
|
||||
return value ?? process.platform;
|
||||
}
|
||||
|
||||
function validatePackagedEmbeddedMpv(resourceDir, options = {}) {
|
||||
if (process.platform !== 'darwin') {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!commandExists('otool')) {
|
||||
return ['otool is required to validate embedded MPV macOS packaging.'];
|
||||
}
|
||||
|
||||
const platform = normalizeEmbeddedMpvPlatform(options.platform);
|
||||
const unpackedNativeDir = path.join(
|
||||
resourceDir,
|
||||
'app.asar.unpacked',
|
||||
@@ -440,12 +468,38 @@ function validatePackagedEmbeddedMpv(resourceDir, options = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs.existsSync(path.join(libDir, 'libmpv.2.dylib'))) {
|
||||
errors.push(`Missing bundled libmpv.2.dylib in ${libDir}`);
|
||||
const runtimeCandidates = getPackagedRuntimeCandidates(
|
||||
libDir,
|
||||
platform,
|
||||
unpackedNativeDir
|
||||
);
|
||||
if (
|
||||
runtimeCandidates.length > 0 &&
|
||||
!runtimeCandidates.some((candidate) => fs.existsSync(candidate))
|
||||
) {
|
||||
errors.push(
|
||||
[
|
||||
`Missing bundled embedded MPV runtime for ${platform} in ${libDir}.`,
|
||||
'Expected one of:',
|
||||
...runtimeCandidates.map((candidate) => `- ${candidate}`),
|
||||
].join('\n')
|
||||
);
|
||||
}
|
||||
|
||||
const binaries = [addonPath, ...listRuntimeFiles(libDir)];
|
||||
errors.push(...validateNoForbiddenRuntimeLinks(binaries));
|
||||
if (platform === 'darwin') {
|
||||
if (process.platform !== 'darwin') {
|
||||
errors.push(
|
||||
'macOS embedded MPV link validation must run on a macOS host.'
|
||||
);
|
||||
} else if (!commandExists('otool')) {
|
||||
errors.push(
|
||||
'otool is required to validate embedded MPV macOS packaging.'
|
||||
);
|
||||
} else {
|
||||
const binaries = [addonPath, ...listRuntimeFiles(libDir)];
|
||||
errors.push(...validateNoForbiddenRuntimeLinks(binaries));
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
@@ -461,5 +515,6 @@ module.exports = {
|
||||
parseOtoolRpaths,
|
||||
patchAddonForBundledRuntime,
|
||||
validateNoForbiddenRuntimeLinks,
|
||||
getPackagedRuntimeCandidates,
|
||||
validatePackagedEmbeddedMpv,
|
||||
};
|
||||
@@ -6,7 +6,7 @@ import { buildElectronBuilderMetadata } from './generate-electron-builder-metada
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { extractFile } = require('@electron/asar');
|
||||
const { validatePackagedEmbeddedMpv } = require('./embedded-mpv-macos.cjs');
|
||||
const { validatePackagedEmbeddedMpv } = require('./embedded-mpv-packaging.cjs');
|
||||
const args = process.argv.slice(2);
|
||||
const normalizedArgs = args[0] === '--' ? args.slice(1) : args;
|
||||
const [platform, arch = ''] = normalizedArgs;
|
||||
@@ -566,13 +566,12 @@ function verifyResourceDir(resourceDir) {
|
||||
verifyLinuxLauncher(resourceDir, errors);
|
||||
}
|
||||
|
||||
if (platform === 'macos') {
|
||||
errors.push(
|
||||
...validatePackagedEmbeddedMpv(resourceDir, {
|
||||
required: embeddedMpvRequired,
|
||||
})
|
||||
);
|
||||
}
|
||||
errors.push(
|
||||
...validatePackagedEmbeddedMpv(resourceDir, {
|
||||
platform,
|
||||
required: embeddedMpvRequired,
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
resourceDir,
|
||||
|
||||
Reference in new issue
Block a user