/* * 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 * ) — other platforms export an empty object; the TypeScript * side gates on platform before requiring it. */ #define NAPI_VERSION 8 #include #if defined(__APPLE__) || defined(__linux__) || defined(_WIN32) #if defined(_WIN32) #include #include #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 #include #include #include #include #include #include #include #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)