Troubleshoot the NovaVMS mobile app
This page is the recovery flowchart for problems that surface inside the mobile app. Each section is one symptom.
Symptom: splash screen never disappears
The app’s splash holds until the cold-launch session restore (SecureStore → access token + refresh) completes OR an 8-second watchdog fires. The watchdog was introduced in v1.0.3 specifically because the SecureStore + /auth/refresh call could hang silently on locked iOS Keychains or congested networks.
Recovery:
- Wait 8 seconds. The watchdog will time out and route you to the sign-in screen.
- If splash still holds longer than 10 seconds, kill the app (swipe up from app switcher) and reopen. The next cold-launch boots with a clean watchdog.
- If the problem repeats every launch, sign out from another device that’s still authenticated (web Account → Active sessions → revoke this device), then sign back in fresh on mobile. SecureStore’s refresh token will be replaced.
Symptom: live tile shows black or “Reconnecting…” forever
The mobile live transport is codec-split: H.264 cameras use WebRTC/WHEP; H.265 cameras use LL-HLS. The recovery differs by camera codec.
For H.264 (WebRTC) cameras
A black tile that never starts streaming usually means WHEP negotiation failed. Causes:
- TURN credentials expired (rare; the cloud refreshes hourly)
- The cloud could not reach the gateway (gateway offline)
- The carrier filters WebRTC (uncommon)
Recovery:
- Confirm the camera is online on the web Cameras page. If it shows Offline there, the mobile tile cannot recover until the gateway/camera issue is fixed.
- Toggle Wi-Fi off and back on. This forces a fresh ICE negotiation.
- Kill and reopen the app — a fresh prewarm-pool of PeerConnections gets allocated at app mount.
For H.265 (LL-HLS) cameras
A tile stuck on “Reconnecting…” for 20+ seconds on cold start means the cloud LL-HLS writer is cold. The cloud needs to produce the init segment and the first few CMAF parts before AVPlayer / ExoPlayer can play.
Recovery:
- Background the app and bring it back to the foreground. The pre-warm fires on every active transition — by the time you reopen, the writer should be warm.
- If still cold after foreground re-warm, sign out and sign in. The login response triggers
prewarmH265Writers('login')which warms every H.265 camera you can see. - If the problem affects every H.265 camera (not just one), the cloud-side LL-HLS service may be unhealthy. Check your team’s status channel; the web Live view will be similarly affected.
Symptom: live tile shows a static thumbnail and never goes live
The decoder lease is full. The phone’s MediaCodec / VideoToolbox can hold a finite number of simultaneous hardware decoders (Android emulator: 4, Android real device: 8, iOS: 8). Tiles beyond the cap render the last thumbnail rather than mounting a transport — better than the alternative (the OS reclaiming an older decoder and the tile cycling forever).
Recovery:
- Tap the thumbnail tile. Expanding it shifts the decoder lease to the foreground player.
- Switch to a layout with fewer cameras. A 2x2 grid (4 tiles) is comfortably under the cap on every supported device.
- Background then foreground the app. Backgrounding pauses every tile and releases the leases; foregrounding re-acquires them in FIFO order from the top of the grid.
Symptom: push notifications are silent
Push requires three conditions to all hold. Walk through them in order:
- OS notification permission is granted. Check More → Notification settings. The row below “Push notifications” shows the current state (“Permission granted”, “Permission denied”, etc.).
- The in-app Push toggle is ON. Same screen.
- Your assigned alert rules are firing. Confirm an alert exists on the web Alerts page; if no alert was fired, no push could have been sent.
Recovery for permission:
- If the row says “Permission denied in system settings”, tap Open System Settings (the CTA below the toggle), grant Notifications for NovaVMS, and return to the app. The screen re-checks permission on every focus return and the CTA disappears.
- If the row says “Permission denied — toggle to grant”, toggling the row will re-show the OS prompt.
Recovery for Android channel routing:
Android 8+ routes notifications through channels. NovaVMS registers the alerts channel at boot with HIGH importance, but a user (or another app via a “do not disturb” config) can override the channel to silent.
- Open Android Settings → Apps → NovaVMS → Notifications.
- Make sure Allow notifications is on and the Alerts channel is set to anything other than “Silent”.
Recovery for iOS:
- Open iOS Settings → NovaVMS → Notifications.
- Make sure Allow Notifications, Sounds, Badges, Banners (Persistent or Temporary) are all on.
Symptom: talkdown button is silent (camera speaker stays quiet)
The talkdown button visual reaches Transmitting (red, with mic icon) but the camera’s speaker stays quiet. Two likely causes:
- The camera’s backchannel speaker accepts only PCMU/PCMA, not Opus. The Opus→PCMU transcode is partial in v1; some legacy cameras may not receive audio. Test against a Tapo C200 or LC-TA3R confirmed in production; if those work, the failing camera is the unsupported codec class.
- The camera’s
audio_enabledis off. Open Camera settings → Audio. Toggle Audio enabled on (operator+ only). - The speaker is physically muted. Some cameras have a hardware switch or a vendor-app-level mute that NovaVMS cannot see. Verify in the vendor app.
If the button never reaches Transmitting at all:
- Error state on first press: microphone permission was denied. Re-enable in OS Settings → NovaVMS → Microphone.
- HTTP 403 in the network trace: your role is not operator or higher. Talkdown is restricted to operator+ roles.
- HTTP 404 in the trace: the cloud could not route to a gateway for this camera (gateway offline).
Symptom: biometric login flipped off without you turning it off
NovaVMS automatically disables biometric unlock when the OS biometric enrollment security level changes — all biometrics were removed, or the security level dropped from Class 3 / Face ID-strong to Class 2 / Touch ID-weak. This is the validateEnrollment check in mobile/src/libraries/auth/biometric.ts. Note: adding a new biometric at the same security level (e.g., a second fingerprint while Class 3 stays Class 3) does NOT trigger auto-disable — the check compares levels, not individual enrollments. This is a deliberate trade-off documented under OWASP MASVS-AUTH-2 guidance.
The app surfaces a one-time toast on the next visit to Account:
Biometric login was turned off
The biometrics enrolled on this device changed, so we disabled biometric sign-in for safety. Re-enable it below if you like.
Recovery: sign in with your password, open More → Account → Security → Biometric login, toggle it on. The toggle runs a fresh biometric ceremony to confirm you can authenticate, then re-snapshots the new enrollment level.
If the toggle is disabled (greyed out), it means the OS reports no biometrics are enrolled. Enroll a fingerprint or set up Face ID in OS Settings first, then return.
Symptom: sign-in fails with “Invalid email or password”
If you’re sure the email and password are correct (try them on the web to confirm), check:
- Soft keyboard dropped characters: Android’s IME can drop characters under high CPU load. Watch the password field as you type to confirm every character appeared.
- Capslock / autocorrect: the email field auto-disables capitalization and autocorrect; the password field auto-disables capitalization. If your password contains a leading uppercase letter, type it manually.
- Account locked / disabled: if web sign-in also fails for the same email, your admin may have disabled the account.
Symptom: share or download fails with “Could not save the clip”
The Event-detail Share + Download path downloads the presigned MP4 to local storage before opening the share sheet. If the download fails, the share never opens.
Causes:
- Presigned URL expired. The cloud’s presigned URLs are short-lived. If you opened the event 10+ minutes ago and never tapped Share until now, the URL has expired. Tap Back then Forward to the event — the screen refetches with a fresh URL.
- Disk full. The app uses the document directory for the local file. If your device is out of space, the download fails. Free space.
- Network failed mid-download. The error alert names the failing step (download vs. share). Retry on a stable connection.
Symptom: app crashes / one screen renders blank with “Something went wrong”
The app wraps every top-level screen in a Sentry error boundary. A render-phase crash in (e.g.) Event detail does not take down the tab bar; you see a “Something went wrong” fallback on that screen with a Retry button.
Recovery:
- Tap Retry. The boundary remounts the screen with fresh state.
- If Retry doesn’t fix it, go back and try a different event / camera. Sentry captures the crash with stack trace for your admin to triage.
- Background the app and reopen. A fresh JS engine + native module reset clears most stuck states.
If the entire app crashes back to the OS, that’s a native-level crash (much rarer). Reopen the app and report to your admin so they can look up the crash in Sentry.
Symptom: app shows a “Session expired” sheet mid-session
Your access token expired AND the refresh attempt failed — typically after long idle periods, or after your admin revoked your sessions from the web.
Recovery: tap Sign in again. If biometric unlock is enabled, the unlock button on LoginScreen completes the re-auth without typing a password.