This branch adds optional stream capture for offline analysis:
- Raw encoded video units (
video.<ext>) - Per-sample index records (
sample_index.jsonl) - Per-frame stats records (
frame_stats.jsonl, optional) - Rolling network status records (
network_stats.jsonl, optional) - Session metadata (
session.json)
- Ensure Android NDK is configured in
local.properties(replace{HOME}):
ndk.dir={HOME}/Android/Sdk/ndk/27.0.12077973
sdk.dir={HOME}/Android/Sdk
- Fetch submodules:
git submodule update --init --recursive- Apply the
moonlight-common-cpatch (adds the RTP video packet-loss counters used bynetwork_stats.jsonl). The submodule tracks upstreammoonlight-stream/moonlight-common-cat8af4562. The modification is kept as a patch rather than a separate submodule fork:
git apply --directory=app/src/main/jni/moonlight-core/moonlight-common-c \
patches/moonlight-common-c-netstats.patch- Build (JDK 17+):
./gradlew :app:assembleNonRootDebug # or :app:assembleRootDebug- Install:
adb install app/build/outputs/apk/nonRoot/debug/app-nonRoot-debug.apk- Open Moonlight on the device.
- In Settings, enable
Capture received video. - Set
Capture size limit per session. - Optional: keep
Save per-frame statisticsenabled forframe_stats.jsonl. - Start a stream.
Capture output location:
/storage/emulated/0/Android/data/com.limelight.debug/files/captures/
Each stream creates a session directory named like:
yyyyMMdd-HHmmss-<codec>-<width>x<height>@<fps>
One metadata file per session.
Fields:
created_wall_timecodec(h264,hevc,av1, orunknown)width,height,fpsvideo_format_maskcapture_mode(raw_only)bitstream_filesample_index_filesession_dir
Raw decode units appended in receive order.
Extension mapping:
.h264for H.264.h265for HEVC.av1for AV1.binfallback
One JSON object per line. Every record includes:
eventseq(monotonic index sequence)uptime_ms(Android uptime timestamp)
Event payloads:
session_start:codec,width,height,fps,cap_bytes,bitstream_file,capture_modesample:frame_number,frame_type,pts_us,receive_time_ms,enqueue_time_ms,file_offset,sample_size,bitstream_filecsd:decode_unit_type,frame_number,pts_us,file_offset,sample_size,bitstream_filesession_end:reason,estimated_video_bytes,cap_reached
Written only when Save per-frame statistics is enabled. Every record includes:
eventuptime_ms
Event payloads:
session_start:codec,width,height,fps,cap_bytes,video_file,sample_index_filecapture_mode:video_format,reasonframe_received:frame_number,frame_type,decode_unit_length,host_processing_latency_0_1ms,receive_time_ms,enqueue_time_ms,pts_usframe_decoded:frame_number,frame_type,pts_us,decoder_latency_ms,queue_delay_ms(or fallback-1for frame metadata when unmatched)network_frame_drop:previous_frame_number,next_frame_number,dropped_frame_count,first_dropped_frame_number,last_dropped_frame_numberjitter_frame_drop:frame_number,frame_type,pts_us,reason(or fallback-1for frame metadata when unmatched)capture_stopped:reasonsession_end:reason,estimated_video_bytes,cap_reached
Written only when Save per-frame statistics is enabled. Captures the rolling network
condition (RTT and downlink video packet loss) sampled roughly once per second, rather than
per frame. Every record includes:
eventuptime_ms
Event payloads:
session_start:codec,width,height,fpsnetwork_stats:enet_rtt_ms,enet_rtt_variance_ms— ENet control-channel RTT estimate at sample time (-1if the peer is not connected)expected_data_packets,received_data_packets,lost_data_packets— cumulative video data-packet counts since connection start (lost = expected - received)expected_parity_packets,received_parity_packets,lost_parity_packets— cumulative FEC parity-packet countswindow_expected_data_packets,window_received_data_packets,window_lost_data_packets— deltas over the sample windowwindow_expected_parity_packets,window_received_parity_packets— parity deltas over the windowwindow_data_loss_ratio—window_lost_data_packets / window_expected_data_packetsfor the window (0on the first sample)
session_end:reason
frame_receivedand index records are emitted when decode units arrive at the decoder input path (onDecodeUnit).frame_decodedis emitted when the frame is released fromMediaCodecoutput (onFrameDecoded), using PTS to match received metadata.network_frame_dropis emitted when frame-number gaps are detected at the decoder input path.jitter_frame_dropis emitted when decoded output is discarded by renderer pacing before display submission.network_statsis sampled at the ~1 s stats-window flip (not per frame): RTT comes from ENet (MoonBridge.getEstimatedRttInfo) and packet counts from the RTP layer (MoonBridge.getVideoPacketStats). The packet counters are cumulative and FEC-aware; "expected" is booked per FEC block as frames arrive, so frames never fully received still count toward loss.- Raw bytes are written continuously until the session ends or the configured size cap is reached.
- Hitting the cap stops capture only (streaming continues), records
capture_stopped/session_end, and shows a toast on device.
adb pull /storage/emulated/0/Android/data/com.limelight.debug/files/captures/WIP: Add tooling to reconstruct frames (for example YUV) using sample_index.jsonl + video.<ext>.
Moonlight for Android is an open source client for NVIDIA GameStream and Sunshine.
Moonlight for Android will allow you to stream your full collection of games from your Windows PC to your Android device, whether in your own home or over the internet.
Moonlight also has a PC client and iOS/tvOS client.
You can follow development on our Discord server and help translate Moonlight into your language on Weblate.
- Install Android Studio and the Android NDK
- Run ‘git submodule update --init --recursive’ from within moonlight-android/
- In moonlight-android/, create a file called ‘local.properties’. Add an ‘ndk.dir=’ property to the local.properties file and set it equal to your NDK directory.
- Build the APK using Android Studio or gradle
Moonlight is the work of students at Case Western and was started as a project at MHacks.