mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 01:56:16 -08:00
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.
266 lines
8.8 KiB
C
266 lines
8.8 KiB
C
/*
|
|
* embedded_mpv_frame_reader — N-API reader for the frame-copy shm ring.
|
|
*
|
|
* Loaded by the Electron preload script; copies the newest complete BGRA
|
|
* frame from the helper's shared-memory ring (native/helper/frame_shm.h)
|
|
* into a caller-provided ArrayBuffer. A memcpy is mandatory: Electron's V8
|
|
* memory cage forbids external ArrayBuffers over foreign memory.
|
|
*
|
|
* Plain C N-API (no node-addon-api) so it stays ABI-stable and trivial.
|
|
* macOS and Linux (POSIX shm) plus Windows (named file mapping; binding.gyp
|
|
* compiles this file as C++ there because MSVC's C mode has no
|
|
* <stdatomic.h>) — other platforms export an empty object; the TypeScript
|
|
* side gates on platform before requiring it.
|
|
*/
|
|
#define NAPI_VERSION 8
|
|
#include <node_api.h>
|
|
|
|
#if defined(__APPLE__) || defined(__linux__) || defined(_WIN32)
|
|
|
|
#if defined(_WIN32)
|
|
|
|
#include <stdint.h>
|
|
#include <string.h>
|
|
|
|
#include "../helper/frame_shm.h"
|
|
|
|
/* frame_shm.h typed the fields as std::atomic in C++ mode; keep the shared
|
|
* atomic_load_explicit call sites below spelling-compatible. */
|
|
using std::atomic_load_explicit;
|
|
using std::memory_order_acquire;
|
|
using std::memory_order_relaxed;
|
|
|
|
#else
|
|
|
|
#include <fcntl.h>
|
|
#include <stdatomic.h>
|
|
#include <stdint.h>
|
|
#include <string.h>
|
|
#include <sys/mman.h>
|
|
#include <sys/stat.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
#include "../helper/frame_shm.h"
|
|
|
|
#endif
|
|
|
|
static FrameShmHeader* g_header = NULL;
|
|
static uint8_t* g_base = NULL;
|
|
static size_t g_size = 0;
|
|
|
|
/* Same clock as the helper's produce_time_ns/heartbeat_ns writers. */
|
|
static uint64_t now_ns(void) { return frame_shm_now_ns(); }
|
|
|
|
static void unmap_current(void) {
|
|
if (g_base) {
|
|
#if defined(_WIN32)
|
|
UnmapViewOfFile(g_base);
|
|
#else
|
|
munmap(g_base, g_size);
|
|
#endif
|
|
}
|
|
g_base = NULL;
|
|
g_header = NULL;
|
|
g_size = 0;
|
|
}
|
|
|
|
static napi_value throw_error(napi_env env, const char* message) {
|
|
napi_throw_error(env, NULL, message);
|
|
return NULL;
|
|
}
|
|
|
|
static void set_named_double(napi_env env, napi_value target, const char* key,
|
|
double value) {
|
|
napi_value wrapped;
|
|
napi_create_double(env, value, &wrapped);
|
|
napi_set_named_property(env, target, key, wrapped);
|
|
}
|
|
|
|
/* open(name) -> { width, height, stride, frameBytes, generation } */
|
|
static napi_value Open(napi_env env, napi_callback_info info) {
|
|
size_t argc = 1;
|
|
napi_value argv[1];
|
|
napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
|
|
if (argc < 1) return throw_error(env, "open(name) requires the shm name");
|
|
|
|
char name[256];
|
|
size_t written = 0;
|
|
if (napi_get_value_string_utf8(env, argv[0], name, sizeof(name),
|
|
&written) != napi_ok) {
|
|
return throw_error(env, "shm name must be a string");
|
|
}
|
|
|
|
#if defined(_WIN32)
|
|
char mapping_name[280];
|
|
frame_shm_windows_name(name, mapping_name, sizeof(mapping_name));
|
|
HANDLE mapping = OpenFileMappingA(FILE_MAP_READ, FALSE, mapping_name);
|
|
if (!mapping) return throw_error(env, "shm open failed (helper gone?)");
|
|
void* mapped = MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, 0);
|
|
/* The view holds its own reference to the section object. */
|
|
CloseHandle(mapping);
|
|
if (!mapped) return throw_error(env, "shm map failed");
|
|
MEMORY_BASIC_INFORMATION region;
|
|
if (VirtualQuery(mapped, ®ion, sizeof(region)) == 0 ||
|
|
region.RegionSize < sizeof(FrameShmHeader)) {
|
|
UnmapViewOfFile(mapped);
|
|
return throw_error(env, "shm segment too small");
|
|
}
|
|
const size_t segment_size = (size_t)region.RegionSize;
|
|
#else
|
|
const int fd = shm_open(name, O_RDONLY, 0);
|
|
if (fd < 0) return throw_error(env, "shm_open failed (helper gone?)");
|
|
struct stat segment;
|
|
if (fstat(fd, &segment) != 0 ||
|
|
segment.st_size < (off_t)sizeof(FrameShmHeader)) {
|
|
close(fd);
|
|
return throw_error(env, "shm segment too small");
|
|
}
|
|
void* mapped =
|
|
mmap(NULL, (size_t)segment.st_size, PROT_READ, MAP_SHARED, fd, 0);
|
|
close(fd);
|
|
if (mapped == MAP_FAILED) return throw_error(env, "mmap failed");
|
|
const size_t segment_size = (size_t)segment.st_size;
|
|
#endif
|
|
|
|
FrameShmHeader* header = (FrameShmHeader*)mapped;
|
|
if (header->magic != FRAME_SHM_MAGIC ||
|
|
header->version != FRAME_SHM_VERSION) {
|
|
#if defined(_WIN32)
|
|
UnmapViewOfFile(mapped);
|
|
#else
|
|
munmap(mapped, segment_size);
|
|
#endif
|
|
return throw_error(env, "frame ring not initialized yet");
|
|
}
|
|
|
|
unmap_current();
|
|
g_base = (uint8_t*)mapped;
|
|
g_size = segment_size;
|
|
g_header = header;
|
|
|
|
napi_value result;
|
|
napi_create_object(env, &result);
|
|
set_named_double(env, result, "width", header->width);
|
|
set_named_double(env, result, "height", header->height);
|
|
set_named_double(env, result, "stride", header->stride);
|
|
set_named_double(env, result, "frameBytes", (double)header->frame_bytes);
|
|
set_named_double(env, result, "generation", header->generation);
|
|
return result;
|
|
}
|
|
|
|
/* latestSeq() -> number (0 = no frame yet / not attached) */
|
|
static napi_value LatestSeq(napi_env env, napi_callback_info info) {
|
|
napi_value result;
|
|
const uint64_t seq =
|
|
g_header
|
|
? atomic_load_explicit(&g_header->latest_seq, memory_order_acquire)
|
|
: 0;
|
|
napi_create_double(env, (double)seq, &result);
|
|
return result;
|
|
}
|
|
|
|
/* copyLatest(arrayBuffer) -> { seq, ageMs, torn } | null */
|
|
static napi_value CopyLatest(napi_env env, napi_callback_info info) {
|
|
size_t argc = 1;
|
|
napi_value argv[1];
|
|
napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
|
|
if (!g_header) return throw_error(env, "call open() first");
|
|
if (argc < 1) return throw_error(env, "copyLatest(buffer) needs a buffer");
|
|
|
|
void* destination = NULL;
|
|
size_t destination_size = 0;
|
|
if (napi_get_arraybuffer_info(env, argv[0], &destination,
|
|
&destination_size) != napi_ok) {
|
|
return throw_error(env, "argument must be an ArrayBuffer");
|
|
}
|
|
if (destination_size < g_header->frame_bytes) {
|
|
return throw_error(env, "buffer smaller than one frame");
|
|
}
|
|
|
|
napi_value null_value;
|
|
napi_get_null(env, &null_value);
|
|
|
|
const uint64_t seq =
|
|
atomic_load_explicit(&g_header->latest_seq, memory_order_acquire);
|
|
if (seq == 0) return null_value;
|
|
|
|
FrameShmSlot* slot = &g_header->slots[seq % FRAME_SHM_RING_SLOTS];
|
|
if (atomic_load_explicit(&slot->seq, memory_order_acquire) != seq) {
|
|
return null_value; /* writer is racing this slot; next tick wins */
|
|
}
|
|
|
|
const uint8_t* source = g_base + g_header->data_offset +
|
|
(seq % FRAME_SHM_RING_SLOTS) *
|
|
g_header->frame_bytes;
|
|
memcpy(destination, source, (size_t)g_header->frame_bytes);
|
|
const uint64_t copied_at = now_ns();
|
|
const int torn =
|
|
atomic_load_explicit(&slot->seq, memory_order_acquire) != seq;
|
|
|
|
napi_value result;
|
|
napi_create_object(env, &result);
|
|
set_named_double(env, result, "seq", (double)seq);
|
|
set_named_double(env, result, "ageMs",
|
|
(double)(copied_at - slot->produce_time_ns) / 1e6);
|
|
napi_value torn_value;
|
|
napi_get_boolean(env, torn, &torn_value);
|
|
napi_set_named_property(env, result, "torn", torn_value);
|
|
return result;
|
|
}
|
|
|
|
/* producerAliveMs() -> ms since the helper's last heartbeat (-1 unknown) */
|
|
static napi_value ProducerAliveMs(napi_env env, napi_callback_info info) {
|
|
napi_value result;
|
|
double ms = -1;
|
|
if (g_header) {
|
|
const uint64_t heartbeat =
|
|
atomic_load_explicit(&g_header->heartbeat_ns, memory_order_relaxed);
|
|
if (heartbeat) ms = (double)(now_ns() - heartbeat) / 1e6;
|
|
}
|
|
napi_create_double(env, ms, &result);
|
|
return result;
|
|
}
|
|
|
|
/* close() -> undefined */
|
|
static napi_value Close(napi_env env, napi_callback_info info) {
|
|
unmap_current();
|
|
napi_value undefined;
|
|
napi_get_undefined(env, &undefined);
|
|
return undefined;
|
|
}
|
|
|
|
static napi_value Init(napi_env env, napi_value exports) {
|
|
const struct {
|
|
const char* name;
|
|
napi_callback fn;
|
|
} exported[] = {
|
|
{"open", Open},
|
|
{"latestSeq", LatestSeq},
|
|
{"copyLatest", CopyLatest},
|
|
{"producerAliveMs", ProducerAliveMs},
|
|
{"close", Close},
|
|
};
|
|
for (size_t i = 0; i < sizeof(exported) / sizeof(exported[0]); i++) {
|
|
napi_value fn;
|
|
napi_create_function(env, exported[i].name, NAPI_AUTO_LENGTH,
|
|
exported[i].fn, NULL, &fn);
|
|
napi_set_named_property(env, exports, exported[i].name, fn);
|
|
}
|
|
return exports;
|
|
}
|
|
|
|
#else /* no frame-copy port for this platform */
|
|
|
|
static napi_value Init(napi_env env, napi_value exports) {
|
|
/* Frame-copy is not ported to this platform yet; loading this module
|
|
* here yields an empty exports object and the TS side treats it as
|
|
* unsupported. */
|
|
(void)env;
|
|
return exports;
|
|
}
|
|
|
|
#endif
|
|
|
|
NAPI_MODULE(embedded_mpv_frame_reader, Init)
|