mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
feat(embedded-mpv): add Windows frame-copy support (#1175)
Port the embedded mpv frame-copy pipeline to Windows with WGL rendering and named shared memory. Includes packaging validation, platform gates, tests, and architecture documentation.
This commit is contained in:
1 parent
7d75d989e8
commit
59e08fd2d6
41 files changed
+1284
-157
No files matched your search
@@ -102,6 +102,37 @@ const embeddedMpvWin32Source = fs.readFileSync(
|
||||
);
|
||||
const { validatePackagedEmbeddedMpv } = require('./embedded-mpv-packaging.cjs');
|
||||
|
||||
function writeWindowsHelperFixture(helperPath, importedDllName) {
|
||||
const peOffset = 0x80;
|
||||
const optionalHeaderOffset = peOffset + 24;
|
||||
const optionalHeaderSize = 0xf0;
|
||||
const sectionTableOffset = optionalHeaderOffset + optionalHeaderSize;
|
||||
const importRva = 0x1000;
|
||||
const importRawOffset = 0x200;
|
||||
const importNameOffset = 0x30;
|
||||
const image = Buffer.alloc(0x400);
|
||||
|
||||
image.write('MZ', 0, 'ascii');
|
||||
image.writeUInt32LE(peOffset, 0x3c);
|
||||
image.write('PE\0\0', peOffset, 'ascii');
|
||||
image.writeUInt16LE(0x8664, peOffset + 4);
|
||||
image.writeUInt16LE(1, peOffset + 6);
|
||||
image.writeUInt16LE(optionalHeaderSize, peOffset + 20);
|
||||
image.writeUInt16LE(0x20b, optionalHeaderOffset);
|
||||
image.writeUInt32LE(0x200, optionalHeaderOffset + 60);
|
||||
image.writeUInt32LE(16, optionalHeaderOffset + 108);
|
||||
image.writeUInt32LE(importRva, optionalHeaderOffset + 120);
|
||||
image.writeUInt32LE(40, optionalHeaderOffset + 124);
|
||||
image.write('.idata\0\0', sectionTableOffset, 'ascii');
|
||||
image.writeUInt32LE(0x200, sectionTableOffset + 8);
|
||||
image.writeUInt32LE(importRva, sectionTableOffset + 12);
|
||||
image.writeUInt32LE(0x200, sectionTableOffset + 16);
|
||||
image.writeUInt32LE(importRawOffset, sectionTableOffset + 20);
|
||||
image.writeUInt32LE(importRva + importNameOffset, importRawOffset + 12);
|
||||
image.write(`${importedDllName}\0`, importRawOffset + importNameOffset);
|
||||
fs.writeFileSync(helperPath, image);
|
||||
}
|
||||
|
||||
test('Linux package identity does not expose the internal Electron backend project name', () => {
|
||||
assert.equal(electronBuilderConfig.productName, 'IPTVnator');
|
||||
assert.equal(electronBuilderConfig.extraMetadata?.name, 'iptvnator');
|
||||
@@ -119,8 +150,10 @@ test('Linux package identity does not expose the internal Electron backend proje
|
||||
});
|
||||
|
||||
test('GitHub Releases auto-update metadata is generated and uploaded', () => {
|
||||
// \r?\n keeps this host-agnostic: Windows checkouts with autocrlf see
|
||||
// CRLF in the workflow file.
|
||||
const releaseFiles = buildAndMakeWorkflow.match(
|
||||
/files: \|\n([\s\S]*?)\n\s+env:/
|
||||
/files: \|\r?\n([\s\S]*?)\r?\n\s+env:/
|
||||
)?.[1];
|
||||
|
||||
assert.ok(releaseFiles, 'release upload files block must exist');
|
||||
@@ -304,10 +337,16 @@ test('embedded MPV package validation accepts Windows runtime files and Linux pr
|
||||
for (const [platform, runtimeFile] of [
|
||||
['windows', 'mpv-2.dll'],
|
||||
['windows', 'libmpv-2.dll'],
|
||||
['windows', join('lib', 'mpv.dll')],
|
||||
['windows', join('lib', 'libmpv.dll')],
|
||||
['windows', 'mpv.dll'],
|
||||
['windows', 'libmpv.dll'],
|
||||
]) {
|
||||
const resourceDir = join(tempDir, platform);
|
||||
// One fixture dir per runtime-file scenario: the frame-copy
|
||||
// artifacts written below must not leak into the next
|
||||
// iteration's missing-artifact assertions.
|
||||
const resourceDir = join(
|
||||
tempDir,
|
||||
`${platform}-${runtimeFile.replace(/[\\/]/g, '_')}`
|
||||
);
|
||||
const nativeDir = join(
|
||||
resourceDir,
|
||||
'app.asar.unpacked',
|
||||
@@ -322,6 +361,32 @@ test('embedded MPV package validation accepts Windows runtime files and Linux pr
|
||||
);
|
||||
fs.writeFileSync(join(nativeDir, runtimeFile), '');
|
||||
|
||||
// Windows packages that ship the addon must also ship the
|
||||
// frame-copy engine artifacts built by the same binding.gyp run.
|
||||
const missingWindowsFrameCopyErrors = validatePackagedEmbeddedMpv(
|
||||
resourceDir,
|
||||
{ platform, required: true }
|
||||
);
|
||||
assert.ok(
|
||||
missingWindowsFrameCopyErrors.some((error) =>
|
||||
error.includes('iptvnator_mpv_helper.exe')
|
||||
)
|
||||
);
|
||||
assert.ok(
|
||||
missingWindowsFrameCopyErrors.some((error) =>
|
||||
error.includes('embedded_mpv_frame_reader.node')
|
||||
)
|
||||
);
|
||||
|
||||
writeWindowsHelperFixture(
|
||||
join(nativeDir, 'iptvnator_mpv_helper.exe'),
|
||||
runtimeFile
|
||||
);
|
||||
fs.writeFileSync(
|
||||
join(nativeDir, 'embedded_mpv_frame_reader.node'),
|
||||
''
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
validatePackagedEmbeddedMpv(resourceDir, {
|
||||
platform,
|
||||
@@ -371,10 +436,10 @@ test('embedded MPV package validation accepts Windows runtime files and Linux pr
|
||||
// Host-agnostic assertion: on non-macOS hosts the validator also
|
||||
// reports that link validation needs a macOS host, so only the
|
||||
// frame-copy artifact requirement is asserted here.
|
||||
const remainingErrors = validatePackagedEmbeddedMpv(
|
||||
darwinResourceDir,
|
||||
{ platform: 'darwin', required: true }
|
||||
);
|
||||
const remainingErrors = validatePackagedEmbeddedMpv(darwinResourceDir, {
|
||||
platform: 'darwin',
|
||||
required: true,
|
||||
});
|
||||
assert.ok(
|
||||
!remainingErrors.some((error) =>
|
||||
error.includes('frame-copy artifact')
|
||||
@@ -408,6 +473,138 @@ test('embedded MPV package validation accepts Windows runtime files and Linux pr
|
||||
}
|
||||
});
|
||||
|
||||
test('Windows frame-copy packages reject an mpv DLL that exists only under native/lib', () => {
|
||||
const tempDir = fs.mkdtempSync(join(os.tmpdir(), 'iptvnator-mpv-package-'));
|
||||
|
||||
try {
|
||||
for (const runtimeFile of [
|
||||
'mpv-2.dll',
|
||||
'libmpv-2.dll',
|
||||
'mpv.dll',
|
||||
'libmpv.dll',
|
||||
]) {
|
||||
const resourceDir = join(tempDir, runtimeFile);
|
||||
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' })
|
||||
);
|
||||
writeWindowsHelperFixture(
|
||||
join(nativeDir, 'iptvnator_mpv_helper.exe'),
|
||||
runtimeFile
|
||||
);
|
||||
fs.writeFileSync(
|
||||
join(nativeDir, 'embedded_mpv_frame_reader.node'),
|
||||
''
|
||||
);
|
||||
fs.writeFileSync(join(nativeDir, 'lib', runtimeFile), '');
|
||||
|
||||
const errors = validatePackagedEmbeddedMpv(resourceDir, {
|
||||
platform: 'windows',
|
||||
required: true,
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
errors.some(
|
||||
(error) =>
|
||||
error.includes(
|
||||
'beside the Windows frame-copy helper'
|
||||
) && error.includes(nativeDir)
|
||||
),
|
||||
`${runtimeFile} under native/lib must not satisfy the helper DLL requirement: ${errors.join('; ')}`
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('Windows frame-copy packages require the DLL imported by the helper', () => {
|
||||
const tempDir = fs.mkdtempSync(join(os.tmpdir(), 'iptvnator-mpv-package-'));
|
||||
|
||||
try {
|
||||
const nativeDir = join(
|
||||
tempDir,
|
||||
'app.asar.unpacked',
|
||||
'electron-backend',
|
||||
'native'
|
||||
);
|
||||
fs.mkdirSync(nativeDir, { recursive: true });
|
||||
fs.writeFileSync(join(nativeDir, 'embedded_mpv.node'), '');
|
||||
fs.writeFileSync(
|
||||
join(nativeDir, 'embedded-mpv-runtime.json'),
|
||||
JSON.stringify({ origin: 'vendored-lgpl' })
|
||||
);
|
||||
writeWindowsHelperFixture(
|
||||
join(nativeDir, 'iptvnator_mpv_helper.exe'),
|
||||
'mpv-2.dll'
|
||||
);
|
||||
fs.writeFileSync(join(nativeDir, 'embedded_mpv_frame_reader.node'), '');
|
||||
fs.writeFileSync(join(nativeDir, 'libmpv.dll'), '');
|
||||
|
||||
const errors = validatePackagedEmbeddedMpv(tempDir, {
|
||||
platform: 'windows',
|
||||
required: true,
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
errors.some(
|
||||
(error) =>
|
||||
error.includes('imports mpv-2.dll') &&
|
||||
error.includes(join(nativeDir, 'mpv-2.dll'))
|
||||
),
|
||||
`a different accepted DLL must not satisfy the helper import: ${errors.join('; ')}`
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('Windows frame-copy package validation fails closed for a malformed helper', () => {
|
||||
const tempDir = fs.mkdtempSync(join(os.tmpdir(), 'iptvnator-mpv-package-'));
|
||||
|
||||
try {
|
||||
const nativeDir = join(
|
||||
tempDir,
|
||||
'app.asar.unpacked',
|
||||
'electron-backend',
|
||||
'native'
|
||||
);
|
||||
fs.mkdirSync(nativeDir, { 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, 'iptvnator_mpv_helper.exe'), 'MZ');
|
||||
fs.writeFileSync(join(nativeDir, 'embedded_mpv_frame_reader.node'), '');
|
||||
fs.writeFileSync(join(nativeDir, 'mpv-2.dll'), '');
|
||||
|
||||
const errors = validatePackagedEmbeddedMpv(tempDir, {
|
||||
platform: 'windows',
|
||||
required: true,
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
errors.some((error) =>
|
||||
error.includes(
|
||||
'Unable to inspect Windows frame-copy helper imports'
|
||||
)
|
||||
),
|
||||
`malformed helper must fail package validation: ${errors.join('; ')}`
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('Windows embedded MPV staging preserves import-library DLL basenames', () => {
|
||||
assert.match(
|
||||
embeddedMpvStageRuntimeSource,
|
||||
@@ -472,10 +669,7 @@ test('frame-copy packaging file operations enforce modes and remove stale artifa
|
||||
|
||||
try {
|
||||
const helperPath = join(tempDir, 'iptvnator_mpv_helper');
|
||||
const windowsHelperPath = join(
|
||||
tempDir,
|
||||
'iptvnator_mpv_helper.exe'
|
||||
);
|
||||
const windowsHelperPath = join(tempDir, 'iptvnator_mpv_helper.exe');
|
||||
const readerPath = join(tempDir, 'embedded_mpv_frame_reader.node');
|
||||
fs.writeFileSync(helperPath, '#!/bin/sh\n');
|
||||
fs.chmodSync(helperPath, 0o644);
|
||||
|
||||
@@ -4,6 +4,12 @@ const { spawnSync } = require('child_process');
|
||||
|
||||
const forbiddenRuntimePathPrefixes = ['/opt/homebrew/', '/usr/local/'];
|
||||
const systemRuntimePathPrefixes = ['/System/Library/', '/usr/lib/'];
|
||||
const windowsMpvRuntimeNames = [
|
||||
'mpv-2.dll',
|
||||
'libmpv-2.dll',
|
||||
'mpv.dll',
|
||||
'libmpv.dll',
|
||||
];
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
const result = spawnSync(command, args, {
|
||||
@@ -67,6 +73,177 @@ function listRuntimeFiles(directoryPath) {
|
||||
.sort();
|
||||
}
|
||||
|
||||
function assertPeRange(image, offset, size, label) {
|
||||
if (
|
||||
!Number.isSafeInteger(offset) ||
|
||||
!Number.isSafeInteger(size) ||
|
||||
offset < 0 ||
|
||||
size < 0 ||
|
||||
offset + size > image.length
|
||||
) {
|
||||
throw new Error(`Invalid PE image: ${label} is out of range.`);
|
||||
}
|
||||
}
|
||||
|
||||
function readPeImportedDllNames(binaryPath) {
|
||||
const image = fs.readFileSync(binaryPath);
|
||||
assertPeRange(image, 0, 0x40, 'DOS header');
|
||||
if (image.toString('ascii', 0, 2) !== 'MZ') {
|
||||
throw new Error('Invalid PE image: missing DOS signature.');
|
||||
}
|
||||
|
||||
const peOffset = image.readUInt32LE(0x3c);
|
||||
assertPeRange(image, peOffset, 24, 'PE header');
|
||||
if (image.toString('ascii', peOffset, peOffset + 4) !== 'PE\0\0') {
|
||||
throw new Error('Invalid PE image: missing PE signature.');
|
||||
}
|
||||
|
||||
const sectionCount = image.readUInt16LE(peOffset + 6);
|
||||
const optionalHeaderSize = image.readUInt16LE(peOffset + 20);
|
||||
const optionalHeaderOffset = peOffset + 24;
|
||||
assertPeRange(
|
||||
image,
|
||||
optionalHeaderOffset,
|
||||
optionalHeaderSize,
|
||||
'optional header'
|
||||
);
|
||||
|
||||
const optionalMagic = image.readUInt16LE(optionalHeaderOffset);
|
||||
const dataDirectoryOffset =
|
||||
optionalMagic === 0x20b
|
||||
? optionalHeaderOffset + 112
|
||||
: optionalMagic === 0x10b
|
||||
? optionalHeaderOffset + 96
|
||||
: null;
|
||||
const directoryCountOffset =
|
||||
optionalMagic === 0x20b
|
||||
? optionalHeaderOffset + 108
|
||||
: optionalMagic === 0x10b
|
||||
? optionalHeaderOffset + 92
|
||||
: null;
|
||||
if (dataDirectoryOffset === null || directoryCountOffset === null) {
|
||||
throw new Error(
|
||||
`Invalid PE image: unsupported optional-header magic 0x${optionalMagic.toString(16)}.`
|
||||
);
|
||||
}
|
||||
const optionalHeaderEnd = optionalHeaderOffset + optionalHeaderSize;
|
||||
if (
|
||||
directoryCountOffset + 4 > optionalHeaderEnd ||
|
||||
dataDirectoryOffset + 16 > optionalHeaderEnd
|
||||
) {
|
||||
throw new Error(
|
||||
'Invalid PE image: data directories exceed the optional header.'
|
||||
);
|
||||
}
|
||||
assertPeRange(image, directoryCountOffset, 4, 'data-directory count');
|
||||
if (image.readUInt32LE(directoryCountOffset) < 2) {
|
||||
return [];
|
||||
}
|
||||
assertPeRange(image, dataDirectoryOffset + 8, 8, 'import directory');
|
||||
|
||||
const importRva = image.readUInt32LE(dataDirectoryOffset + 8);
|
||||
const importSize = image.readUInt32LE(dataDirectoryOffset + 12);
|
||||
if (importRva === 0 && importSize === 0) {
|
||||
return [];
|
||||
}
|
||||
if (importRva === 0 || importSize < 20) {
|
||||
throw new Error('Invalid PE image: malformed import directory.');
|
||||
}
|
||||
|
||||
const sectionTableOffset = optionalHeaderOffset + optionalHeaderSize;
|
||||
assertPeRange(
|
||||
image,
|
||||
sectionTableOffset,
|
||||
sectionCount * 40,
|
||||
'section table'
|
||||
);
|
||||
const sections = Array.from({ length: sectionCount }, (_, index) => {
|
||||
const offset = sectionTableOffset + index * 40;
|
||||
return {
|
||||
virtualSize: image.readUInt32LE(offset + 8),
|
||||
virtualAddress: image.readUInt32LE(offset + 12),
|
||||
rawSize: image.readUInt32LE(offset + 16),
|
||||
rawOffset: image.readUInt32LE(offset + 20),
|
||||
};
|
||||
});
|
||||
|
||||
const rvaToLocation = (rva, label) => {
|
||||
const section = sections.find(
|
||||
(candidate) =>
|
||||
rva >= candidate.virtualAddress &&
|
||||
rva <
|
||||
candidate.virtualAddress +
|
||||
Math.max(candidate.virtualSize, candidate.rawSize)
|
||||
);
|
||||
if (!section) {
|
||||
throw new Error(
|
||||
`Invalid PE image: ${label} RVA 0x${rva.toString(16)} is not mapped.`
|
||||
);
|
||||
}
|
||||
|
||||
const sectionOffset = rva - section.virtualAddress;
|
||||
if (sectionOffset >= section.rawSize) {
|
||||
throw new Error(
|
||||
`Invalid PE image: ${label} is outside section file data.`
|
||||
);
|
||||
}
|
||||
const offset = section.rawOffset + sectionOffset;
|
||||
assertPeRange(image, offset, 1, label);
|
||||
return {
|
||||
offset,
|
||||
sectionEnd: section.rawOffset + section.rawSize,
|
||||
};
|
||||
};
|
||||
|
||||
const importedDllNames = [];
|
||||
const descriptorCount = Math.floor(importSize / 20);
|
||||
let foundTerminator = false;
|
||||
for (let index = 0; index < descriptorCount; index += 1) {
|
||||
const descriptorLocation = rvaToLocation(
|
||||
importRva + index * 20,
|
||||
'import descriptor'
|
||||
);
|
||||
const descriptorOffset = descriptorLocation.offset;
|
||||
if (descriptorOffset + 20 > descriptorLocation.sectionEnd) {
|
||||
throw new Error(
|
||||
'Invalid PE image: import descriptor crosses section file data.'
|
||||
);
|
||||
}
|
||||
assertPeRange(image, descriptorOffset, 20, 'import descriptor');
|
||||
const descriptor = image.subarray(
|
||||
descriptorOffset,
|
||||
descriptorOffset + 20
|
||||
);
|
||||
if (descriptor.every((value) => value === 0)) {
|
||||
foundTerminator = true;
|
||||
break;
|
||||
}
|
||||
|
||||
const nameRva = image.readUInt32LE(descriptorOffset + 12);
|
||||
const nameLocation = rvaToLocation(nameRva, 'imported DLL name');
|
||||
const nameOffset = nameLocation.offset;
|
||||
const nameEnd = image.indexOf(0, nameOffset);
|
||||
if (nameEnd < 0 || nameEnd >= nameLocation.sectionEnd) {
|
||||
throw new Error(
|
||||
'Invalid PE image: imported DLL name is not terminated.'
|
||||
);
|
||||
}
|
||||
const name = image.toString('ascii', nameOffset, nameEnd);
|
||||
if (!name) {
|
||||
throw new Error('Invalid PE image: imported DLL name is empty.');
|
||||
}
|
||||
importedDllNames.push(name);
|
||||
}
|
||||
|
||||
if (!foundTerminator) {
|
||||
throw new Error(
|
||||
'Invalid PE image: import descriptor table has no terminator.'
|
||||
);
|
||||
}
|
||||
|
||||
return [...new Set(importedDllNames)];
|
||||
}
|
||||
|
||||
function parseOtoolDependencies(binaryPath) {
|
||||
const output = run('otool', ['-L', binaryPath]);
|
||||
return output
|
||||
@@ -426,16 +603,12 @@ function getPackagedRuntimeCandidates(libDir, platform, nativeDir) {
|
||||
path.join(libDir, 'libmpv.dylib'),
|
||||
];
|
||||
case 'win32':
|
||||
return [
|
||||
nativeDir ? path.join(nativeDir, 'mpv-2.dll') : null,
|
||||
nativeDir ? path.join(nativeDir, 'libmpv-2.dll') : null,
|
||||
nativeDir ? path.join(nativeDir, 'mpv.dll') : null,
|
||||
nativeDir ? path.join(nativeDir, 'libmpv.dll') : null,
|
||||
path.join(libDir, 'mpv-2.dll'),
|
||||
path.join(libDir, 'libmpv-2.dll'),
|
||||
path.join(libDir, 'mpv.dll'),
|
||||
path.join(libDir, 'libmpv.dll'),
|
||||
].filter(Boolean);
|
||||
return windowsMpvRuntimeNames.flatMap((name) =>
|
||||
[
|
||||
nativeDir ? path.join(nativeDir, name) : null,
|
||||
path.join(libDir, name),
|
||||
].filter(Boolean)
|
||||
);
|
||||
case 'linux':
|
||||
return [];
|
||||
default:
|
||||
@@ -480,7 +653,11 @@ function getEmbeddedMpvAddonArch(env = process.env) {
|
||||
* dist output. Those packages must not ship a foreign-architecture
|
||||
* `embedded_mpv.node` — it can never load and produces a cryptic error.
|
||||
*/
|
||||
function isForeignLinuxEmbeddedMpvArch(platform, targetArch, env = process.env) {
|
||||
function isForeignLinuxEmbeddedMpvArch(
|
||||
platform,
|
||||
targetArch,
|
||||
env = process.env
|
||||
) {
|
||||
if (normalizeEmbeddedMpvPlatform(platform) !== 'linux') {
|
||||
return false;
|
||||
}
|
||||
@@ -540,12 +717,16 @@ function validatePackagedEmbeddedMpv(resourceDir, options = {}) {
|
||||
return errors;
|
||||
}
|
||||
|
||||
if (platform === 'darwin') {
|
||||
if (platform === 'darwin' || platform === 'win32') {
|
||||
// The frame-copy engine artifacts are built by the same binding.gyp
|
||||
// run as the addon; a macOS package that ships the addon without
|
||||
// them would silently lose the engine (support probe hides it).
|
||||
// run as the addon; a macOS/Windows package that ships the addon
|
||||
// without them would silently lose the engine (support probe hides
|
||||
// it). Linux packages intentionally strip the helper until the
|
||||
// bundled-libmpv runtime lands (see electron-after-pack.cjs).
|
||||
const missingFrameCopyArtifacts = [
|
||||
'iptvnator_mpv_helper',
|
||||
platform === 'win32'
|
||||
? 'iptvnator_mpv_helper.exe'
|
||||
: 'iptvnator_mpv_helper',
|
||||
'embedded_mpv_frame_reader.node',
|
||||
]
|
||||
.map((name) => path.join(unpackedNativeDir, name))
|
||||
@@ -556,6 +737,77 @@ function validatePackagedEmbeddedMpv(resourceDir, options = {}) {
|
||||
`Missing embedded MPV frame-copy artifact: ${artifactPath}`
|
||||
)
|
||||
);
|
||||
|
||||
if (platform === 'win32') {
|
||||
// The helper is a separate executable. Windows resolves its
|
||||
// imported libmpv DLL from the executable directory, so a copy
|
||||
// under native/lib may satisfy addon bookkeeping but cannot
|
||||
// start iptvnator_mpv_helper.exe.
|
||||
const helperPath = path.join(
|
||||
unpackedNativeDir,
|
||||
'iptvnator_mpv_helper.exe'
|
||||
);
|
||||
const helperRuntimeCandidates = windowsMpvRuntimeNames.map((name) =>
|
||||
path.join(unpackedNativeDir, name)
|
||||
);
|
||||
if (
|
||||
!helperRuntimeCandidates.some((candidate) =>
|
||||
fs.existsSync(candidate)
|
||||
)
|
||||
) {
|
||||
errors.push(
|
||||
[
|
||||
`Missing bundled MPV DLL beside the Windows frame-copy helper in ${unpackedNativeDir}.`,
|
||||
'Expected one of:',
|
||||
...helperRuntimeCandidates.map(
|
||||
(candidate) => `- ${candidate}`
|
||||
),
|
||||
].join('\n')
|
||||
);
|
||||
}
|
||||
|
||||
if (fs.existsSync(helperPath)) {
|
||||
try {
|
||||
const acceptedRuntimeNames = new Set(
|
||||
windowsMpvRuntimeNames.map((name) => name.toLowerCase())
|
||||
);
|
||||
const importedRuntimeNames = readPeImportedDllNames(
|
||||
helperPath
|
||||
).filter((name) =>
|
||||
acceptedRuntimeNames.has(name.toLowerCase())
|
||||
);
|
||||
if (importedRuntimeNames.length === 0) {
|
||||
errors.push(
|
||||
`Windows frame-copy helper does not import a supported MPV DLL: ${helperPath}`
|
||||
);
|
||||
}
|
||||
|
||||
const packagedRuntimeNames = new Set(
|
||||
fs
|
||||
.readdirSync(unpackedNativeDir, {
|
||||
withFileTypes: true,
|
||||
})
|
||||
.filter((entry) => entry.isFile())
|
||||
.map((entry) => entry.name.toLowerCase())
|
||||
);
|
||||
for (const importedRuntimeName of importedRuntimeNames) {
|
||||
if (
|
||||
!packagedRuntimeNames.has(
|
||||
importedRuntimeName.toLowerCase()
|
||||
)
|
||||
) {
|
||||
errors.push(
|
||||
`Windows frame-copy helper imports ${importedRuntimeName}, but the matching DLL is missing beside it: ${path.join(unpackedNativeDir, importedRuntimeName)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(
|
||||
`Unable to inspect Windows frame-copy helper imports at ${helperPath}: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
|
||||
Reference in new issue
Block a user