Low-latency Android receiver for live MPEG-TS over SRT. Built for professional live production: handshake, decode, surface — minimum moving parts, end-to-end target < 200 ms on LAN.
- libsrt 1.5.4 with AES encryption (via mbedtls 3.6.2), built from source per ABI
- Media3 / ExoPlayer 1.3.1 with MPEG-TS extractor and hardware H.264 / H.265 decode
- Kotlin 2.0 + Jetpack Compose UI; minSdk 26, target 34
- ABIs:
arm64-v8a,x86_64 - 16 KB page-size aligned (Android 15+ requirement)
Verified end-to-end against a vMix SRT listener (HEVC + AAC) on a Pixel 8. Encryption (passphrase / PBKEYLEN 128/192/256) works against vMix. Receiver-side TSBPD latency, bandwidth cap, and stream-ID are all wired through.
- Caller, Listener, Rendezvous modes — pick the one that matches your sender
- Caller: we initiate to the sender's address
- Listener: we bind a local port and wait for the sender to connect to us (UI shows the device's LAN IP so you know where to point the sender)
- Rendezvous: symmetric NAT-traversal handshake
- Adjustable receiver latency (slider 20–1000 ms, manual input up to 8000 ms)
- AES-128/192/256 passphrase support, key-length selectable
- Live stats overlay: RTT, bitrate, packet loss (colour-coded), jitter, retx count
- Codec / resolution / sample-rate readout from
Player.Listener.onTracksChanged - Aspect-ratio auto-detect via
AspectRatioFrameLayout - Auto-reconnect on network drops (exponential backoff 200 ms → 5 s, infinite retries)
- Faster reconnect on Wi-Fi roaming / network swap via
ConnectivityManager.NetworkCallback - Lifecycle-driven reconnect on app resume (skipped during PiP transitions)
- Picture-in-Picture support — playback survives Home / Recents without audio interruption
- Proper AudioFocus integration (auto-pause on call / notification, ducking, Bluetooth routing, Media-volume rocker)
- Wi-Fi performance lock while a session is active to suppress radio power-save jitter
- Software-decoder toggle in settings — escape hatch when a buggy HW
decoder breaks playback. By default HW decode is used, but
c2.exynos.avc.decoderis automatically excluded on Pixel 8 / 9 to avoid the known green-tearing / freeze bug on live MPEG-TS H.264 - Immersive fullscreen, lock mode (long-press to unlock)
- Swipe-to-adjust brightness (left half) and volume (right half)
KarPlayer/
├── app/ Application module, DI, MainActivity, launcher icon
├── srt/ JNI bridge + libsrt + mbedtls (built via ExternalProject_Add)
├── player/ ExoPlayer integration, LoadControl, MediaInfo state, auto-reconnect
└── ui/ Compose: ConnectionScreen, PlayerScreen, ViewModel
- JDK 17 (OpenJDK / Temurin)
- Android SDK with:
platforms;android-34build-tools;34.0.0ndk;27.3.13750724(NDK r27 or newer — required for 16 KB-alignedlibc++_shared.so)cmake;3.22.1
- Linux / macOS / WSL2 recommended. Native windows builds work too if your CMake / NDK paths are POSIX-clean.
./gradlew :app:assembleDebugNote the first build is slow (~10 min): libsrt and mbedtls are cloned
shallow from upstream at the tags pinned in
srt/src/main/cpp/CMakeLists.txt and cross-compiled per ABI. Subsequent
builds reuse the cached EP artifacts.
./gradlew :app:assembleReleaseRelease signing is loaded from keystore.properties at the repo root
(gitignored). To produce a signed release on a fresh checkout, generate
your own keystore and create the properties file:
keytool -genkeypair -keystore karplayer-release.keystore \
-alias karplayer -keyalg RSA -keysize 4096 -validity 10000 \
-dname "CN=Your Name, O=KarPlayer, C=US"
cat > keystore.properties <<EOF
storeFile=karplayer-release.keystore
storePassword=<your-password>
keyAlias=karplayer
keyPassword=<your-password>
EOFIf keystore.properties is absent, release builds fall back to the standard
Android debug keystore so local development still works — but you cannot
distribute that APK publicly.
R8 minification is disabled by default; the file app/proguard-rules.pro
is a placeholder for when you turn it on.
Edit the SRT_VERSION / MBEDTLS_VERSION values in
srt/src/main/cpp/CMakeLists.txt (or override -DSRT_VERSION=... in
srt/build.gradle.kts). Then either:
rm -rf srt/.cxx srt/build # full reset
# or just the ExternalProject caches:
rm -rf srt/.cxx/Debug/*/srt-ep srt/.cxx/Debug/*/mbedtls-ep…and rebuild.
ffmpeg -re -f lavfi -i testsrc=size=1280x720:rate=30 \
-f lavfi -i sine=frequency=440 \
-c:v libx264 -preset ultrafast -tune zerolatency \
-c:a aac \
-f mpegts "srt://0.0.0.0:9000?mode=listener&latency=120"In the app: Mode = Caller, Host = your machine's LAN IP, Port = 9000, Latency = 120.
ffmpeg -re -f lavfi -i testsrc=size=1280x720:rate=30 \
-f lavfi -i sine=frequency=440 \
-c:v libx264 -preset ultrafast -tune zerolatency \
-c:a aac \
-f mpegts "srt://<phone-ip>:9000?mode=caller&latency=120"In the app: Mode = Listener, leave Host empty (binds 0.0.0.0), Port = 9000. The ConnectionScreen shows the phone's LAN IP under the host field — point the sender at it. This is the right configuration for production setups like vMix, where the encoder pushes to the phone.
SrtDataSource↔ ExoPlayer: SRT live mode delivers fixed-size payloads (1316 bytes by default). Media3 extractors callDataSource.readwith arbitrary lengths (down to a few bytes during track sniffing). We absorb that mismatch with an internalrxBufinSrtSocket.kt: each underlyingsrt_recvpulls a full message; the caller is served incrementally.- TRANSTYPE first:
SRTO_TRANSTYPE = SRTT_LIVEis set before any other option per the libsrt configuration guidelines — it bulk-presetsMESSAGEAPI,TSBPDMODE,TLPKTDROP, andPAYLOADSIZE. - State race fix: ExoPlayer's
STATE_IDLEevent fromplayer.stop()duringdisconnect()is ignored while we're inCONNECTING/RECONNECTING, otherwise it would clobber the new attempt. - 16 KB alignment: All native
.sofiles are linked with-Wl,-z,max-page-size=16384(applied viaCMAKE_SHARED_LINKER_FLAGSto both libsrt's and our own JNI library).
- No Android TV launcher integration yet — leanback uses-feature,
LEANBACK_LAUNCHERintent filter, banner, and D-pad-first focus order are all pending. Sideloading and using with a phone or touch screen works. - No PiP / background audio when the app is minimised — current behaviour drops the SRT socket and reconnects on resume.
- No stream recording (writing the received MPEG-TS to disk).
- No multipath / bonding on RX (libsrt bonding is compiled in but not used).
pktRcvRetransTotalis unavailable in this libsrt minor — we report the sender-side counter (pktRetransTotal) as a stand-in.- Jitter in the overlay is a proxy (
msRcvBuffromSRT_TRACEBSTATS); true per-packet PCR jitter would have to be measured on the TS layer.
Apache License 2.0 — see LICENSE.
Third-party components and their licenses are listed in NOTICE.
This project was developed with extensive AI assistance (Anthropic Claude).
Architecture decisions, the libsrt + mbedtls integration, the JNI layer, the
Compose UI, and most of the iteration on protocol-level issues happened in
pair-programming sessions. Every commit carries a Co-Authored-By trailer
reflecting that. Human selection, structure, review, and end-to-end
verification on real hardware are mine.
Alexander Karabatov