Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 76 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,74 +1,104 @@
# RubyFPV Android Viewer

Android app that displays the live video feed from a [RubyFPV](https://rubyfpv.com) ground station over USB-C tethering.

Plug your phone into the Ruby ground station, enable USB tethering, and see exactly what the pilot sees.

## How it works

1. Phone connects to Ruby ground station via USB cable
2. USB tethering is enabled on the phone (creates a network link)
3. Ruby detects the phone and sends raw H.264 video over UDP port 5001
4. The app decodes and displays the video in fullscreen with minimal latency
Android companion app for [RubyFPV](https://rubyfpv.com). Browse, download, play and
manage the onboard `.ts` recordings off your drone's SD card over Wi-Fi — DJI-Fly style —
then tap **Return to FPV mode** to reboot the drone back to flight. A live USB viewer
(the original `MainActivity`) is still available behind the ⋮ menu.

## Phone-transfer workflow

1. **On the Ruby Ground Station**, press **Enter phone-transfer mode**. The drone reboots
its radio into an open Wi-Fi access point named `RubyFPV-<MAC4>` at `192.168.4.1`.
2. **On your phone**, join that Wi-Fi network.
3. Open the app and tap **Connect**. It SSHes the drone and lists the onboard recordings.
4. Browse clips with thumbnails, **download**, **play**, and **delete** as you like.
5. When you're done, tap **⋮ → Return to FPV mode**. The drone reboots out of
phone-transfer AP mode back into normal flight/FPV (ready to fly again in ~45 s).

The drone runs **dropbear**, whose build ships **no SFTP / SCP subsystem**, so the app
never opens an SFTP channel. It drives plain `exec` channels instead: listing with
`stat`, transferring by streaming `cat`. Recordings live in `/mnt/mmcblk0p1/ruby` and the
default login is `root@192.168.4.1:22` (editable in **⋮ → Connection settings**).

## Recordings — download & playback

- **Thumbnails** are decoded on-device from the first few MB of each clip (it starts on a
keyframe). The onboard recordings are **HEVC in MPEG-TS**, which the Android framework
demuxer reports as 0 tracks; the app uses media3 1.6.0's
`ExperimentalFrameExtractor` — the same ExoPlayer pipeline used for playback — to pull
the first frame.
- **Playback** plays the downloaded `.ts` directly in ExoPlayer (no remux / re-encode).
- **Multi-select**: long-press a clip to enter selection mode, then bulk-delete.
- **Per-item delete** lets you choose where to remove a clip from: **phone**, **drone SD
card**, or **both**.
- **Share** a downloaded clip to any app via the system share sheet.

## Live USB viewer

The original USB viewer is still present, reachable from **⋮ → Live view (USB)**:

1. Connect the phone to the Ruby ground station via USB cable and enable USB tethering.
2. Ruby forwards raw H.264 video over UDP port 5001.
3. The app decodes and displays it fullscreen with minimal latency.

To enable forwarding on the ground station: **Controller > Video Forward**, turn on
**Video Forward To USB Device**, type **Raw (H264)**, port **5001**.

## Features

- Hardware H.264 decoding via Android MediaCodec
- Fullscreen landscape display
- Auto-recovery on stream loss (3-second watchdog)
- Stream stats overlay (tap screen to toggle): bitrate, packet rate, NAL rate
- Minimal latency — designed for FPV use
- Recordings browser with thumbnails, file size, date and on-drone / on-device state
- ExoPlayer playback of `.ts` HEVC clips (hardware decode, no remux)
- Multi-select (long-press) with bulk delete
- Per-item delete: phone / drone SD card / both
- Share downloaded clips via the system share sheet
- **Return to FPV mode** — reboots the drone out of phone-transfer AP back into flight
- Live USB viewer (raw H.264 over UDP 5001) behind the ⋮ menu
- In-app diagnostics / debug log viewer

## Requirements

- Android 7.0+ (API 24)
- USB data cable (not charge-only)
- RubyFPV ground station with USB video forwarding enabled

## Ruby ground station setup

1. Go to **Controller > Video Forward** in the Ruby menu
2. Enable **Video Forward To USB Device**
3. Set type to **Raw (H264)**
4. Port: **5001** (default)

## Install
- Android 7.0+ (min SDK 24, target SDK 35)
- A RubyFPV drone that supports phone-transfer mode (open AP `RubyFPV-<MAC4>` @ 192.168.4.1)
- For the live USB viewer: a USB data cable and a Ruby ground station with USB video
forwarding enabled

Download the latest APK from [GitHub Actions](https://github.com/wkumik/RubyFPV-Android-Viewer/actions) (build artifacts) or build from source.
## Build from source

### Build from source
Requires **Java 21** and the Android SDK (`ANDROID_HOME` set, or a `local.properties`
with `sdk.dir`).

```
```sh
git clone https://github.com/wkumik/RubyFPV-Android-Viewer.git
cd RubyFPV-Android-Viewer
./gradlew assembleDebug
./gradlew :app:assembleDebug
```

APK will be at `app/build/outputs/apk/debug/app-debug.apk`
The APK is stamped with its version and lands at:

## Testing without a ground station
```
app/build/outputs/apk/debug/RubyFPV-v<versionName>-b<versionCode>-debug.apk
```

You can test the app using ffmpeg from a PC while the phone is USB-tethered to the PC:
(e.g. `RubyFPV-v1.6-b8-debug.apk`).

```
ffmpeg -f lavfi -i testsrc=size=1280x720:rate=30 \
-pix_fmt yuv420p -c:v libx264 -profile:v baseline \
-tune zerolatency -g 30 -bsf:v dump_extra \
-f h264 udp://<phone_ip>:5001?pkt_size=1024
Install it with adb:

```sh
adb install -r app/build/outputs/apk/debug/RubyFPV-v1.6-b8-debug.apk
```

## Roadmap
## Tech notes

See [ROADMAP.md](ROADMAP.md) for planned features including:
- Ruby OSD overlay (V2)
- H.265 support (V3)
- Screen recording / DVR (V4)
- media3 1.6.0 (ExoPlayer + UI + Transformer's `ExperimentalFrameExtractor`)
- jsch (`com.github.mwiede:jsch`) for SSH over the drone's dropbear (exec channels only —
no SFTP)
- Min SDK 24, target / compile SDK 35, Java 17 source/target

## Credits

- [RubyFPV](https://rubyfpv.com) by Petru Soroaga
- Video decoding architecture inspired by [OpenIPC Decoder](https://github.com/OpenIPC/decoder) (MIT License)
- NAL parsing based on [Consti10/myMediaCodecPlayer-for-FPV](https://github.com/Consti10/myMediaCodecPlayer-for-FPV)
- Live USB viewer NAL parsing based on
[Consti10/myMediaCodecPlayer-for-FPV](https://github.com/Consti10/myMediaCodecPlayer-for-FPV)

## License

Expand Down
16 changes: 16 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# RubyFPV Android Viewer — Roadmap

## Recordings — DJI-style transfer & preview (primary)

Get onboard HEVC recordings off the drone over its Wi-Fi, review and share them.

- [x] Material 3 dark "album" UI — thumbnails, duration, size, date
- [x] SSH connection to the drone over dropbear (exec + `cat`, no SFTP subsystem)
- [x] List `/mnt/mmcblk0p1/ruby/*.ts` (+ paired `.osd`)
- [x] Download with progress
- [x] Lossless `.ts` → `.mp4` remux on-device (MediaExtractor + MediaMuxer)
- [x] Native HEVC playback (VideoView) with seeking
- [x] First-frame thumbnails (MediaMetadataRetriever)
- [x] Share sheet (`video/mp4` via FileProvider)
- [x] Delete from drone / phone
- [ ] In-app Wi-Fi join wired to drone AP (pending drone "phone-transfer mode")
- [ ] End-to-end test against the AP, then package as a Ruby-update zip

## V1 — Basic Video Viewer (current)

Core functionality: plug in phone via USB-C, see what the pilot sees.
Expand Down
27 changes: 24 additions & 3 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,41 @@ android {
applicationId "com.rubyfpv.viewer"
minSdk 24
targetSdk 35
versionCode 1
versionName "1.0"
versionCode 10
versionName "2.0"
}

buildFeatures {
buildConfig = true
}

buildTypes {
release {
minifyEnabled = true
shrinkResources = true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

// Stamp the version into the APK filename, e.g. RubyFPV-v1.2-b3-debug.apk
applicationVariants.all { variant ->
variant.outputs.all { output ->
outputFileName = "RubyFPV-v${variant.versionName}-b${variant.versionCode}-${variant.buildType.name}.apk"
}
}
}

dependencies {
implementation libs.material
implementation libs.constraintlayout
implementation libs.swiperefreshlayout
implementation libs.jsch
implementation libs.media3.exoplayer
implementation libs.media3.ui
implementation libs.media3.transformer
}
5 changes: 5 additions & 0 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# JSch (mwiede fork) loads ciphers/kex/mac/hostkey classes reflectively by name.
-keep class com.jcraft.jsch.** { *; }
-keep class com.jcraft.jzlib.** { *; }
-dontwarn com.jcraft.jsch.**
-dontwarn org.ietf.jgss.**
46 changes: 43 additions & 3 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,21 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />

<!-- Read the Movies/RubyFPV gallery album (to list clips a prior install created) -->
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<!-- Pre-Q: write clips into the public Movies dir -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />

<application
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.RubyFPV"
android:usesCleartextTraffic="true">

<!-- Primary screen: download & preview onboard recordings -->
<activity
android:name=".MainActivity"
android:name=".recordings.RecordingsActivity"
android:exported="true"
android:screenOrientation="sensorLandscape"
android:theme="@style/Theme">
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<!-- Recorded clip playback -->
<activity
android:name=".player.PlaybackActivity"
android:exported="false"
android:configChanges="orientation|screenSize|keyboardHidden"
android:theme="@style/Theme.RubyFPV.Fullscreen" />

<!-- Secondary screen: live USB-tethered FPV view -->
<activity
android:name=".MainActivity"
android:exported="false"
android:screenOrientation="sensorLandscape"
android:theme="@style/Theme.Live" />

<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>

</manifest>
Loading
Loading