#include "player.h" #include "render.h" #include "velix/core.h" #include #include #include #include #include namespace { PlaybackEngine gPlayer; EglRenderer gRenderer(gPlayer); napi_value boolean(napi_env env, bool value) { napi_value result = nullptr; napi_get_boolean(env, value, &result); return result; } void setString(napi_env env, napi_value object, const char* name, const std::string& value) { napi_value item = nullptr; napi_create_string_utf8(env, value.c_str(), NAPI_AUTO_LENGTH, &item); napi_set_named_property(env, object, name, item); } void setString(napi_env env, napi_value object, const char* name, const char* value) { napi_value item = nullptr; napi_create_string_utf8(env, value == nullptr ? "" : value, NAPI_AUTO_LENGTH, &item); napi_set_named_property(env, object, name, item); } void setDouble(napi_env env, napi_value object, const char* name, double value) { napi_value item = nullptr; napi_create_double(env, value, &item); napi_set_named_property(env, object, name, item); } void setInt64(napi_env env, napi_value object, const char* name, int64_t value) { napi_value item = nullptr; napi_create_int64(env, value, &item); napi_set_named_property(env, object, name, item); } void setBool(napi_env env, napi_value object, const char* name, bool value) { napi_value item = nullptr; napi_get_boolean(env, value, &item); napi_set_named_property(env, object, name, item); } bool firstArg(napi_env env, napi_callback_info info, napi_value& value) { size_t argc = 1; napi_value argv[1] = {nullptr}; if (napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr) != napi_ok || argc == 0) return false; value = argv[0]; return true; } bool valueString(napi_env env, napi_value value, std::string& result) { size_t size = 0; if (napi_get_value_string_utf8(env, value, nullptr, 0, &size) != napi_ok) return false; result.resize(size + 1); if (napi_get_value_string_utf8(env, value, result.data(), result.size(), &size) != napi_ok) return false; result.resize(size); return true; } bool namedString(napi_env env, napi_value object, const char* name, std::string& result) { bool has = false; napi_value value = nullptr; return napi_has_named_property(env, object, name, &has) == napi_ok && has && napi_get_named_property(env, object, name, &value) == napi_ok && valueString(env, value, result); } bool namedDouble(napi_env env, napi_value object, const char* name, double& result) { bool has = false; napi_value value = nullptr; return napi_has_named_property(env, object, name, &has) == napi_ok && has && napi_get_named_property(env, object, name, &value) == napi_ok && napi_get_value_double(env, value, &result) == napi_ok; } bool namedInt(napi_env env, napi_value object, const char* name, int64_t& result) { double number = 0.0; if (!namedDouble(env, object, name, number)) return false; result = static_cast(number); return true; } bool namedBool(napi_env env, napi_value object, const char* name, bool& result) { bool has = false; napi_value value = nullptr; return napi_has_named_property(env, object, name, &has) == napi_ok && has && napi_get_named_property(env, object, name, &value) == napi_ok && napi_get_value_bool(env, value, &result) == napi_ok; } bool requestFromArg(napi_env env, napi_value argument, PlaybackRequestModel& request) { napi_valuetype type = napi_undefined; if (napi_typeof(env, argument, &type) != napi_ok) return false; if (type == napi_string) return valueString(env, argument, request.url) && !request.url.empty(); if (type != napi_object || !namedString(env, argument, "url", request.url) || request.url.empty()) return false; namedString(env, argument, "title", request.title); namedString(env, argument, "userAgent", request.userAgent); namedString(env, argument, "referer", request.referer); namedString(env, argument, "cookies", request.cookies); int64_t number = -1; if (namedInt(env, argument, "startPositionMs", number)) request.startPositionMs = number; if (namedInt(env, argument, "audioTrackId", number)) request.audioTrackId = static_cast(number); if (namedInt(env, argument, "subtitleTrackId", number)) request.subtitleTrackId = static_cast(number); double decimal = 0.0; if (namedDouble(env, argument, "playbackSpeed", decimal)) request.playbackSpeed = decimal; if (namedDouble(env, argument, "networkTimeout", decimal)) request.networkTimeout = decimal; bool enabled = true; if (namedBool(env, argument, "subtitleEnabled", enabled)) request.subtitleEnabled = enabled ? 1 : 0; napi_value headers = nullptr; bool has = false; if (napi_has_named_property(env, argument, "httpHeaders", &has) == napi_ok && has && napi_get_named_property(env, argument, "httpHeaders", &headers) == napi_ok) { uint32_t length = 0; if (napi_get_array_length(env, headers, &length) == napi_ok) { for (uint32_t index = 0; index < length && index < 32; ++index) { napi_value item = nullptr; PlaybackHeader header; if (napi_get_element(env, headers, index, &item) == napi_ok && namedString(env, item, "name", header.name) && namedString(env, item, "value", header.value)) { request.headers.push_back(std::move(header)); } } } } return true; } void serializeTrack(napi_env env, napi_value object, const PlaybackTrack& track) { setInt64(env, object, "id", track.id); setString(env, object, "type", track.type); setString(env, object, "title", track.title); setString(env, object, "language", track.language); setString(env, object, "codec", track.codec); setBool(env, object, "default", track.isDefault); setBool(env, object, "forced", track.forced); setBool(env, object, "selected", track.selected); setBool(env, object, "external", track.external); } napi_value tracksValue(napi_env env, const std::vector& tracks) { napi_value result = nullptr; napi_create_array_with_length(env, tracks.size(), &result); for (size_t index = 0; index < tracks.size(); ++index) { napi_value item = nullptr; napi_create_object(env, &item); serializeTrack(env, item, tracks[index]); napi_set_element(env, result, index, item); } return result; } napi_value mediaValue(napi_env env, const PlaybackMediaInfo& media) { napi_value result = nullptr; napi_create_object(env, &result); setString(env, result, "container", media.container); setString(env, result, "demuxer", media.demuxer); setDouble(env, result, "duration", media.duration); setString(env, result, "videoCodec", media.videoCodec); setString(env, result, "audioCodec", media.audioCodec); setString(env, result, "pixelFormat", media.pixelFormat); setString(env, result, "colorPrimaries", media.colorPrimaries); setString(env, result, "colorTransfer", media.colorTransfer); setString(env, result, "colorSpace", media.colorSpace); setString(env, result, "colorMatrix", media.colorMatrix); setInt64(env, result, "width", media.width); setInt64(env, result, "height", media.height); setInt64(env, result, "videoWidth", media.videoWidth); setInt64(env, result, "videoHeight", media.videoHeight); setInt64(env, result, "bitDepth", media.bitDepth); setInt64(env, result, "audioSampleRate", media.audioSampleRate); setInt64(env, result, "audioChannels", media.audioChannels); setString(env, result, "audioChannelLayout", media.audioChannelLayout); setDouble(env, result, "fps", media.fps); setInt64(env, result, "bitrate", media.bitrate); setInt64(env, result, "videoBitrate", media.videoBitrate); setInt64(env, result, "audioBitrate", media.audioBitrate); setInt64(env, result, "trackCount", media.trackCount); setInt64(env, result, "subtitleCount", media.subtitleCount); return result; } napi_value metricsValue(napi_env env, const PlaybackSnapshot& state) { napi_value result = nullptr; napi_create_object(env, &result); setInt64(env, result, "droppedFrames", state.dropped); setInt64(env, result, "renderCount", static_cast(state.renderCount)); setBool(env, result, "buffering", state.network.buffering); setInt64(env, result, "bufferingEnterCount", state.metrics.buffering_enter_count); setInt64(env, result, "bufferingExitCount", state.metrics.buffering_exit_count); setInt64(env, result, "bufferingTotalMs", state.metrics.buffering_total_ms); setInt64(env, result, "bufferingMaxSingleMs", state.metrics.buffering_max_single_ms); setInt64(env, result, "networkWaitMs", state.metrics.network_wait_ms); setInt64(env, result, "lateFrames", state.metrics.late_frames); setInt64(env, result, "mistimedFrames", state.metrics.mistimed_frames); setInt64(env, result, "decoderFrames", state.metrics.decoder_frames); setInt64(env, result, "cacheBytes", state.network.cacheBytes); setDouble(env, result, "cacheDuration", state.network.cacheDuration); setInt64(env, result, "currentBitrate", state.network.currentBitrate); return result; } napi_value stateValue(napi_env env) { const PlaybackSnapshot state = gPlayer.snapshot(); napi_value result = nullptr; napi_create_object(env, &result); setString(env, result, "state", state.state); setInt64(env, result, "sessionId", static_cast(state.sessionId)); setDouble(env, result, "position", state.position); setDouble(env, result, "duration", state.duration); setDouble(env, result, "fps", state.fps); setInt64(env, result, "dropped", state.dropped); setInt64(env, result, "firstFrameMs", state.firstFrameMs); setInt64(env, result, "renderCount", static_cast(state.renderCount)); setString(env, result, "lastError", state.lastError); setString(env, result, "errorCode", state.errorCode); setDouble(env, result, "speed", state.speed); setDouble(env, result, "volume", state.volume); setBool(env, result, "muted", state.muted); setInt64(env, result, "subtitleDelayMs", state.subtitleDelayMs); setDouble(env, result, "subtitleScale", state.subtitleScale); setDouble(env, result, "subtitlePosition", state.subtitlePosition); setString(env, result, "requestedDecoder", state.requestedDecoder); setString(env, result, "actualDecoder", state.actualDecoder); setString(env, result, "hwCapability", state.hwCapability); setBool(env, result, "hwdecRequested", state.hwdecRequested); setBool(env, result, "hwdecActive", state.hwdecActive); setInt64(env, result, "hardwareFrameCount", state.hardwareFrameCount); setInt64(env, result, "softwareFrameCount", state.softwareFrameCount); setInt64(env, result, "hwFallbackCount", state.hwFallbackCount); setInt64(env, result, "nativeBufferImportCount", state.nativeBufferImportCount); setInt64(env, result, "nativeBufferImportFailure", state.nativeBufferImportFailure); setInt64(env, result, "surfacePresentCount", state.surfacePresentCount); setInt64(env, result, "gpuInteropFrameCount", state.gpuInteropFrameCount); setString(env, result, "surface", gRenderer.surfaceStatus()); setString(env, result, "egl", gRenderer.eglStatus()); setString(env, result, "mpv", gRenderer.mpvStatus()); napi_set_named_property(env, result, "media", mediaValue(env, state.media)); napi_set_named_property(env, result, "tracks", tracksValue(env, state.tracks)); napi_set_named_property(env, result, "metrics", metricsValue(env, state)); return result; } napi_value createPlayer(napi_env env, napi_callback_info) { return boolean(env, gPlayer.initialize()); } napi_value prepare(napi_env env, napi_callback_info info) { napi_value argument = nullptr; PlaybackRequestModel request; return firstArg(env, info, argument) && requestFromArg(env, argument, request) ? boolean(env, gPlayer.load(request)) : boolean(env, false); } napi_value play(napi_env env, napi_callback_info) { gPlayer.play(); return boolean(env, true); } napi_value pause(napi_env env, napi_callback_info) { gPlayer.pause(); return boolean(env, true); } napi_value stop(napi_env env, napi_callback_info) { gPlayer.stop(); return boolean(env, true); } napi_value release(napi_env env, napi_callback_info) { gRenderer.onSurfaceDestroyed(nullptr); gPlayer.release(); return boolean(env, true); } napi_value attachSurface(napi_env env, napi_callback_info) { gRenderer.requestRender(); return boolean(env, gRenderer.hasSurface()); } napi_value detachSurface(napi_env env, napi_callback_info) { gRenderer.onSurfaceDestroyed(nullptr); return boolean(env, true); } napi_value onForeground(napi_env env, napi_callback_info) { gRenderer.requestRender(); return boolean(env, true); } napi_value onBackground(napi_env env, napi_callback_info) { return boolean(env, true); } napi_value onSurfaceAvailable(napi_env env, napi_callback_info) { gRenderer.requestRender(); return boolean(env, true); } napi_value onSurfaceDestroyed(napi_env env, napi_callback_info) { gRenderer.onSurfaceDestroyed(nullptr); return boolean(env, true); } napi_value seekTo(napi_env env, napi_callback_info info) { napi_value argument = nullptr; double milliseconds = 0.0; if (!firstArg(env, info, argument) || napi_get_value_double(env, argument, &milliseconds) != napi_ok) { return boolean(env, false); } return boolean(env, gPlayer.seekAbsolute(milliseconds / 1000.0, false, false)); } napi_value seekBy(napi_env env, napi_callback_info info) { napi_value argument = nullptr; double milliseconds = 0.0; if (!firstArg(env, info, argument) || napi_get_value_double(env, argument, &milliseconds) != napi_ok) { return boolean(env, false); } gPlayer.seek(milliseconds / 1000.0); return boolean(env, true); } napi_value setSpeed(napi_env env, napi_callback_info info) { napi_value argument = nullptr; double value = 1.0; return firstArg(env, info, argument) && napi_get_value_double(env, argument, &value) == napi_ok ? boolean(env, gPlayer.setSpeed(value)) : boolean(env, false); } napi_value setVolume(napi_env env, napi_callback_info info) { napi_value argument = nullptr; double value = 100.0; return firstArg(env, info, argument) && napi_get_value_double(env, argument, &value) == napi_ok ? boolean(env, gPlayer.setVolume(value)) : boolean(env, false); } napi_value setMute(napi_env env, napi_callback_info info) { napi_value argument = nullptr; bool value = false; return firstArg(env, info, argument) && napi_get_value_bool(env, argument, &value) == napi_ok ? boolean(env, gPlayer.setMute(value)) : boolean(env, false); } napi_value selectAudioTrack(napi_env env, napi_callback_info info) { napi_value argument = nullptr; double value = -1; return firstArg(env, info, argument) && napi_get_value_double(env, argument, &value) == napi_ok ? boolean(env, gPlayer.selectAudioTrack(static_cast(value))) : boolean(env, false); } napi_value selectSubtitleTrack(napi_env env, napi_callback_info info) { napi_value argument = nullptr; double value = -1; return firstArg(env, info, argument) && napi_get_value_double(env, argument, &value) == napi_ok ? boolean(env, gPlayer.selectSubtitleTrack(static_cast(value))) : boolean(env, false); } napi_value disableSubtitle(napi_env env, napi_callback_info) { return boolean(env, gPlayer.disableSubtitle()); } napi_value addSubtitle(napi_env env, napi_callback_info info) { napi_value argument = nullptr; if (!firstArg(env, info, argument)) return boolean(env, false); PlaybackSubtitleSource source; napi_valuetype type = napi_undefined; if (napi_typeof(env, argument, &type) != napi_ok) return boolean(env, false); if (type == napi_string) { if (!valueString(env, argument, source.url)) return boolean(env, false); } else if (type == napi_object) { namedString(env, argument, "url", source.url); namedString(env, argument, "title", source.title); namedString(env, argument, "language", source.language); namedString(env, argument, "encoding", source.encoding); namedBool(env, argument, "forced", source.forced); namedBool(env, argument, "select", source.select); } return boolean(env, !source.url.empty() && gPlayer.addSubtitle(source)); } napi_value removeSubtitle(napi_env env, napi_callback_info info) { napi_value argument = nullptr; double value = -1; return firstArg(env, info, argument) && napi_get_value_double(env, argument, &value) == napi_ok ? boolean(env, gPlayer.removeSubtitle(static_cast(value))) : boolean(env, false); } napi_value setSubtitleDelay(napi_env env, napi_callback_info info) { napi_value argument = nullptr; double value = 0.0; return firstArg(env, info, argument) && napi_get_value_double(env, argument, &value) == napi_ok ? boolean(env, gPlayer.setSubtitleDelay(value)) : boolean(env, false); } napi_value setSubtitleScale(napi_env env, napi_callback_info info) { napi_value argument = nullptr; double value = 1.0; return firstArg(env, info, argument) && napi_get_value_double(env, argument, &value) == napi_ok ? boolean(env, gPlayer.setSubtitleScale(value)) : boolean(env, false); } napi_value setSubtitlePosition(napi_env env, napi_callback_info info) { napi_value argument = nullptr; double value = 100.0; return firstArg(env, info, argument) && napi_get_value_double(env, argument, &value) == napi_ok ? boolean(env, gPlayer.setSubtitlePosition(value)) : boolean(env, false); } napi_value getState(napi_env env, napi_callback_info) { return stateValue(env); } napi_value getMediaInfo(napi_env env, napi_callback_info) { return mediaValue(env, gPlayer.snapshot().media); } napi_value getTracks(napi_env env, napi_callback_info) { return tracksValue(env, gPlayer.snapshot().tracks); } napi_value getMetrics(napi_env env, napi_callback_info) { return metricsValue(env, gPlayer.snapshot()); } napi_value getBuildInfo(napi_env env, napi_callback_info) { const velix_core_build_info* info = velix_core_get_build_info(); napi_value result = nullptr; napi_create_object(env, &result); setInt64(env, result, "apiVersion", info->api_version); setString(env, result, "sdkVersion", info->sdk_version); setString(env, result, "mpvVersion", info->mpv_version); setString(env, result, "mpvCommit", info->mpv_commit); setString(env, result, "ffmpegCommit", info->ffmpeg_commit); setString(env, result, "ffmpegHarmonyCommit", info->ffmpeg_harmony_commit); setString(env, result, "buildDate", info->build_date); setString(env, result, "target", info->target); setString(env, result, "architecture", info->architecture); setString(env, result, "profile", info->profile); setString(env, result, "featureFlags", info->feature_flags); return result; } void onSurfaceCreated(OH_NativeXComponent* component, void* window) { gRenderer.onSurfaceCreated(component, window); } void onSurfaceChanged(OH_NativeXComponent* component, void* window) { gRenderer.onSurfaceChanged(component, window); } void onSurfaceDestroyedCallback(OH_NativeXComponent*, void* window) { gRenderer.onSurfaceDestroyed(window); } void onTouch(OH_NativeXComponent*, void*) {} napi_value Init(napi_env env, napi_value exports) { gPlayer.initialize(); napi_value xcomponent = nullptr; if (napi_get_named_property(env, exports, OH_NATIVE_XCOMPONENT_OBJ, &xcomponent) == napi_ok) { OH_NativeXComponent* component = nullptr; if (napi_unwrap(env, xcomponent, reinterpret_cast(&component)) == napi_ok && component != nullptr) { static OH_NativeXComponent_Callback callbacks = { onSurfaceCreated, onSurfaceChanged, onSurfaceDestroyedCallback, onTouch }; OH_NativeXComponent_RegisterCallback(component, &callbacks); } } napi_property_descriptor properties[] = { {"createPlayer", nullptr, createPlayer, nullptr, nullptr, nullptr, napi_default, nullptr}, {"prepare", nullptr, prepare, nullptr, nullptr, nullptr, napi_default, nullptr}, {"play", nullptr, play, nullptr, nullptr, nullptr, napi_default, nullptr}, {"pause", nullptr, pause, nullptr, nullptr, nullptr, napi_default, nullptr}, {"stop", nullptr, stop, nullptr, nullptr, nullptr, napi_default, nullptr}, {"release", nullptr, release, nullptr, nullptr, nullptr, napi_default, nullptr}, {"attachSurface", nullptr, attachSurface, nullptr, nullptr, nullptr, napi_default, nullptr}, {"detachSurface", nullptr, detachSurface, nullptr, nullptr, nullptr, napi_default, nullptr}, {"onForeground", nullptr, onForeground, nullptr, nullptr, nullptr, napi_default, nullptr}, {"onBackground", nullptr, onBackground, nullptr, nullptr, nullptr, napi_default, nullptr}, {"onSurfaceAvailable", nullptr, onSurfaceAvailable, nullptr, nullptr, nullptr, napi_default, nullptr}, {"onSurfaceDestroyed", nullptr, onSurfaceDestroyed, nullptr, nullptr, nullptr, napi_default, nullptr}, {"seekTo", nullptr, seekTo, nullptr, nullptr, nullptr, napi_default, nullptr}, {"seekBy", nullptr, seekBy, nullptr, nullptr, nullptr, napi_default, nullptr}, {"setSpeed", nullptr, setSpeed, nullptr, nullptr, nullptr, napi_default, nullptr}, {"setVolume", nullptr, setVolume, nullptr, nullptr, nullptr, napi_default, nullptr}, {"setMute", nullptr, setMute, nullptr, nullptr, nullptr, napi_default, nullptr}, {"selectAudioTrack", nullptr, selectAudioTrack, nullptr, nullptr, nullptr, napi_default, nullptr}, {"selectSubtitleTrack", nullptr, selectSubtitleTrack, nullptr, nullptr, nullptr, napi_default, nullptr}, {"disableSubtitle", nullptr, disableSubtitle, nullptr, nullptr, nullptr, napi_default, nullptr}, {"addSubtitle", nullptr, addSubtitle, nullptr, nullptr, nullptr, napi_default, nullptr}, {"removeSubtitle", nullptr, removeSubtitle, nullptr, nullptr, nullptr, napi_default, nullptr}, {"setSubtitleDelay", nullptr, setSubtitleDelay, nullptr, nullptr, nullptr, napi_default, nullptr}, {"setSubtitleScale", nullptr, setSubtitleScale, nullptr, nullptr, nullptr, napi_default, nullptr}, {"setSubtitlePosition", nullptr, setSubtitlePosition, nullptr, nullptr, nullptr, napi_default, nullptr}, {"getState", nullptr, getState, nullptr, nullptr, nullptr, napi_default, nullptr}, {"getMediaInfo", nullptr, getMediaInfo, nullptr, nullptr, nullptr, napi_default, nullptr}, {"getTracks", nullptr, getTracks, nullptr, nullptr, nullptr, napi_default, nullptr}, {"getMetrics", nullptr, getMetrics, nullptr, nullptr, nullptr, napi_default, nullptr}, {"getBuildInfo", nullptr, getBuildInfo, nullptr, nullptr, nullptr, napi_default, nullptr}, }; napi_define_properties(env, exports, sizeof(properties) / sizeof(properties[0]), properties); return exports; } } // namespace NAPI_MODULE(velix_core_binding, Init)