# Java Quickstart (Linux)

Use the SmartSpectra C++ SDK from Java via JNI to measure pulse and breathing rate from a webcam.

**Platform:** Ubuntu 22.04 / Linux Mint 21 (x86_64) only

**Requirements:** JDK 11+, webcam, API key from [physiology.presagetech.com](https://physiology.presagetech.com)

> **Looking for Android?** See the [Kotlin quickstart](/docs/quickstart-kotlin) — the Android SDK has a native Kotlin/Java API and does not require this JNI approach.

> **Note:** There is no Maven/Gradle package for SmartSpectra on desktop. This guide builds a JNI bridge around the C++ SDK.

---

## 1. Install the C++ SDK and JDK

```bash
sudo apt update
sudo apt install -y build-essential libcurl4-openssl-dev libssl-dev \
  pkg-config libv4l-dev libgles2-mesa-dev libunwind-dev gpg curl \
  openjdk-17-jdk libopencv-dev

# CMake 3.27+
curl -L -o cmake-3.27.0-linux-x86_64.sh \
  https://github.com/Kitware/CMake/releases/download/v3.27.0/cmake-3.27.0-linux-x86_64.sh
chmod +x cmake-3.27.0-linux-x86_64.sh
sudo ./cmake-3.27.0-linux-x86_64.sh --skip-license --prefix=/usr/local

# Presage PPA
curl -s "https://presage-security.github.io/PPA/KEY.gpg" | gpg --dearmor | \
  sudo tee /etc/apt/trusted.gpg.d/presage-technologies.gpg >/dev/null
sudo curl -s --compressed -o /etc/apt/sources.list.d/presage-technologies.list \
  "https://presage-security.github.io/PPA/presage-technologies.list"
sudo apt update
sudo apt install libsmartspectra-dev
```

## 2. Create the Project

```bash
mkdir smartspectra-java && cd smartspectra-java
```

### Java Class

**SmartSpectra.java:**

```java
public class SmartSpectra {

    static {
        System.loadLibrary("smartspectra_jni");
    }

    // Native methods (implemented in C++)
    private static native long nativeCreate(String apiKey, int cameraIndex);
    private static native int nativeInitialize(long handle);
    private static native int nativeRun(long handle);
    private static native void nativeStop(long handle);
    private static native float[] nativeGetMetrics(long handle);
    private static native void nativeDestroy(long handle);

    private long handle;

    public SmartSpectra(String apiKey) {
        this(apiKey, 0);
    }

    public SmartSpectra(String apiKey, int cameraIndex) {
        this.handle = nativeCreate(apiKey, cameraIndex);
        if (this.handle == 0) {
            throw new RuntimeException("Failed to create SmartSpectra instance");
        }
    }

    public void initialize() {
        if (nativeInitialize(handle) != 0) {
            throw new RuntimeException("Failed to initialize SmartSpectra");
        }
    }

    /** Starts processing. Blocks the calling thread. */
    public void run() {
        nativeRun(handle);
    }

    public void stop() {
        nativeStop(handle);
    }

    /**
     * Returns [pulseRate, breathingRate] or null if no data yet.
     */
    public float[] getMetrics() {
        return nativeGetMetrics(handle);
    }

    public void destroy() {
        if (handle != 0) {
            nativeDestroy(handle);
            handle = 0;
        }
    }
}
```

### Hello World App

**HelloVitals.java:**

```java
public class HelloVitals {

    public static void main(String[] args) {
        String apiKey = args.length > 0
            ? args[0]
            : System.getenv("SMARTSPECTRA_API_KEY");

        if (apiKey == null || apiKey.isEmpty()) {
            System.out.println("Usage: java HelloVitals YOUR_API_KEY");
            System.out.println("Or:    export SMARTSPECTRA_API_KEY=YOUR_KEY");
            System.out.println("Get a key at: https://physiology.presagetech.com");
            System.exit(1);
        }

        SmartSpectra spectra = new SmartSpectra(apiKey);

        System.out.println("Initializing camera...");
        spectra.initialize();

        // Run processing in a background thread (it blocks)
        Thread processingThread = new Thread(() -> spectra.run());
        processingThread.setDaemon(true);
        processingThread.start();

        System.out.println("Running! Reading metrics every 2 seconds.");
        System.out.println("Press Ctrl+C to stop.\n");

        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println("\nStopping...");
            spectra.stop();
            spectra.destroy();
            System.out.println("Done.");
        }));

        while (processingThread.isAlive()) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                break;
            }

            float[] metrics = spectra.getMetrics();
            if (metrics != null) {
                System.out.printf("Pulse: %.0f BPM | Breathing: %.1f BPM%n",
                    metrics[0], metrics[1]);
            } else {
                System.out.println("Waiting for metrics...");
            }
        }
    }
}
```

### JNI Bridge (C++)

First, compile the Java class and generate the JNI header:

```bash
javac SmartSpectra.java
javac -h . SmartSpectra.java
```

This generates `SmartSpectra.h` with the native method signatures. Now create the implementation:

**smartspectra_jni.cpp:**

```cpp
#include <jni.h>
#include <smartspectra/container/foreground_container.hpp>
#include <smartspectra/container/settings.hpp>
#include <physiology/modules/messages/metrics.h>
#include <physiology/modules/messages/status.h>
#include <glog/logging.h>
#include <opencv2/opencv.hpp>
#include <memory>
#include <mutex>
#include <atomic>

using namespace presage::smartspectra;

struct SmartSpectraHandle {
    std::unique_ptr<container::CpuContinuousRestForegroundContainer> container;
    std::mutex mu;
    float pulse_rate = 0;
    float breathing_rate = 0;
    bool has_data = false;
    std::atomic<bool> running{false};
};

extern "C" {

JNIEXPORT jlong JNICALL Java_SmartSpectra_nativeCreate(
        JNIEnv* env, jclass, jstring apiKey, jint cameraIndex) {
    static bool glog_init = false;
    if (!glog_init) {
        google::InitGoogleLogging("smartspectra_java");
        FLAGS_alsologtostderr = false;
        glog_init = true;
    }

    const char* key = env->GetStringUTFChars(apiKey, nullptr);

    auto handle = new SmartSpectraHandle();

    container::settings::Settings<
        container::settings::OperationMode::Continuous,
        container::settings::IntegrationMode::Rest
    > settings;

    settings.video_source.device_index = cameraIndex;
    settings.video_source.capture_width_px = 1280;
    settings.video_source.capture_height_px = 720;
    settings.video_source.codec = presage::camera::CaptureCodec::MJPG;
    settings.video_source.auto_lock = true;
    settings.headless = true;
    settings.enable_edge_metrics = true;
    settings.verbosity_level = 0;
    settings.continuous.preprocessed_data_buffer_duration_s = 0.5;
    settings.integration.api_key = key;

    env->ReleaseStringUTFChars(apiKey, key);

    handle->container =
        std::make_unique<container::CpuContinuousRestForegroundContainer>(
            settings);

    handle->container->SetOnCoreMetricsOutput(
        [handle](const presage::physiology::MetricsBuffer& metrics,
                 int64_t timestamp) {
            std::lock_guard<std::mutex> lock(handle->mu);
            if (!metrics.pulse().rate().empty())
                handle->pulse_rate = metrics.pulse().rate().rbegin()->value();
            if (!metrics.breathing().rate().empty())
                handle->breathing_rate =
                    metrics.breathing().rate().rbegin()->value();
            handle->has_data = true;
            return absl::OkStatus();
        }
    );

    handle->container->SetOnVideoOutput(
        [handle](cv::Mat& frame, int64_t timestamp) {
            if (!handle->running.load())
                return absl::CancelledError("Stopped");
            return absl::OkStatus();
        }
    );

    handle->container->SetOnStatusChange(
        [](presage::physiology::StatusValue status) {
            fprintf(stderr, "[SmartSpectra] Status: %s\n",
                presage::physiology::GetStatusDescription(
                    status.value()).c_str());
            return absl::OkStatus();
        }
    );

    return reinterpret_cast<jlong>(handle);
}

JNIEXPORT jint JNICALL Java_SmartSpectra_nativeInitialize(
        JNIEnv*, jclass, jlong ptr) {
    auto handle = reinterpret_cast<SmartSpectraHandle*>(ptr);
    return handle->container->Initialize().ok() ? 0 : -1;
}

JNIEXPORT jint JNICALL Java_SmartSpectra_nativeRun(
        JNIEnv*, jclass, jlong ptr) {
    auto handle = reinterpret_cast<SmartSpectraHandle*>(ptr);
    handle->running.store(true);
    return handle->container->Run().ok() ? 0 : -1;
}

JNIEXPORT void JNICALL Java_SmartSpectra_nativeStop(
        JNIEnv*, jclass, jlong ptr) {
    auto handle = reinterpret_cast<SmartSpectraHandle*>(ptr);
    handle->running.store(false);
}

JNIEXPORT jfloatArray JNICALL Java_SmartSpectra_nativeGetMetrics(
        JNIEnv* env, jclass, jlong ptr) {
    auto handle = reinterpret_cast<SmartSpectraHandle*>(ptr);
    std::lock_guard<std::mutex> lock(handle->mu);
    if (!handle->has_data) return nullptr;

    jfloatArray result = env->NewFloatArray(2);
    float values[2] = {handle->pulse_rate, handle->breathing_rate};
    env->SetFloatArrayRegion(result, 0, 2, values);
    return result;
}

JNIEXPORT void JNICALL Java_SmartSpectra_nativeDestroy(
        JNIEnv*, jclass, jlong ptr) {
    delete reinterpret_cast<SmartSpectraHandle*>(ptr);
}

}  // extern "C"
```

**CMakeLists.txt:**

```cmake
cmake_minimum_required(VERSION 3.27.0)
project(SmartSpectraJNI CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(SmartSpectra REQUIRED)
find_package(OpenCV REQUIRED)
find_package(JNI REQUIRED)

add_library(smartspectra_jni SHARED smartspectra_jni.cpp)

target_include_directories(smartspectra_jni PRIVATE
  ${JNI_INCLUDE_DIRS}
  ${CMAKE_CURRENT_SOURCE_DIR}  # For generated SmartSpectra.h
)

target_link_libraries(smartspectra_jni
  SmartSpectra::Container
  ${OpenCV_LIBS}
)
```

## 3. Build and Run

```bash
# Compile Java and generate JNI header
javac SmartSpectra.java
javac -h . SmartSpectra.java

# Build the native library
mkdir -p build && cd build
cmake ..
make -j$(nproc)
cd ..

# Run
java -Djava.library.path=./build HelloVitals YOUR_API_KEY

# Or with environment variable
export SMARTSPECTRA_API_KEY=YOUR_API_KEY
java -Djava.library.path=./build HelloVitals
```

Output:

```
Initializing camera...
Running! Reading metrics every 2 seconds.
Press Ctrl+C to stop.

Waiting for metrics...
Waiting for metrics...
Pulse: 72 BPM | Breathing: 16.2 BPM
Pulse: 73 BPM | Breathing: 15.8 BPM
^C
Stopping...
Done.
```

## How It Works

```
Java → JNI → SmartSpectra C++ SDK → Camera + REST API
```

1. `SmartSpectra.java` loads the JNI shared library and declares native methods
2. `smartspectra_jni.cpp` implements those methods using the C++ SDK container API
3. Processing runs in a daemon thread — the SDK captures frames and calls the REST API
4. Java polls for the latest metrics on the main thread

## Platform Support

This approach only works on **Linux x86_64** (Ubuntu 22.04 / Mint 21). For **Android**, use the native Android SDK instead — see the [Kotlin quickstart](/docs/quickstart-kotlin).

## Troubleshooting

**`UnsatisfiedLinkError: no smartspectra_jni in java.library.path`**
- Make sure `-Djava.library.path=./build` points to the directory containing `libsmartspectra_jni.so`
- Or: `export LD_LIBRARY_PATH=./build:$LD_LIBRARY_PATH`

**JNI header not generated**
- Use `javac -h . SmartSpectra.java` (requires JDK 10+)
- On older JDKs, use `javah -jni SmartSpectra`

**Camera not found**
- Check: `ls /dev/video*`
- Try changing `cameraIndex` to `1` in `HelloVitals.java`

For other issues, see the [C++ troubleshooting](/docs/quickstart-cpp#troubleshooting) section.

## Next Steps

- For Android apps, use the native SDK: [Kotlin quickstart](/docs/quickstart-kotlin)
- See the [C++ quickstart](/docs/quickstart-cpp) for the full C++ API
- Browse the [SDK source](https://github.com/Presage-Security/SmartSpectra/tree/main/cpp)
