Skip to content

Per-camera mute, pinch-to-zoom, grid system bars, config export/import (AI-written)#40

Open
mmssix wants to merge 7 commits into
penguin86:stablefrom
mmssix:stable
Open

Per-camera mute, pinch-to-zoom, grid system bars, config export/import (AI-written)#40
mmssix wants to merge 7 commits into
penguin86:stablefrom
mmssix:stable

Conversation

@mmssix

@mmssix mmssix commented Jun 10, 2026

Copy link
Copy Markdown

⚠️ AI-written code

To be fully transparent: all code in this PR was written by an AI (Claude, via Claude Code), working under my direction. I described the features and problems, the AI wrote the code, and every change was tested on real hardware before being committed — a Samsung SM-G736U (Android 13) and a Fairphone 6 (LineageOS), against five live RTSP cameras (thingino firmware), including system-level audio verification with dumpsys media.audio_flinger. Please review with that in mind; happy to split this into smaller PRs or drop any part you don't want.

What's included (4 commits)

1. Export/import camera configuration

New overflow menu items in Settings export the internal settings.bin to a user-chosen location via the Storage Access Framework, and import one back (with UI reload). Useful for backup and for moving a camera list between devices.

2. Per-camera mute button + single-camera view fixes

Each camera tile gets a mute toggle (green note = audio playing, red slashed note = muted, grey = stream has no audio track). Mute state is persisted per camera in Settings (backward compatible with existing settings.bin files). Also fixes the single-camera view rendering blank after surface resizes.

Two libvlc findings baked into this commit that may interest you:

  • setAudioTrack(-1) permanently kills the audio output pipeline of a MediaPlayer — nothing restores it short of recreating the player. Mute is therefore implemented with software volume (setVolume(0/100)).
  • --aout=opensles (previously in VLC_OPTIONS) makes setVolume/getVolume silently non-functional. The option is removed; the default AudioTrack output honors software volume.
  • surfaceDestroyed now only detaches the vout (no stop()), so streams survive surface resizes/relayouts.

3. Pinch-to-zoom, pan, and double-tap zoom in single camera view

Pinch zooms 1x–8x anchored at the gesture focal point, drag pans while zoomed, double-tap zooms to 3x on the tapped spot (again to reset). Implemented with view transforms on the SurfaceView (compositor-side only — the stream and vout are untouched). Panning is clamped to the actual video frame using getCurrentVideoTrack() dimensions, so the letterbox bars are never pannable. Zoom resets when returning to the grid. The duplicated fullscreen-toggle logic was unified into one method, and intent-based camera opening (expandByIndex/expandByName) now correctly sets the fullscreen flag.

4. System bars stay visible in grid view

Leanback/immersive mode now applies only in single-camera view; the grid keeps the status and navigation bars and lays out between them (FLAG_LAYOUT_NO_LIMITS is toggled accordingly). The bar-restore branch of leanbackMode(false) was previously commented out; it is now implemented.

Notes

  • One behavior trade-off: tap-to-toggle between grid and single view now waits the standard ~300 ms double-tap disambiguation delay.
  • Commit messages carry the full per-change details and test evidence.

🤖 Generated with Claude Code

marvan and others added 7 commits June 10, 2026 14:58
## Features Added

### Export/Import Camera Configuration (SettingsFragment.java)
- Added Export Configuration and Import Configuration menu items to the
  settings toolbar overflow menu (settings_menu.xml, strings.xml)
- Implemented full export functionality: copies internal settings.bin to a
  user-selected location via Android Storage Access Framework (SAF)
- Implemented full import functionality: reads a settings.bin file from a
  user-selected location, overwrites internal storage, and reloads the UI
- Used ActivityResultLauncher with ActivityResultContracts.GetContent() for
  the file picker, registered in onViewCreated()
- Added Settings.getFileName() helper to expose the internal FILENAME constant
  for use in SettingsFragment (Settings.java)

## Bug Fixes

### NullPointerException crash when navigating to Settings (SurveillanceFragment.java)
- Root cause: the onGlobalLayout listener on each camera's SurfaceView was
  still firing after CameraView.destroy() was called during onPause(). At
  that point mediaPlayer had already been set to null, causing a NPE on
  mediaPlayer.getVLCVout().setWindowSize().
- Fix: added a null check on mediaPlayer inside the onGlobalLayout lambda so
  the resize call is skipped if the player has already been destroyed.

### VLC library update (build.gradle)
- Replaced deprecated de.mrmaffen:libvlc-android:2.1.12 (unavailable on
  Maven Central) with org.videolan.android:libvlc-all:3.4.0
- Bumped minSdkVersion from 15 to 17 to satisfy the new library's minimum
  requirement
- Removed IVLCVout field from CameraView and replaced with direct calls to
  mediaPlayer.getVLCVout() to match the updated API

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mute button:
- Add an overlay mute button to each camera cell (ImageButton on a
  translucent background in a FrameLayout wrapping the SurfaceView).
- Three states: grey slashed note = stream has no audio track,
  red slashed note = muted, green music note = audio playing.
  New vector drawables ic_music_note and ic_music_off (Material icons).
- Audio availability is detected via the MediaPlayer ESAdded event
  (getAudioTracksCount() > 0).
- Mute is implemented with software volume (setVolume(0)/setVolume(100))
  instead of setAudioTrack(-1): disabling the audio track shuts down
  libvlc's audio output pipeline permanently, and nothing short of
  recreating the player brings it back. Volume changes leave the
  pipeline alive so unmuting is instant and reliable.
- Remove the --aout=opensles option: the OpenSL ES output in
  libvlc-android ignores setVolume/getVolume entirely (set succeeds
  but volume never changes), which made volume-based muting a no-op.
  The default AudioTrack output honors software volume.

Persistence:
- Add a 'muted' flag to the Camera entity, stored with the existing
  settings.bin serialization (backward compatible, defaults to false).
- Toggling mute saves settings immediately; on restart the saved state
  is re-applied when the audio track is detected, with a short
  verify-and-retry loop since the audio output may not be initialized
  yet at ESAdded time.

Single-camera (fullscreen) view fixes:
- Tapping a camera previously showed a blank screen: the weighted grid
  layout params (width=0, weight=1) don't fill the screen when sibling
  views are hidden, and the SurfaceHolder callback stopped playback
  when the surface was destroyed during the resize.
- Explicitly set MATCH_PARENT params when entering fullscreen and
  restore the original grid params when leaving.
- SurfaceHolder callback now only detaches/reattaches the VLC video
  views on surface destroy/create and updates the window size on
  surface changes; playback is never stopped by surface transitions.
- Move the mute button further from the corner (24dp instead of 4dp)
  in fullscreen so it clears curved screen corners, and restore the
  tight position in grid view.

Tested on-device (SM-G736U): mute/unmute toggling, mute state surviving
app restart (verified at the audio_flinger level: track volume 0.0/-inf
after relaunch), fullscreen enter/exit, and grid layout restoration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
In single (fullscreen) view, the video can now be zoomed and panned:

- Pinch zooms between 1x and 8x, anchored at the gesture focal point
  (the video point under the fingers stays put while scaling).
- Single-finger drag pans while zoomed; two-finger drag pans during
  a pinch as well.
- Double-tap zooms to 3x centered on the tapped point; double-tap
  again resets to 1x. In grid view a double-tap simply expands the
  camera like a single tap.
- Zoom and pan are reset automatically whenever the view returns to
  the grid (tap or back button).

Implementation: a ScaleGestureDetector plus GestureDetector attached
to each camera's container FrameLayout drive setScaleX/setScaleY and
setTranslationX/setTranslationY on the SurfaceView, so the zoom is
purely compositor-side; the stream and VLC vout window size are
untouched. Panning is clamped to the actual video frame, computed
from MediaPlayer.getCurrentVideoTrack() dimensions and the
aspect-fit scale, so the letterboxing bars stay centered and the
view can never be panned into them (falls back to surface bounds if
the track size is not yet known).

Refactoring along the way:

- The fullscreen-toggle logic, previously duplicated verbatim in the
  SurfaceView and container click listeners, is now a single
  toggleFullscreen() method invoked via the container's click
  listener; taps are delivered through the gesture detector
  (onSingleTapConfirmed -> performClick). Note this adds the
  standard ~300ms double-tap disambiguation delay to the toggle.
- expandByIndex()/expandByName() (intent-based camera opening) never
  set the fullscreenCameraView flag; they now do, and also apply the
  fullscreen mute-button offset, so gestures behave correctly when a
  camera is opened via shortcut.
- showAllCameras() resets zoom and mute-button position for all
  views, covering the back-button exit path.

Tested on device (SM-G736U) via adb: double-tap zoom on two cameras,
swipe panning with edge clamping, double-tap reset, return to grid,
and mute button still receives clicks through the new container
touch listener. Pinch itself shares the same transform/clamp code
path (not simulatable over adb).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Previously the surveillance screen entered leanback mode (system bars
hidden, drawing below the notch) for its entire lifetime. Now the
grid view keeps the status and navigation bars, and leanback applies
only while a single camera is displayed fullscreen.

Changes:

- leanbackMode(false) now actually shows the system bars - the
  restore branch was commented out upstream and never implemented,
  since it was only called from onPause.
- leanbackMode() also toggles FLAG_LAYOUT_NO_LIMITS (set globally in
  MainActivity.onCreate, left in place there for startup): cleared
  in grid view so the layout fits between the bars instead of
  drawing underneath them, set in single view together with
  LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES so video still uses the
  whole screen including the notch area.
- Leanback is entered/exited in toggleFullscreen() and on the
  back-button exit path, and entered by expandByIndex()/
  expandByName() for intent-based camera opening.

Tested on Fairphone 6 (LineageOS): bars visible in grid, hidden in
single view, restored on return to grid via tap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When the Fragment is paused while in single camera view, the gesture
detectors (ScaleGestureDetector + GestureDetector) attached to the
container could receive touch events on detached views. This could
cause crashes or unexpected behavior.

Fix: clear the container's OnTouchListener in onPause() before
destroying the cameras, ensuring no touch events fire on detached
views.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When a camera enters fullscreen view, keep the screen on to prevent
disruption during surveillance monitoring. The flag is cleared when
returning to grid view.
Review by pi-lens identifying one medium-severity issue:
- Gesture listener memory leak in onPause() when fragment is paused
  in single camera view

Also notes strengths in lifecycle management, pan clamping, and
backward compatibility.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant