#include "bridge.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace ins_camera; namespace { constexpr size_t MAX_PACKET = 4 * 1024 * 1024; constexpr size_t MAX_QUEUE = 8 * 1024 * 1024; constexpr size_t MAX_RESULT = 60 * 1024; std::string quote(const std::string& value) { if (value.size() > 1024) throw std::runtime_error("oversized SDK string"); std::ostringstream out; out << '"'; for (unsigned char c : value) { if (c == '"' || c == '\\') out << '\\' << c; else if (c < 32) out << "\\u" << std::hex << std::setw(4) << std::setfill('0') << int(c); else out << c; } return out.str() + '"'; } template bool contains(const std::vector& values, T value) { return std::find(values.begin(), values.end(), value) != values.end(); } template std::string numbers(const std::vector& values) { if (values.size() > 256) throw std::runtime_error("oversized capabilities"); std::string out = "["; for (auto v : values) { if (out.size() > 1) out += ','; out += std::to_string(int(v)); } return out + ']'; } std::string strings(const std::vector& values) { if (values.size() > 256) throw std::runtime_error("oversized SDK list"); std::string out = "["; for (const auto& v : values) { if (out.size() > 1) out += ','; out += quote(v); } return out + ']'; } std::string complete(const std::string& result) { return "{\"state\":\"complete\",\"result\":" + result + '}'; } const char* UNKNOWN = "{\"state\":\"unknown\",\"error\":\"camera_result_unconfirmed\"}"; const char* INVALID = "{\"state\":\"error\",\"error\":\"unsupported_camera_parameter\"}"; struct Packet { mc_x4_video info; std::vector bytes; }; struct Frames final : StreamDelegate { std::mutex mutex; std::deque queue; size_t bytes = 0; uint64_t generation = 0, sequence = 0; std::atomic codec{0}; std::atomic enabled{false}; void reset() { std::lock_guard lock(mutex); queue.clear(); bytes = 0; ++generation; } void OnVideoData(const uint8_t* data, size_t size, int64_t timestamp, uint8_t, int stream_index) override { if (!enabled || !data || !size || size > MAX_PACKET || stream_index < 0 || stream_index > 1) return; // SDK owns the callback bytes. Copy into a bounded queue; never block // a vendor callback on a browser, disk, decoder or Python interpreter. try { std::lock_guard lock(mutex); if (bytes + size > MAX_QUEUE || queue.size() >= 64) { queue.clear(); bytes = 0; ++generation; } queue.push_back({{++sequence, generation, timestamp, stream_index, codec.load(), uint32_t(size)}, {data, data + size}}); bytes += size; } catch (...) { // A memory allocation failure drops data; it must not unwind into // the vendor's callback thread or terminate camera recording. } } void OnAudioData(const uint8_t*, size_t, int64_t) override {} void OnGyroData(const std::vector&) override {} void OnExposureData(const ExposureData&) override {} }; struct Notifications { std::mutex mutex; std::atomic recording{-1}; std::atomic revision{0}; std::atomic stopped_reason{-1}; std::atomic battery_low{false}, storage_full{false}, temperature_high{false}; }; } struct mc_x4 { DeviceDiscovery discovery; std::vector descriptors; std::shared_ptr camera; std::shared_ptr frames = std::make_shared(); std::shared_ptr notifications = std::make_shared(); std::string firmware, result; bool opened = false; int preview = 0; // Only the isolated worker calls control methods; callbacks use separate // shared state that remains alive until the vendor releases its callbacks. ~mc_x4() { frames->enabled = false; if (camera && opened) { try { camera->Close(); } catch (...) {} } camera.reset(); try { discovery.FreeDeviceDescriptors(descriptors); } catch (...) {} } bool mode_supported(CameraFunctionMode mode) { return contains(camera->GetSupportedVideoModes(), mode) || contains(camera->GetSupportedPhotoModes(), mode); } std::string status() { BatteryStatus battery{}; StorageStatus storage{}; bool battery_ok = camera->GetBatteryStatus(battery); bool storage_ok = camera->GetStorageState(storage); // The SDK exposes a boolean capture poll, not a separate timeout code. // Report its value only while independent status reads succeed; never // replace a notification that arrived during the blocking poll. auto revision = notifications->revision.load(); bool active = camera->CaptureCurrentStatus(); bool connected = camera->IsConnected(); { std::lock_guard lock(notifications->mutex); if (revision == notifications->revision.load()) { notifications->recording = connected && battery_ok && storage_ok ? (active ? 1 : 0) : -1; } } std::string result = "{\"firmware\":" + quote(firmware) + ",\"connected\":" + (connected ? "true" : "false") + ",\"preview\":" + std::to_string(preview) + ",\"recording\":" + std::to_string(notifications->recording) + ",\"stopped_reason\":" + std::to_string(notifications->stopped_reason) + ",\"function_mode\":" + std::to_string(int(camera->GetCurrentFunctionMode())) + ",\"codec\":" + std::to_string(int(camera->GetVideoEncodeType())) + ",\"battery\":"; result += battery_ok ? "{\"level\":" + std::to_string(battery.battery_level) + ",\"scale\":" + std::to_string(battery.battery_scale) + ",\"power_type\":" + std::to_string(int(battery.power_type)) + "}" : "null"; result += ",\"storage\":"; result += storage_ok ? "{\"state\":" + std::to_string(int(storage.state)) + ",\"free_bytes\":" + std::to_string(storage.free_space) + ",\"total_bytes\":" + std::to_string(storage.total_space) + "}" : "null"; result += ",\"alerts\":{\"battery_low\":" + std::string(notifications->battery_low ? "true" : "false") + ",\"storage_full\":" + (notifications->storage_full ? "true" : "false") + ",\"temperature_high\":" + (notifications->temperature_high ? "true" : "false") + "}}"; return complete(result); } std::string settings(CameraFunctionMode mode) { if (!mode_supported(mode)) return INVALID; if (!camera->SyncPhotographyOptions(mode)) return UNKNOWN; auto capture = camera->GetCaptureSettings(mode); auto exposure = camera->GetExposureSettings(mode); if (!capture || !exposure || !std::isfinite(exposure->ShutterSpeed())) return UNKNOWN; std::ostringstream out; out.imbue(std::locale::classic()); out << "{\"mode\":" << int(mode) << ",\"photo_modes\":" << numbers(camera->GetSupportedPhotoModes()) << ",\"video_modes\":" << numbers(camera->GetSupportedVideoModes()) << ",\"photo_sizes\":" << numbers(camera->GetSupportedPhotoSizes(mode)) << ",\"video_resolutions\":["; auto resolutions = camera->GetSupportedVideoResolutions(mode); if (resolutions.size() > 256) return UNKNOWN; bool first = true; for (auto resolution : resolutions) { if (!first) out << ','; first = false; out << "{\"id\":" << int(resolution) << ",\"name\":" << quote(camera->GetVideoResolutionName(resolution)) << '}'; } out << "],\"values\":{\"iso\":" << exposure->Iso() << ",\"shutter_seconds\":" << std::setprecision(17) << exposure->ShutterSpeed() << ",\"exposure_mode\":" << int(exposure->ExposureMode()) << ",\"ev_twentieths\":" << exposure->EVBias() << ",\"white_balance\":" << capture->GetIntValue(CaptureSettings::CaptureSettings_WhiteBalance) << ",\"video_resolution\":" << capture->GetIntValue(CaptureSettings::CaptureSettings_RecordResolution) << ",\"photo_size\":" << capture->GetIntValue(CaptureSettings::CaptureSettings_PhotoResolution) << "},\"attributes\":{"; auto names = camera->GetSupportedAttrNames(mode); if (names.size() > 128) return UNKNOWN; first = true; for (const auto& name : names) { if (!first) out << ','; first = false; out << quote(name) << ":{\"values\":" << strings(camera->GetSupportedAttrValues(mode, name)) << ",\"depends_on\":" << strings(camera->GetAttrDependOn(mode, name)) << '}'; } return complete(out.str() + "}}"); } std::string apply(CameraFunctionMode mode, const std::string& key, double value) { if (!mode_supported(mode) || !std::isfinite(value)) return INVALID; status(); if (notifications->recording != 0 || camera->CaptureCurrentStatus()) return INVALID; // One parameter per operation: partial application of a multi-setting // request must never be presented as an atomic success. if (!camera->SyncPhotographyOptions(mode)) return UNKNOWN; bool acknowledged = false; if (key == "function_mode") { if (value == FUNCTION_MODE_NORMAL_VIDEO && contains(camera->GetSupportedVideoModes(), FUNCTION_MODE_NORMAL_VIDEO)) acknowledged = camera->SetVideoSubMode(VIDEO_NORMAL); else if (value == FUNCTION_MODE_NORMAL_IMAGE && contains(camera->GetSupportedPhotoModes(), FUNCTION_MODE_NORMAL_IMAGE)) acknowledged = camera->SetPhotoSubMode(PHOTO_SINGLE); else return INVALID; if (!acknowledged || int(camera->GetCurrentFunctionMode()) != int(value)) return UNKNOWN; return settings(static_cast(int(value))); } else if (key == "video_resolution" && value == std::floor(value) && value >= 0 && value <= 4096) { auto resolution = static_cast(int(value)); if (!contains(camera->GetSupportedVideoResolutions(mode), resolution)) return INVALID; RecordParams params{}; params.resolution = resolution; acknowledged = camera->SetVideoCaptureParams(params, mode); } else if (key == "photo_size" && value == std::floor(value) && value >= 0 && value <= 4096) { auto size = static_cast(int(value)); if (!contains(camera->GetSupportedPhotoSizes(mode), size)) return INVALID; acknowledged = camera->SetPhotoSize(mode, size); } else { // Exposure/white-balance ranges must come from this camera's // capability table. No undocumented enum or guessed write. const std::string attr = key == "iso" ? "exposure_iso" : key == "white_balance" ? "white_balance" : key == "exposure_mode" ? "exposure_program" : ""; if (attr.empty() || value != std::floor(value) || value < 0 || value > 65535) return INVALID; std::string context; for (const auto& dependency : camera->GetAttrDependOn(mode, attr)) { // Resolve the current camera state, never a client-provided // context that could widen a dependent capability branch. auto current = camera->GetCaptureSettings(mode); if (!current || dependency != "record_resolution") return INVALID; if (!context.empty()) context += '|'; context += camera->GetVideoResolutionName(static_cast(current->GetIntValue(CaptureSettings::CaptureSettings_RecordResolution))); if (context.empty()) return INVALID; } auto values = camera->GetSupportedAttrValues(mode, attr, context); bool admitted = false; for (const auto& option : values) { if (option == std::to_string(int(value)) || camera->GetAttrValueByName(attr, option) == int(value)) admitted = true; } if (!admitted) return INVALID; if (key == "iso") { auto settings = camera->GetExposureSettings(mode); if (!settings || (settings->ExposureMode() != MANUAL && settings->ExposureMode() != ISO_PRIORITY)) return INVALID; settings->SetIso(int(value)); acknowledged = camera->SetExposureSettings(mode, settings); } else if (key == "exposure_mode") { if (value > 5) return INVALID; auto settings = camera->GetExposureSettings(mode); if (!settings) return UNKNOWN; settings->SetExposureMode(static_cast(int(value))); acknowledged = camera->SetExposureSettings(mode, settings); } else { auto settings = camera->GetCaptureSettings(mode); if (!settings) return UNKNOWN; settings->ResetSettingTypes(); // X4 capabilities and the pinned SDK example use Kelvin (0 // auto) via SetValue, not the legacy 0..5 WB enum. settings->SetValue(CaptureSettings::CaptureSettings_WhiteBalance, int(value)); acknowledged = camera->SetCaptureSettings(mode, settings); } } // Returning the actual refreshed values lets the caller distinguish an // acknowledged request from a setting the camera declined to retain. return acknowledged ? settings(mode) : UNKNOWN; } std::string call(int action, int mode_number, const std::string& key, double value) { if (mode_number < 0 || mode_number > 255 || !std::isfinite(value)) return INVALID; if (!camera->IsConnected()) return UNKNOWN; auto mode = static_cast(mode_number); if (action == MC_X4_STATUS) return status(); if (action == MC_X4_SETTINGS_READ) return settings(mode); if (action == MC_X4_SETTINGS_APPLY) return apply(mode, key, value); if (action == MC_X4_PREVIEW_START) { if (preview == 1) return complete("{\"ok\":true}"); if (preview == -1) return UNKNOWN; LiveStreamParam params{}; params.enable_audio = false; params.enable_gyro = false; params.using_lrv = false; params.video_resolution = RES_1920_960P30; params.lrv_video_resulution = RES_1920_960P30; params.video_bitrate = 4000000; frames->reset(); frames->codec = int(camera->GetVideoEncodeType()); frames->enabled = true; preview = -1; bool ok = camera->StartLiveStreaming(params); frames->codec = int(camera->GetVideoEncodeType()); if (!ok) { frames->enabled = false; return UNKNOWN; } preview = 1; return complete("{\"ok\":true}"); } if (action == MC_X4_PREVIEW_STOP) { if (preview == 0) return complete("{\"ok\":true}"); preview = -1; if (!camera->StopLiveStreaming()) return UNKNOWN; preview = 0; frames->enabled = false; frames->reset(); return complete("{\"ok\":true}"); } if (action == MC_X4_RECORD_START) { if (camera->CaptureCurrentStatus()) { notifications->recording = 1; return complete("{\"recording\":true}"); } if (camera->GetCurrentFunctionMode() != FUNCTION_MODE_NORMAL_VIDEO) return INVALID; uint64_t revision; { std::lock_guard lock(notifications->mutex); revision = notifications->revision; notifications->recording = -1; } if (!camera->StartRecording()) return UNKNOWN; { std::lock_guard lock(notifications->mutex); // A storage/temperature stop notification can precede the // START acknowledgement. Never overwrite that newer fact. if (notifications->revision == revision) notifications->recording = 1; if (notifications->recording != 1) return UNKNOWN; } return complete("{\"recording\":true}"); } if (action == MC_X4_RECORD_STOP) { uint64_t revision; { std::lock_guard lock(notifications->mutex); revision = notifications->revision; notifications->recording = -1; } auto media = camera->StopRecording(); if (media.Empty()) return UNKNOWN; { std::lock_guard lock(notifications->mutex); if (notifications->revision == revision) notifications->recording = 0; if (notifications->recording != 0) return UNKNOWN; } return complete("{\"recording\":false,\"files\":" + strings(media.OriginUrls()) + '}'); } if (action == MC_X4_PHOTO) { status(); if (notifications->recording != 0 || camera->CaptureCurrentStatus()) return INVALID; // Capture the operator-selected mode. Initialization never changes // it or takes a photo as a side effect of verifying live frames. if (camera->GetCurrentFunctionMode() != FUNCTION_MODE_NORMAL_IMAGE) return INVALID; auto media = camera->TakePhoto(RawCaptureType::PureShot, 15000); return media.Empty() ? UNKNOWN : complete("{\"files\":" + strings(media.OriginUrls()) + '}'); } if (action == MC_X4_FILES) { if (value < 0 || value > 100000 || value != std::floor(value)) return INVALID; auto files = camera->GetCameraFilesList(); size_t offset = size_t(value), stop = std::min(files.size(), offset + 32); if (files.size() > 100000 || offset > files.size()) return INVALID; std::vector page(files.begin() + offset, files.begin() + stop); return complete("{\"items\":" + strings(page) + ",\"total\":" + std::to_string(files.size()) + ",\"offset\":" + std::to_string(offset) + '}'); } return INVALID; } }; extern "C" { int mc_x4_abi(void) { return 1; } mc_x4* mc_x4_open(const char* serial, const char* log_directory) { try { if (!serial || !*serial || std::strlen(serial) > 256 || !log_directory || !*log_directory) return nullptr; auto handle = std::make_unique(); SetLogPath(log_directory); SetLogLevel(LogLevel::ERR); handle->descriptors = handle->discovery.GetAvailableDevices(); // The service namespace must expose exactly one physical camera. // Never take list[0] from a host-wide SDK enumeration. if (handle->descriptors.size() != 1) return nullptr; const auto& device = handle->descriptors.front(); if (device.camera_type != CameraType::Insta360X4 || device.info.connection_type != ConnectionType::USB || device.serial_number != serial) return nullptr; handle->firmware = device.fw_version; handle->camera = std::make_shared(device.info); handle->camera->SetServicePort(9099); // private network namespace per instance handle->camera->SetTimeout(10000); handle->opened = handle->camera->Open(); if (!handle->opened) return nullptr; auto n = handle->notifications; handle->camera->SetCaptureStateNotification([n](bool active) { std::lock_guard lock(n->mutex); n->recording = active ? 1 : 0; ++n->revision; }); handle->camera->SetCaptureStoppedNotification([n](const std::string&, int reason) { std::lock_guard lock(n->mutex); n->recording = 0; n->stopped_reason = reason; ++n->revision; }); handle->camera->SetBatteryLowNotification([n](int) { n->battery_low = true; }); handle->camera->SetStorageFullNotification([n]() { n->storage_full = true; }); handle->camera->SetTemperatureHighNotification([n]() { n->temperature_high = true; }); std::shared_ptr delegate = handle->frames; handle->camera->SetStreamDelegate(delegate); return handle.release(); } catch (...) { return nullptr; } } const char* mc_x4_call(mc_x4* handle, int action, int mode, const char* key, double value) { if (!handle) return UNKNOWN; try { if (key && std::strlen(key) > 64) return INVALID; handle->result = handle->call(action, mode, key ? key : "", value); if (handle->result.size() > MAX_RESULT) handle->result = UNKNOWN; return handle->result.c_str(); } catch (...) { return UNKNOWN; } } int mc_x4_read_video(mc_x4* handle, mc_x4_video* info, uint8_t* buffer, size_t capacity) { if (!handle || !info || !buffer) return -1; try { std::lock_guard lock(handle->frames->mutex); if (handle->frames->queue.empty()) return 0; auto& packet = handle->frames->queue.front(); if (capacity < packet.bytes.size()) return -1; *info = packet.info; std::memcpy(buffer, packet.bytes.data(), packet.bytes.size()); handle->frames->bytes -= packet.bytes.size(); handle->frames->queue.pop_front(); return 1; } catch (...) { return -1; } } void mc_x4_close(mc_x4* handle) { try { delete handle; } catch (...) {} } }