Skip to content

fix(Android): save videos and photos directly to MediaStore - #15075

Open
bosism wants to merge 1 commit into
mavlink:masterfrom
bosism:codex/android-mediastore-recording
Open

bosism wants to merge 1 commit into
mavlink:masterfrom
bosism:codex/android-mediastore-recording

Conversation

@bosism

@bosism bosism commented Sep 10, 2026

Copy link
Copy Markdown

Bug Description

On recent Android versions, streaming videos and local video snapshots are saved under QGC's app-specific Android/data/... directory. A successful capture is therefore difficult to find in Gallery/Photos or ordinary file managers.

Related to the storage-location discussion in #12989. #12999 fixed the recording filename, but did not resolve shared-media discoverability. This PR addresses new GStreamer recordings and local snapshots on Android 10/API 29+; it does not implement SD-card selection or migrate existing files.

Root Cause

The writers use filesystem paths in QGC's app-specific storage. These paths do not create shared MediaStore entries. A MediaStore content URI also cannot be passed to a GStreamer filesystem sink as a pathname.

Solution

  • Use one small Java/JNI helper to create pending owned media, lend a seekable descriptor to the writer, and publish only after completion.
  • Record directly into MediaStore.Video under Movies/QGroundControl/ using fdsink. Keep descriptor ownership through muxer shutdown, require finalization and a received keyframe, and remove empty attempts.
  • Route the existing QML main-video frame grab through VideoManager::saveImage. Encode JPEG directly into MediaStore.Images under Pictures/QGroundControl/, flush/close before publication, and discard failed image writes. The main-stream guard avoids duplicate snapshots when thermal video is visible.
  • Preserve the video storage limit: cleanup considers only this package's completed videos in this album, never photos, other apps' media, or pending items.
  • Keep existing destinations for desktop, API 28, UVC, and the QtMultimedia-only backend. Native MAVLink camera still capture is unchanged.

There is one retained media file, not an app-private copy plus an exported copy. The existing MP4/MOV faststart muxer can still use temporary storage. No new storage permission or export UI is added.

User-visible behavior and limitations

This intentionally changes the default destination for the supported Android path to primary shared storage. Published media survives uninstall and can be accessed by appropriately authorized media apps or included in user-configured Gallery/cloud backup. Publication itself does not upload anything.

Auto-Delete Saved Recordings still applies and is enabled by default on mobile. Users who want to retain all published videos must disable it. Existing private clips are not migrated; telemetry .ass sidecars remain private.

Nonempty unfinished recordings remain pending, subject to Android expiry; this is not crash recovery. There is no silent private-storage fallback on MediaStore error. Gallery codec/container support varies. The local-photo counter and backend screenshot-completion logs are separate from this destination change.

Testing

  • Tested locally
  • Added regression tests
  • Tested with simulator (SITL)
  • Tested with hardware on this combined revision

Validation on commit 057897b0f:

  • Android arm64 Debug native/Java build and APK packaging passed: Qt 6.11.1, NDK 27.2.12479018, GStreamer 1.28.4, min API 28 / target API 36, Java 21.
  • Linux Debug test-enabled build passed. The final amendment after that build is Java-only; the verified C++/QML sources are unchanged.
  • VideoManagerTest passed, including a real QML-to-C++ invocation producing a decodable JPEG and empty-input rejection.
  • GStreamerTest passed: 89 passes and 16 platform/GPU-capability skips. Both filesystem and borrowed-descriptor H.265 MP4 finalization/decoding cases passed, not skipped; the latter verifies the sink leaves its caller-owned descriptor open.
  • QGCMediaStoreTest: 4 passed (video/JPEG MIME mapping and unsupported input).
  • Fresh pre-submission ctest --output-on-failure -L Unit --parallel 4: 266/267 passed. The untouched BluetoothWorkerTest::_testDisconnectResetsReconnectState fails on the host BlueZ warning Invalid address to remote address passed. This also reproduces alone and was present in the local validation notes for fix(Joystick): match actual update rate to joystick setting #14797.
  • git diff --check, focused formatting and source-policy checks passed. The broader pre-commit run still reports existing whole-file formatting/QML/assertion findings and missing local clang-tidy; no unrelated cleanup is included.
  • Android lint has no findings in the new helper, but the project report retains an existing USB-receiver registration error in QGCUsbSerialManager.java and 77 warnings. lintDebug returning success is not a clean lint gate because abortOnError=false.

