Skip to content

Latest commit

 

History

History

app

README.md

OpenCCTV app

iOS and Android app for OpenCCTV: live view, playback, motion events, push notifications and server management for your own OpenCCTV server or gateway. Expo SDK 57, React Native 0.86, expo-router with native tabs. MIT, © 2026 Codext GmbH.

Bundle ID / package de.codext.opencctv, URL scheme opencctv (opencctv://pair?server=…&code=…).

Development

bun install
bun run typecheck      # TypeScript
bun test src           # unit tests (URL building, timeline maths)
bun run ios            # expo run:ios (Debug build), Metro on port 8097
bun run start          # Metro only, port 8097

ios/ and android/ are generated by bunx expo prebuild and not committed. plugins/with-scene-lifecycle.js adds the UIScene life cycle the iOS 27 SDK requires.

Local test server (from the repo root):

cd server && OPENCCTV_DEMO=1 OPENCCTV_PORT=18190 OPENCCTV_DATA=/tmp/occtv-app-test \
  OPENCCTV_RTSP_PORT=18554 OPENCCTV_RTMP_PORT=11935 OPENCCTV_WEBRTC_PORT=18555 \
  OPENCCTV_ADMIN_USER=admin OPENCCTV_ADMIN_PASSWORD=admin12345 bun src/index.ts serve

Connect to http://localhost:18190 (Android emulator: http://10.0.2.2:18190). Demo mode adds the viewer account demo / demo.

The public demo (https://opencctv-demo.codext.de, demo / demo) is set in src/lib/config.ts and can be overridden with EXPO_PUBLIC_DEMO_URL. The app never registers push tokens and never sends a device name to the demo server.

Release builds

# iOS (CocoaPods needs a UTF-8 locale; rerun pod install if it fails right after prebuild)
bunx expo prebuild --platform ios
cd ios && LANG=en_US.UTF-8 pod install && cd ..
LANG=en_US.UTF-8 bunx expo run:ios --configuration Release --device <simulator-udid> --no-bundler

# Android (JDK 17 or newer, e.g. Android Studio's bundled JBR)
bunx expo prebuild --platform android
cd android && JAVA_HOME=<jdk> ./gradlew assembleRelease

Store builds go through EAS (@codext-gmbh/opencctv, project id in app.json). Push uses Expo push tokens, FCM via google-services.json. The Android release build is signed with the debug key until a release keystore is configured.

Store screenshots

Build with EXPO_PUBLIC_SCREENSHOTS=1 (Release build as above). This enables the /shots route behind a Stack.Protected guard; it never exists in store builds. The route signs in and opens a screen, e.g. opencctv://shots?server=http://localhost:18190&user=admin&pass=admin12345&lang=de&go=/events, or resets to onboarding with ?reset=1.

maestro/run-screenshots.sh <simulator-udid> [server-url] ["en de"] [output-dir]
# e.g. iPhone 17 Pro Max (6.9", required by App Store Connect) and iPhone 17 Pro (6.3")
maestro/run-screenshots.sh <pro-max-udid> http://localhost:18190 "en de" screenshots/iphone-6.9
maestro/run-screenshots.sh <pro-udid> http://localhost:18190 "en de" screenshots/iphone-6.3

The script sets a clean status bar (9:41, full signal and battery) and dark appearance, runs maestro/screenshots.yaml per language and writes <output-dir>/<lang>/01-welcome.png … 07-storage.png plus 08-fullscreen-landscape.png (taken with simctl, so it carries no rotation tag). The storage shot expects a target named "NAS backup" on the server. Demo clips loop and a frame at the loop boundary can be black: check the images and rerun a single flow (maestro/shot.yaml, maestro/live-landscape.yaml) if needed.

Structure

src/app/            routes (expo-router)
  (tabs)/           Live, Playback, Events, Manage (admins only), Settings
  camera/[id]       full-screen live: WebRTC WebView with HLS fallback, zoom, swipe, PTZ, talk
  replay/[id]       full-screen playback for a camera (?at=ISO opens at a time)
  manage/*          cameras, add wizard, storage targets, users, pairing, gateway, system
  settings/*        servers, about, privacy, licences
  welcome, connect, scan, pair, sites   onboarding and server management
  shots             screenshot helper (EXPO_PUBLIC_SCREENSHOTS=1 only)
src/components/     UI kit, players (hls-video, webrtc-player, live-stage), timeline, PTZ pad
src/lib/            API client, query cache, live updates (WebSocket), push, timeline maths
src/store/          zustand stores (servers with tokens in SecureStore, settings)
src/i18n/           de.ts (source) and en.ts
maestro/            screenshot flows

How video plays

  • Grid: JPEG snapshots as an instant placeholder, then the HLS substream (live.m3u8?quality=sd) for visible tiles (at most six at a time, can be turned off in Settings).
  • Full screen: the server's /player/:id?embed=app page in a WebView (WebRTC, then MSE, then HLS inside the page) for low latency and two-way audio. If it does not start within nine seconds the app switches to native HLS with expo-video (Picture in Picture and AirPlay available there).
  • Recordings: MP4 segments with Range support, played with two alternating expo-video players so the next segment is preloaded and playback continues seamlessly.