diff --git a/demo/android_receiver/app/build.gradle b/demo/android_receiver/app/build.gradle
new file mode 100644
index 0000000..54bf63a
--- /dev/null
+++ b/demo/android_receiver/app/build.gradle
@@ -0,0 +1,49 @@
+plugins {
+ id 'com.android.application'
+ id 'kotlin-android'
+}
+
+android {
+ compileSdk 31
+
+ defaultConfig {
+ applicationId "com.displayflow.receiver"
+ minSdk 24
+ targetSdk 31
+ versionCode 1
+ versionName "1.0"
+
+ externalNativeBuild {
+ cmake {
+ cppFlags "-std=c++17"
+ }
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+ externalNativeBuild {
+ cmake {
+ path file('src/main/cpp/CMakeLists.txt')
+ version '3.18.1'
+ }
+ }
+}
+
+dependencies {
+ implementation 'androidx.core:core-ktx:1.7.0'
+ implementation 'androidx.appcompat:appcompat:1.4.1'
+ implementation 'com.google.android.material:material:1.5.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
+}
diff --git a/demo/android_receiver/app/src/main/AndroidManifest.xml b/demo/android_receiver/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..cd3ac28
--- /dev/null
+++ b/demo/android_receiver/app/src/main/AndroidManifest.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demo/android_receiver/app/src/main/cpp/CMakeLists.txt b/demo/android_receiver/app/src/main/cpp/CMakeLists.txt
new file mode 100644
index 0000000..77e73ab
--- /dev/null
+++ b/demo/android_receiver/app/src/main/cpp/CMakeLists.txt
@@ -0,0 +1,36 @@
+cmake_minimum_required(VERSION 3.10.2)
+
+project("receiver-lib")
+
+add_library( # Sets the name of the library.
+ receiver-lib
+
+ # Sets the library as a shared library.
+ SHARED
+
+ # Provides a relative path to your source file(s).
+ native-lib.cpp
+ UdpReceiver.cpp
+ VideoDecoder.cpp
+ ReceiverEngine.cpp
+)
+
+find_library( # Sets the name of the path variable.
+ log-lib
+
+ # Specifies the name of the NDK library that
+ # you want CMake to locate.
+ log )
+
+find_library( android-lib android )
+find_library( mediandk-lib mediandk )
+
+target_link_libraries( # Specifies the target library.
+ receiver-lib
+
+ # Links the target library to the log library
+ # included in the NDK.
+ ${log-lib}
+ ${android-lib}
+ ${mediandk-lib}
+)
diff --git a/demo/android_receiver/app/src/main/cpp/ReceiverEngine.cpp b/demo/android_receiver/app/src/main/cpp/ReceiverEngine.cpp
new file mode 100644
index 0000000..9377868
--- /dev/null
+++ b/demo/android_receiver/app/src/main/cpp/ReceiverEngine.cpp
@@ -0,0 +1,47 @@
+#include "ReceiverEngine.h"
+#include
+
+#define LOG_TAG "ReceiverEngine"
+#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
+
+ReceiverEngine::ReceiverEngine(JNIEnv* env, jobject surface) {
+ window_ = ANativeWindow_fromSurface(env, surface);
+
+ // Bind callback
+ receiver_.SetCallback([this](const std::vector& data, const FrameHeader& header) {
+ this->OnFrameReceived(data, header);
+ });
+}
+
+ReceiverEngine::~ReceiverEngine() {
+ Stop();
+ if (window_) {
+ ANativeWindow_release(window_);
+ window_ = nullptr;
+ }
+}
+
+void ReceiverEngine::Start(int port) {
+ receiver_.Start(port);
+}
+
+void ReceiverEngine::Stop() {
+ receiver_.Stop();
+ decoder_.Release();
+}
+
+void ReceiverEngine::OnFrameReceived(const std::vector& data, const FrameHeader& header) {
+ if (!decoder_initialized_) {
+ // Init decoder on first frame (assuming we get width/height from header)
+ // Note: Windows sender sends width/height in FrameHeader
+ if (header.width > 0 && header.height > 0) {
+ if (decoder_.Initialize(window_, header.width, header.height)) {
+ decoder_initialized_ = true;
+ }
+ }
+ }
+
+ if (decoder_initialized_) {
+ decoder_.Decode(data.data(), data.size(), header.timestamp);
+ }
+}
diff --git a/demo/android_receiver/app/src/main/cpp/ReceiverEngine.h b/demo/android_receiver/app/src/main/cpp/ReceiverEngine.h
new file mode 100644
index 0000000..1e3e17d
--- /dev/null
+++ b/demo/android_receiver/app/src/main/cpp/ReceiverEngine.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include
+#include
+#include
+#include "UdpReceiver.h"
+#include "VideoDecoder.h"
+
+class ReceiverEngine {
+public:
+ ReceiverEngine(JNIEnv* env, jobject surface);
+ ~ReceiverEngine();
+
+ void Start(int port);
+ void Stop();
+
+private:
+ void OnFrameReceived(const std::vector& data, const FrameHeader& header);
+
+ ANativeWindow* window_ = nullptr;
+ UdpReceiver receiver_;
+ VideoDecoder decoder_;
+ bool decoder_initialized_ = false;
+};
diff --git a/demo/android_receiver/app/src/main/cpp/UdpReceiver.cpp b/demo/android_receiver/app/src/main/cpp/UdpReceiver.cpp
new file mode 100644
index 0000000..582ee41
--- /dev/null
+++ b/demo/android_receiver/app/src/main/cpp/UdpReceiver.cpp
@@ -0,0 +1,153 @@
+#include "UdpReceiver.h"
+#include
+#include
+#include
+#include
+#include
+
+#define LOG_TAG "UdpReceiver"
+#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
+#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
+
+UdpReceiver::UdpReceiver() {}
+
+UdpReceiver::~UdpReceiver() {
+ Stop();
+}
+
+bool UdpReceiver::Start(int port) {
+ if (running_) return true;
+
+ sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
+ if (sockfd_ < 0) {
+ LOGE("Failed to create socket");
+ return false;
+ }
+
+ struct sockaddr_in addr;
+ memset(&addr, 0, sizeof(addr));
+ addr.sin_family = AF_INET;
+ addr.sin_port = htons(port);
+ addr.sin_addr.s_addr = INADDR_ANY;
+
+ if (bind(sockfd_, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
+ LOGE("Failed to bind socket");
+ close(sockfd_);
+ return false;
+ }
+
+ running_ = true;
+ worker_thread_ = std::thread(&UdpReceiver::ReceiveLoop, this);
+ return true;
+}
+
+void UdpReceiver::Stop() {
+ running_ = false;
+ if (sockfd_ >= 0) {
+ close(sockfd_);
+ sockfd_ = -1;
+ }
+ if (worker_thread_.joinable()) {
+ worker_thread_.join();
+ }
+}
+
+void UdpReceiver::SetCallback(OnFrameReceivedCallback callback) {
+ callback_ = callback;
+}
+
+void UdpReceiver::ReceiveLoop() {
+ std::vector buffer(65535);
+ LOGI("Receiver thread started");
+
+ while (running_) {
+ ssize_t received = recvfrom(sockfd_, buffer.data(), buffer.size(), 0, nullptr, nullptr);
+ if (received <= 0) {
+ if (running_) LOGE("Recv failed or socket closed");
+ continue;
+ }
+
+ if (received < 8) continue; // Header size check
+
+ // Parse Packet Header
+ uint32_t frameId = *reinterpret_cast(&buffer[0]);
+ uint16_t fragId = *reinterpret_cast(&buffer[4]);
+ uint16_t totalFrags = *reinterpret_cast(&buffer[6]);
+
+ size_t payloadSize = received - 8;
+ uint8_t* payload = &buffer[8];
+
+ std::lock_guard lock(frames_mutex_);
+ PendingFrame& frame = pending_frames_[frameId];
+
+ // Init frame if new
+ if (frame.fragments.empty()) {
+ frame.frameId = frameId;
+ frame.totalFrags = totalFrags;
+ frame.receivedFrags = 0;
+ frame.startTime = std::chrono::steady_clock::now();
+ }
+
+ // Store fragment
+ if (frame.fragments.find(fragId) == frame.fragments.end()) {
+ frame.fragments[fragId] = std::vector(payload, payload + payloadSize);
+ frame.receivedFrags++;
+
+ // If first fragment, parse Frame Header
+ if (fragId == 0 && payloadSize >= 24) {
+ frame.header.timestamp = *reinterpret_cast(payload);
+ frame.header.width = *reinterpret_cast(payload + 8);
+ frame.header.height = *reinterpret_cast(payload + 12);
+ frame.header.type = *reinterpret_cast(payload + 16);
+ frame.header.size = *reinterpret_cast(payload + 20);
+
+ // Remove header from payload for reassembly
+ // Note: Windows sender includes frame header in the H264 stream logic?
+ // Let's check sender logic.
+ // Sender logic:
+ // memcpy(packet.data() + 8, &header, sizeof(header));
+ // memcpy(packet.data() + 8 + sizeof(header), frameData, ...);
+ // So frag 0 contains: PacketHeader(8) + FrameHeader(24) + H264Data...
+
+ // We stored payload (FrameHeader + H264Data) in fragments[0].
+ // When reassembling, we should skip the FrameHeader (24 bytes) from frag 0.
+ }
+ }
+
+ // Check completion
+ if (frame.receivedFrags == frame.totalFrags) {
+ // Reassemble
+ std::vector fullFrame;
+ fullFrame.reserve(frame.header.size); // Approximate or exact
+
+ for (uint16_t i = 0; i < frame.totalFrags; ++i) {
+ if (frame.fragments.count(i)) {
+ const auto& fragData = frame.fragments[i];
+ if (i == 0) {
+ // Skip Frame Header (24 bytes)
+ if (fragData.size() > 24) {
+ fullFrame.insert(fullFrame.end(), fragData.begin() + 24, fragData.end());
+ }
+ } else {
+ fullFrame.insert(fullFrame.end(), fragData.begin(), fragData.end());
+ }
+ } else {
+ // Missing fragment? Should not happen if receivedFrags == totalFrags
+ LOGE("Logic error: Missing fragment %d", i);
+ }
+ }
+
+ if (callback_) {
+ callback_(fullFrame, frame.header);
+ }
+
+ pending_frames_.erase(frameId);
+ }
+
+ // Cleanup old frames (simple timeout mechanism could be added here)
+ // For now, just keep map size in check roughly?
+ if (pending_frames_.size() > 30) {
+ pending_frames_.erase(pending_frames_.begin()); // Remove oldest
+ }
+ }
+}
diff --git a/demo/android_receiver/app/src/main/cpp/UdpReceiver.h b/demo/android_receiver/app/src/main/cpp/UdpReceiver.h
new file mode 100644
index 0000000..acbcd79
--- /dev/null
+++ b/demo/android_receiver/app/src/main/cpp/UdpReceiver.h
@@ -0,0 +1,55 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include