The backend README includes manual Gallery, repeated capture/stop-start, thermal, permissions, retention, failure-path, API-28 fallback, and release/JNI acceptance checks. End-to-end device acceptance of this combined revision and release/R8 runtime validation are not claimed by the automated tests.

Platforms Tested

  • Linux
  • Windows
  • macOS
  • Android (build/package and Java unit tests; device acceptance still needed)
  • iOS

Flight Stacks Tested

  • PX4
  • ArduPilot
  • N/A — local media storage; no vehicle protocol change

Checklist

  • I have read the Contribution Guidelines and Code of Conduct
  • Added focused regression coverage and documented manual acceptance
  • One focused commit based on current official master
  • All style/static-analysis gates pass (limitations above)
  • New and existing unit tests pass locally (266/267; limitation above)

By submitting this pull request, I confirm that my contribution is made under the terms of the project's dual license (Apache 2.0 and GPL v3).

Use one shared pending-media helper for GStreamer recordings and local main-video JPEG snapshots on Android 10 and newer. Write through borrowed descriptors, publish only completed media, and avoid a second app-private copy.

Keep descriptor ownership through muxer shutdown, preserve owned-video storage limits, and retain legacy destinations for other backends and older Android. Add recording descriptor and QML image-save regressions plus Android acceptance guidance. Snapshot counter/completion feedback and interrupted-recording recovery remain separate.
@codecov

codecov Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 13.63636% with 38 lines in your changes missing coverage. Please review.
✅ Project coverage is 34.85%. Comparing base (f29efd3) to head (057897b).
⚠️ Report is 375 commits behind head on master.

Files with missing lines Patch % Lines
...anager/VideoReceiver/GStreamer/GstVideoReceiver.cc 6.66% 25 Missing and 3 partials ⚠️
src/VideoManager/VideoManager.cc 28.57% 10 Missing ⚠️

❌ Your patch check has failed because the patch coverage (13.63%) is below the target coverage (30.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master   #15075      +/-   ##
==========================================
+ Coverage   25.47%   34.85%   +9.38%     
==========================================
  Files         769      950     +181     
  Lines       65912    85936   +20024     
  Branches    30495    39606    +9111     
==========================================
+ Hits        16788    29951   +13163     
+ Misses      37285    35987    -1298     
- Partials    11839    19998    +8159     
Flag Coverage Δ
unittests 34.85% <13.63%> (+9.38%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/VideoManager/VideoManager.h 22.22% <ø> (+22.22%) ⬆️
...Manager/VideoReceiver/GStreamer/GstVideoReceiver.h 0.00% <ø> (ø)
src/VideoManager/VideoManager.cc 8.51% <28.57%> (+6.61%) ⬆️
...anager/VideoReceiver/GStreamer/GstVideoReceiver.cc 5.35% <6.66%> (+2.45%) ⬆️

... and 680 files with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update e6aeacb...057897b. Read the comment docs.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

Copy link
Copy Markdown
Contributor

Build Results

Platform Status

Platform Status Details
Linux Passed View
Windows Passed View
MacOS Passed View
Android Passed View

All builds passed.

Pre-commit

Check Status Details
pre-commit Failed (non-blocking) View

Pre-commit hooks: 2 passed, 77 failed, 7 skipped.

Test Results

linux-coverage-integration: 52 passed, 0 skipped
linux-coverage-unit: 190 passed, 0 skipped
linux-sanitizers-integration: 31 passed, 0 skipped
linux-sanitizers-unit: 190 passed, 0 skipped
Total: 463 passed, 0 skipped

Code Coverage

Coverage: 71.2%

No baseline available for comparison

Artifact Sizes

Artifact Size
QGroundControl 253.15 MB
QGroundControl-aarch64 177.37 MB
QGroundControl-installer-AMD64 128.44 MB
QGroundControl-installer-AMD64-ARM64 70.84 MB
QGroundControl-installer-ARM64 99.58 MB
QGroundControl-linux 84.78 MB
QGroundControl-mac 84.78 MB
QGroundControl-x86_64 189.50 MB
No baseline available for comparison

Updated: 2026-09-10 15:19:11 UTC • Commit: 057897b • Triggered by: Linux

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant