Per-camera mute, pinch-to-zoom, grid system bars, config export/import (AI-written)#40
Open
mmssix wants to merge 7 commits into
Open
Per-camera mute, pinch-to-zoom, grid system bars, config export/import (AI-written)#40mmssix wants to merge 7 commits into
mmssix wants to merge 7 commits into
Conversation
## 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.binto 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 existingsettings.binfiles). 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 aMediaPlayer— nothing restores it short of recreating the player. Mute is therefore implemented with software volume (setVolume(0/100)).--aout=opensles(previously inVLC_OPTIONS) makessetVolume/getVolumesilently non-functional. The option is removed; the default AudioTrack output honors software volume.surfaceDestroyednow only detaches the vout (nostop()), 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 usinggetCurrentVideoTrack()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_LIMITSis toggled accordingly). The bar-restore branch ofleanbackMode(false)was previously commented out; it is now implemented.Notes
🤖 Generated with Claude Code