A dependency-free Java library and the camera-control command-line client for
Camera Control protocol version 4. It supports status, idempotent single
capture, and cancellable time-lapse operation over verified TLS. Status polling
reconnects with bounded exponential backoff when Wi-Fi or the server connection
is interrupted.
The client uses java.net.http.WebSocket and a small bundled JSON reader, so
the shipped JAR has no runtime dependencies at all.
- JDK 17 or newer
- Camera Control's TLS-only WebSocket API enabled and running
- The verified local CA certificate shown by the phone app
cd java
mvn packageThe build produces target/camera-control-client-0.1.0.jar with an executable
manifest. Set the password without exposing it in process arguments:
export CAMERA_CONTROL_PASSWORD='your password'If this variable isn't present, the CLI prompts on the console without echoing the password.
Read camera, device, capture-session, and reliability status:
java -jar target/camera-control-client-0.1.0.jar \
status wss://192.168.1.50:8080/ws \
--ca camera-control-local-ca.crtCapture and save one JPEG through the idempotent job API:
java -jar target/camera-control-client-0.1.0.jar \
capture wss://192.168.1.50:8080/ws \
--ca camera-control-local-ca.crt \
--camera 1 --resolution high --flash off \
--jpeg-quality 90 --rotation 90 --output photo.jpgRotation accepts 0, 90, 180, or 270 clockwise degrees. Omit the flag
to keep the current app setting; use --rotation 0 to reset it. The app starts
at 0°. Rotation applies to new photos and live previews; existing gallery
photos keep their orientation. Library callers pass "rotation": 90 in the
existing controls map. Check status.capabilities.rotation for support.
Start a 5-second time lapse, stopping after 20 photos:
java -jar target/camera-control-client-0.1.0.jar \
time-lapse wss://192.168.1.50:8080/ws \
--ca camera-control-local-ca.crt \
--interval 5 --count 20Omit --count to run until Ctrl+C, or use --duration SECONDS.
import app.flutterdev.cameracontrol.CameraControlClient;
import app.flutterdev.cameracontrol.CameraControlClient.CaptureOptions;
import app.flutterdev.cameracontrol.CameraControlClient.Options;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
try (CameraControlClient camera = CameraControlClient.withCaFile(
"wss://192.168.1.50:8080/ws",
System.getenv("CAMERA_CONTROL_PASSWORD"),
Path.of("camera-control-local-ca.crt"),
Options.defaults())) {
camera.connect();
System.out.println(camera.status());
Map<String, Object> job = camera.captureJob(
CaptureOptions.defaults().withRequestId("document-page-42"));
Map<String, Object> result =
CameraControlClient.objectField(job, "result", "job.result");
Files.write(Path.of("photo.jpg"), CameraControlClient.decodePhoto(
CameraControlClient.objectField(result, "photo", "job.result.photo")));
}captureJob retries reuse the same semantic requestId, allowing the running
app process to deduplicate an ambiguous reconnect. Do not deliberately reuse a
request ID with different capture settings. If the phone app itself restarts,
its in-memory idempotency history is lost; verify status before replaying an
ambiguous capture.
A CameraControlClient is not safe for concurrent use by multiple threads.
Download the CA from the URL shown by the app, then compare its SHA-256 fingerprint with the phone before trusting it:
curl --insecure --output camera-control-local-ca.crt \
https://192.168.1.50:8080/ca.crt
openssl x509 -in camera-control-local-ca.crt -noout -fingerprint -sha256--insecure is only for the initial CA download. WebSocketChannel builds a
KeyStore containing only this certificate and an SSLContext that trusts
nothing else.
Run tests with:
mvn testSee ../api.md for the command and event summary.