From 36841d0b48eee83222539c19884f0a1d8123cef2 Mon Sep 17 00:00:00 2001 From: huanglinhuan Date: Thu, 18 Dec 2025 23:50:53 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0Android=E7=AB=AF=E6=8E=A5?= =?UTF-8?q?=E6=94=B6demo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- demo/android_receiver/app/build.gradle | 49 ++++++ .../app/src/main/AndroidManifest.xml | 28 ++++ .../app/src/main/cpp/CMakeLists.txt | 36 +++++ .../app/src/main/cpp/ReceiverEngine.cpp | 47 ++++++ .../app/src/main/cpp/ReceiverEngine.h | 24 +++ .../app/src/main/cpp/UdpReceiver.cpp | 153 ++++++++++++++++++ .../app/src/main/cpp/UdpReceiver.h | 55 +++++++ .../app/src/main/cpp/VideoDecoder.cpp | 93 +++++++++++ .../app/src/main/cpp/VideoDecoder.h | 23 +++ .../app/src/main/cpp/native-lib.cpp | 46 ++++++ .../com/displayflow/receiver/MainActivity.kt | 60 +++++++ demo/android_receiver/build.gradle | 15 ++ demo/android_receiver/settings.gradle | 2 + 13 files changed, 631 insertions(+) create mode 100644 demo/android_receiver/app/build.gradle create mode 100644 demo/android_receiver/app/src/main/AndroidManifest.xml create mode 100644 demo/android_receiver/app/src/main/cpp/CMakeLists.txt create mode 100644 demo/android_receiver/app/src/main/cpp/ReceiverEngine.cpp create mode 100644 demo/android_receiver/app/src/main/cpp/ReceiverEngine.h create mode 100644 demo/android_receiver/app/src/main/cpp/UdpReceiver.cpp create mode 100644 demo/android_receiver/app/src/main/cpp/UdpReceiver.h create mode 100644 demo/android_receiver/app/src/main/cpp/VideoDecoder.cpp create mode 100644 demo/android_receiver/app/src/main/cpp/VideoDecoder.h create mode 100644 demo/android_receiver/app/src/main/cpp/native-lib.cpp create mode 100644 demo/android_receiver/app/src/main/java/com/displayflow/receiver/MainActivity.kt create mode 100644 demo/android_receiver/build.gradle create mode 100644 demo/android_receiver/settings.gradle 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 +#include + +struct FrameHeader { + uint64_t timestamp; + uint32_t width; + uint32_t height; + uint32_t type; // 0: Unknown, 1: I-Frame, 2: P-Frame + uint32_t size; +}; + +// Callback for full frames +using OnFrameReceivedCallback = std::function& frameData, const FrameHeader& header)>; + +class UdpReceiver { +public: + UdpReceiver(); + ~UdpReceiver(); + + bool Start(int port); + void Stop(); + void SetCallback(OnFrameReceivedCallback callback); + +private: + void ReceiveLoop(); + + int sockfd_ = -1; + std::atomic running_{false}; + std::thread worker_thread_; + OnFrameReceivedCallback callback_; + + // Fragmentation handling + struct Fragment { + uint16_t fragId; + std::vector data; + }; + + struct PendingFrame { + uint32_t frameId; + uint16_t totalFrags; + uint16_t receivedFrags; + std::map> fragments; + FrameHeader header; + std::chrono::steady_clock::time_point startTime; + }; + + std::map pending_frames_; + std::mutex frames_mutex_; +}; diff --git a/demo/android_receiver/app/src/main/cpp/VideoDecoder.cpp b/demo/android_receiver/app/src/main/cpp/VideoDecoder.cpp new file mode 100644 index 0000000..24cc946 --- /dev/null +++ b/demo/android_receiver/app/src/main/cpp/VideoDecoder.cpp @@ -0,0 +1,93 @@ +#include "VideoDecoder.h" +#include +#include + +#define LOG_TAG "VideoDecoder" +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) + +VideoDecoder::VideoDecoder() {} + +VideoDecoder::~VideoDecoder() { + Release(); +} + +bool VideoDecoder::Initialize(ANativeWindow* window, int width, int height) { + if (codec_) { + Release(); + } + + width_ = width; + height_ = height; + + codec_ = AMediaCodec_createDecoderByType("video/avc"); + if (!codec_) { + LOGE("Failed to create decoder for video/avc"); + return false; + } + + AMediaFormat* format = AMediaFormat_new(); + AMediaFormat_setString(format, AMEDIAFORMAT_KEY_MIME, "video/avc"); + AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_WIDTH, width); + AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_HEIGHT, height); + + // Low latency config + AMediaFormat_setInt32(format, "low-latency", 1); + AMediaFormat_setInt32(format, "priority", 0); + + media_status_t status = AMediaCodec_configure(codec_, format, window, nullptr, 0); + AMediaFormat_delete(format); + + if (status != AMEDIA_OK) { + LOGE("Failed to configure codec: %d", status); + return false; + } + + status = AMediaCodec_start(codec_); + if (status != AMEDIA_OK) { + LOGE("Failed to start codec: %d", status); + return false; + } + + is_configured_ = true; + LOGI("Decoder initialized: %dx%d", width, height); + return true; +} + +void VideoDecoder::Decode(const uint8_t* data, size_t size, uint64_t timestamp) { + if (!is_configured_ || !codec_) return; + + // Get Input Buffer + ssize_t bufIdx = AMediaCodec_dequeueInputBuffer(codec_, 2000); + if (bufIdx >= 0) { + size_t bufSize; + uint8_t* buf = AMediaCodec_getInputBuffer(codec_, bufIdx, &bufSize); + if (buf && size <= bufSize) { + memcpy(buf, data, size); + AMediaCodec_queueInputBuffer(codec_, bufIdx, 0, size, timestamp, 0); + } + } + + // Handle Output (Rendering) + AMediaCodecBufferInfo info; + ssize_t outBufIdx = AMediaCodec_dequeueOutputBuffer(codec_, &info, 0); + while (outBufIdx >= 0) { + AMediaCodec_releaseOutputBuffer(codec_, outBufIdx, true); // true = render to surface + outBufIdx = AMediaCodec_dequeueOutputBuffer(codec_, &info, 0); + } + + if (outBufIdx == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED) { + auto format = AMediaCodec_getOutputFormat(codec_); + LOGI("Output format changed: %s", AMediaFormat_toString(format)); + AMediaFormat_delete(format); + } +} + +void VideoDecoder::Release() { + if (codec_) { + AMediaCodec_stop(codec_); + AMediaCodec_delete(codec_); + codec_ = nullptr; + } + is_configured_ = false; +} diff --git a/demo/android_receiver/app/src/main/cpp/VideoDecoder.h b/demo/android_receiver/app/src/main/cpp/VideoDecoder.h new file mode 100644 index 0000000..2f28e11 --- /dev/null +++ b/demo/android_receiver/app/src/main/cpp/VideoDecoder.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include +#include +#include + +class VideoDecoder { +public: + VideoDecoder(); + ~VideoDecoder(); + + bool Initialize(ANativeWindow* window, int width, int height); + void Decode(const uint8_t* data, size_t size, uint64_t timestamp); + void Release(); + +private: + AMediaCodec* codec_ = nullptr; + bool is_configured_ = false; + int width_ = 0; + int height_ = 0; +}; diff --git a/demo/android_receiver/app/src/main/cpp/native-lib.cpp b/demo/android_receiver/app/src/main/cpp/native-lib.cpp new file mode 100644 index 0000000..2312dc2 --- /dev/null +++ b/demo/android_receiver/app/src/main/cpp/native-lib.cpp @@ -0,0 +1,46 @@ +#include +#include +#include "ReceiverEngine.h" + +extern "C" JNIEXPORT jlong JNICALL +Java_com_displayflow_receiver_MainActivity_nativeInit( + JNIEnv* env, + jobject /* this */, + jobject surface) { + auto engine = new ReceiverEngine(env, surface); + return reinterpret_cast(engine); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_displayflow_receiver_MainActivity_nativeStart( + JNIEnv* env, + jobject /* this */, + jlong enginePtr, + jint port) { + auto engine = reinterpret_cast(enginePtr); + if (engine) { + engine->Start(port); + } +} + +extern "C" JNIEXPORT void JNICALL +Java_com_displayflow_receiver_MainActivity_nativeStop( + JNIEnv* env, + jobject /* this */, + jlong enginePtr) { + auto engine = reinterpret_cast(enginePtr); + if (engine) { + engine->Stop(); + } +} + +extern "C" JNIEXPORT void JNICALL +Java_com_displayflow_receiver_MainActivity_nativeRelease( + JNIEnv* env, + jobject /* this */, + jlong enginePtr) { + auto engine = reinterpret_cast(enginePtr); + if (engine) { + delete engine; + } +} diff --git a/demo/android_receiver/app/src/main/java/com/displayflow/receiver/MainActivity.kt b/demo/android_receiver/app/src/main/java/com/displayflow/receiver/MainActivity.kt new file mode 100644 index 0000000..9f6d015 --- /dev/null +++ b/demo/android_receiver/app/src/main/java/com/displayflow/receiver/MainActivity.kt @@ -0,0 +1,60 @@ +package com.displayflow.receiver + +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import android.view.Surface +import android.view.SurfaceHolder +import android.view.SurfaceView +import android.view.WindowManager +import android.widget.FrameLayout + +class MainActivity : AppCompatActivity(), SurfaceHolder.Callback { + + private lateinit var surfaceView: SurfaceView + private var nativeEngine: Long = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Keep screen on + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + + // Create layout programmatically + val layout = FrameLayout(this) + surfaceView = SurfaceView(this) + layout.addView(surfaceView) + setContentView(layout) + + surfaceView.holder.addCallback(this) + } + + override fun surfaceCreated(holder: SurfaceHolder) { + // Init native engine with surface + nativeEngine = nativeInit(holder.surface) + nativeStart(nativeEngine, 8888) // Listen on port 8888 + } + + override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { + // Handle resize if needed + } + + override fun surfaceDestroyed(holder: SurfaceHolder) { + if (nativeEngine != 0L) { + nativeStop(nativeEngine) + nativeRelease(nativeEngine) + nativeEngine = 0 + } + } + + // Native methods + external fun nativeInit(surface: Surface): Long + external fun nativeStart(enginePtr: Long, port: Int) + external fun nativeStop(enginePtr: Long) + external fun nativeRelease(enginePtr: Long) + + companion object { + init { + System.loadLibrary("receiver-lib") + } + } +} diff --git a/demo/android_receiver/build.gradle b/demo/android_receiver/build.gradle new file mode 100644 index 0000000..b0b767d --- /dev/null +++ b/demo/android_receiver/build.gradle @@ -0,0 +1,15 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath "com.android.tools.build:gradle:7.0.4" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10" + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/demo/android_receiver/settings.gradle b/demo/android_receiver/settings.gradle new file mode 100644 index 0000000..3a260f2 --- /dev/null +++ b/demo/android_receiver/settings.gradle @@ -0,0 +1,2 @@ +include ':app' +rootProject.name = "AndroidReceiverDemo"