diff --git a/demo/android_receiver/app/src/main/AndroidManifest.xml b/demo/android_receiver/app/src/main/AndroidManifest.xml
index 6429998..5626f29 100644
--- a/demo/android_receiver/app/src/main/AndroidManifest.xml
+++ b/demo/android_receiver/app/src/main/AndroidManifest.xml
@@ -5,6 +5,7 @@
+
GetJavaVM(&jvm_);
+ callbackObj_ = env->NewGlobalRef(callbackObj);
window_ = ANativeWindow_fromSurface(env, surface);
// Bind callback
receiver_.SetCallback([this](const std::vector& data, const FrameHeader& header) {
this->OnFrameReceived(data, header);
});
+
+ receiver_.SetSenderDetectedCallback([this](const std::string& ip, int port) {
+ this->NotifyJavaSenderDetected(ip, port);
+ });
}
ReceiverEngine::~ReceiverEngine() {
@@ -19,8 +25,43 @@ ReceiverEngine::~ReceiverEngine() {
ANativeWindow_release(window_);
window_ = nullptr;
}
+ if (callbackObj_ && jvm_) {
+ JNIEnv* env = nullptr;
+ if (jvm_->GetEnv((void**)&env, JNI_VERSION_1_6) == JNI_OK) {
+ env->DeleteGlobalRef(callbackObj_);
+ } else {
+ // If thread not attached, we leak? Or attach to delete?
+ // Usually destructor called on main thread or known thread.
+ }
+ }
}
+void ReceiverEngine::NotifyJavaSenderDetected(const std::string& ip, int port) {
+ if (!jvm_ || !callbackObj_) return;
+
+ JNIEnv* env = nullptr;
+ bool needsDetach = false;
+ int res = jvm_->GetEnv((void**)&env, JNI_VERSION_1_6);
+ if (res == JNI_EDETACHED) {
+ if (jvm_->AttachCurrentThread(&env, nullptr) != JNI_OK) return;
+ needsDetach = true;
+ }
+
+ jclass clazz = env->GetObjectClass(callbackObj_);
+ jmethodID methodId = env->GetMethodID(clazz, "onSenderDetected", "(Ljava/lang/String;I)V");
+ if (methodId) {
+ jstring jIp = env->NewStringUTF(ip.c_str());
+ env->CallVoidMethod(callbackObj_, methodId, jIp, port);
+ env->DeleteLocalRef(jIp);
+ }
+
+ if (needsDetach) {
+ jvm_->DetachCurrentThread();
+ }
+}
+
+
+
void ReceiverEngine::Start(int port) {
receiver_.Start(port);
}
diff --git a/demo/android_receiver/app/src/main/cpp/ReceiverEngine.h b/demo/android_receiver/app/src/main/cpp/ReceiverEngine.h
index 1e3e17d..d723faf 100644
--- a/demo/android_receiver/app/src/main/cpp/ReceiverEngine.h
+++ b/demo/android_receiver/app/src/main/cpp/ReceiverEngine.h
@@ -8,17 +8,25 @@
class ReceiverEngine {
public:
- ReceiverEngine(JNIEnv* env, jobject surface);
+ ReceiverEngine(JNIEnv* env, jobject surface, jobject callbackObj);
~ReceiverEngine();
void Start(int port);
void Stop();
+
+ // Callback to JNI
+ void SetSenderDetectedCallback(std::function cb);
private:
void OnFrameReceived(const std::vector& data, const FrameHeader& header);
+ void NotifyJavaSenderDetected(const std::string& ip, int port);
ANativeWindow* window_ = nullptr;
UdpReceiver receiver_;
VideoDecoder decoder_;
bool decoder_initialized_ = false;
+ std::function sender_cb_;
+
+ JavaVM* jvm_ = nullptr;
+ jobject callbackObj_ = nullptr;
};
diff --git a/demo/android_receiver/app/src/main/cpp/UdpReceiver.cpp b/demo/android_receiver/app/src/main/cpp/UdpReceiver.cpp
index 582ee41..a0b25a6 100644
--- a/demo/android_receiver/app/src/main/cpp/UdpReceiver.cpp
+++ b/demo/android_receiver/app/src/main/cpp/UdpReceiver.cpp
@@ -1,6 +1,7 @@
#include "UdpReceiver.h"
#include
#include
+#include
#include
#include
#include
@@ -56,17 +57,36 @@ void UdpReceiver::SetCallback(OnFrameReceivedCallback callback) {
callback_ = callback;
}
+void UdpReceiver::SetSenderDetectedCallback(OnSenderDetectedCallback callback) {
+ sender_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);
+ struct sockaddr_in senderAddr;
+ socklen_t senderLen = sizeof(senderAddr);
+ ssize_t received = recvfrom(sockfd_, buffer.data(), buffer.size(), 0, (struct sockaddr*)&senderAddr, &senderLen);
if (received <= 0) {
if (running_) LOGE("Recv failed or socket closed");
continue;
}
+ // Check sender IP
+ char ipStr[INET_ADDRSTRLEN];
+ inet_ntop(AF_INET, &(senderAddr.sin_addr), ipStr, INET_ADDRSTRLEN);
+ std::string senderIp(ipStr);
+
+ if (senderIp != last_sender_ip_) {
+ last_sender_ip_ = senderIp;
+ LOGI("New sender detected: %s", senderIp.c_str());
+ if (sender_callback_) {
+ sender_callback_(senderIp, 8889); // Assume TCP port 8889
+ }
+ }
+
if (received < 8) continue; // Header size check
// Parse Packet Header
diff --git a/demo/android_receiver/app/src/main/cpp/UdpReceiver.h b/demo/android_receiver/app/src/main/cpp/UdpReceiver.h
index acbcd79..a3304cc 100644
--- a/demo/android_receiver/app/src/main/cpp/UdpReceiver.h
+++ b/demo/android_receiver/app/src/main/cpp/UdpReceiver.h
@@ -17,6 +17,7 @@ struct FrameHeader {
// Callback for full frames
using OnFrameReceivedCallback = std::function& frameData, const FrameHeader& header)>;
+using OnSenderDetectedCallback = std::function;
class UdpReceiver {
public:
@@ -26,6 +27,7 @@ public:
bool Start(int port);
void Stop();
void SetCallback(OnFrameReceivedCallback callback);
+ void SetSenderDetectedCallback(OnSenderDetectedCallback callback);
private:
void ReceiveLoop();
@@ -34,6 +36,8 @@ private:
std::atomic running_{false};
std::thread worker_thread_;
OnFrameReceivedCallback callback_;
+ OnSenderDetectedCallback sender_callback_;
+ std::string last_sender_ip_;
// Fragmentation handling
struct Fragment {
diff --git a/demo/android_receiver/app/src/main/cpp/native-lib.cpp b/demo/android_receiver/app/src/main/cpp/native-lib.cpp
index 2312dc2..e3ce7f6 100644
--- a/demo/android_receiver/app/src/main/cpp/native-lib.cpp
+++ b/demo/android_receiver/app/src/main/cpp/native-lib.cpp
@@ -5,9 +5,9 @@
extern "C" JNIEXPORT jlong JNICALL
Java_com_displayflow_receiver_MainActivity_nativeInit(
JNIEnv* env,
- jobject /* this */,
+ jobject thiz,
jobject surface) {
- auto engine = new ReceiverEngine(env, surface);
+ auto engine = new ReceiverEngine(env, surface, thiz);
return reinterpret_cast(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
index 9f6d015..a67cf19 100644
--- 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
@@ -7,35 +7,65 @@ import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.WindowManager
import android.widget.FrameLayout
+import android.content.Intent
+import android.net.Uri
+import android.util.Log
+import android.view.Gravity
+import android.graphics.Color
+import android.widget.Button
class MainActivity : AppCompatActivity(), SurfaceHolder.Callback {
private lateinit var surfaceView: SurfaceView
private var nativeEngine: Long = 0
+ private lateinit var fileTransfer: TcpFileTransfer
+ private val REQUEST_CODE_PICK_FILE = 1001
+ private val REQUEST_CODE_PICK_FOLDER = 1002
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)
+ val btn = Button(this)
+ btn.text = "发送文件夹"
+ btn.textSize = 12f
+ btn.setTextColor(Color.WHITE)
+ btn.setBackgroundColor(0x66000000)
+ btn.alpha = 0.8f
+ btn.setPadding(24, 12, 24, 12)
+ val margin = (16 * resources.displayMetrics.density).toInt()
+ val lp = FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.WRAP_CONTENT,
+ FrameLayout.LayoutParams.WRAP_CONTENT,
+ Gravity.END or Gravity.BOTTOM
+ )
+ lp.setMargins(margin, margin, margin, margin)
+ layout.addView(btn, lp)
setContentView(layout)
surfaceView.holder.addCallback(this)
+
+ fileTransfer = TcpFileTransfer(this)
+
+ surfaceView.setOnLongClickListener {
+ pickFileToSend()
+ true
+ }
+ btn.setOnClickListener {
+ pickFolderToSend()
+ }
}
override fun surfaceCreated(holder: SurfaceHolder) {
- // Init native engine with surface
nativeEngine = nativeInit(holder.surface)
- nativeStart(nativeEngine, 8888) // Listen on port 8888
+ nativeStart(nativeEngine, 8888)
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
- // Handle resize if needed
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
@@ -46,6 +76,38 @@ class MainActivity : AppCompatActivity(), SurfaceHolder.Callback {
}
}
+ fun onSenderDetected(ip: String, port: Int) {
+ Log.i("MainActivity", "Sender detected: $ip:$port")
+ fileTransfer.connect(ip, port)
+ }
+
+ private fun pickFileToSend() {
+ val intent = Intent(Intent.ACTION_GET_CONTENT)
+ intent.addCategory(Intent.CATEGORY_OPENABLE)
+ intent.type = "*/*"
+ startActivityForResult(Intent.createChooser(intent, "选择要发送的文件"), REQUEST_CODE_PICK_FILE)
+ }
+ private fun pickFolderToSend() {
+ val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
+ startActivityForResult(intent, REQUEST_CODE_PICK_FOLDER)
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ if (requestCode == REQUEST_CODE_PICK_FILE && resultCode == RESULT_OK) {
+ val uri: Uri? = data?.data
+ if (uri != null) {
+ fileTransfer.sendFile(uri)
+ }
+ }
+ if (requestCode == REQUEST_CODE_PICK_FOLDER && resultCode == RESULT_OK) {
+ val uri: Uri? = data?.data
+ if (uri != null) {
+ fileTransfer.sendFolder(uri)
+ }
+ }
+ }
+
// Native methods
external fun nativeInit(surface: Surface): Long
external fun nativeStart(enginePtr: Long, port: Int)
diff --git a/demo/android_receiver/app/src/main/java/com/displayflow/receiver/TcpFileTransfer.kt b/demo/android_receiver/app/src/main/java/com/displayflow/receiver/TcpFileTransfer.kt
new file mode 100644
index 0000000..4964294
--- /dev/null
+++ b/demo/android_receiver/app/src/main/java/com/displayflow/receiver/TcpFileTransfer.kt
@@ -0,0 +1,301 @@
+package com.displayflow.receiver
+
+import android.content.Context
+import android.content.ContentValues
+import android.net.Uri
+import android.os.Environment
+import android.os.Build
+import android.os.Handler
+import android.os.Looper
+import android.util.Log
+import android.widget.Toast
+import android.provider.MediaStore
+import androidx.documentfile.provider.DocumentFile
+import java.io.DataInputStream
+import java.io.DataOutputStream
+import java.io.File
+import java.io.OutputStream
+import java.net.Socket
+import java.nio.charset.StandardCharsets
+
+class TcpFileTransfer(private val context: Context) {
+ private var socket: Socket? = null
+ private var dis: DataInputStream? = null
+ private var dos: DataOutputStream? = null
+ private var isConnected = false
+ private val mainHandler = Handler(Looper.getMainLooper())
+
+ fun connect(ip: String, port: Int) {
+ Thread {
+ try {
+ if (isConnected) return@Thread
+ Log.i("TcpFileTransfer", "Connecting to $ip:$port")
+ socket = Socket(ip, port)
+ dis = DataInputStream(socket!!.getInputStream())
+ dos = DataOutputStream(socket!!.getOutputStream())
+ isConnected = true
+ Log.i("TcpFileTransfer", "Connected")
+
+ // Start receiving loop
+ receiveLoop()
+ } catch (e: Exception) {
+ Log.e("TcpFileTransfer", "Connection failed", e)
+ }
+ }.start()
+ }
+
+ private fun receiveLoop() {
+ try {
+ while (isConnected) {
+ val type = dis!!.readUnsignedByte()
+ val sizeBytes = ByteArray(4)
+ dis!!.readFully(sizeBytes)
+ val realSize = java.nio.ByteBuffer.wrap(sizeBytes).order(java.nio.ByteOrder.LITTLE_ENDIAN).int
+
+ val payload = ByteArray(realSize)
+ if (realSize > 0) {
+ dis!!.readFully(payload)
+ }
+
+ when (type) {
+ 0x10 -> handleFileHeader(payload)
+ 0x11 -> handleFileData(payload)
+ 0x12 -> handleFileEnd()
+ 0x20 -> handleFolderHeader(payload)
+ 0x21 -> handleDirEntry(payload)
+ 0x22 -> handleFileHeaderV2(payload)
+ 0x23 -> { folderRootName = null }
+ }
+ }
+ } catch (e: Exception) {
+ Log.e("TcpFileTransfer", "Receive loop error", e)
+ disconnect()
+ }
+ }
+
+ private var currentFileOutputStream: OutputStream? = null
+ private var currentFileName: String = ""
+ private var currentFileSize: Long = 0
+ private var currentFilePath: File? = null
+ private var currentFileUri: Uri? = null
+ private var folderRootName: String? = null
+
+ private fun handleFileHeader(payload: ByteArray) {
+ val buffer = java.nio.ByteBuffer.wrap(payload)
+ buffer.order(java.nio.ByteOrder.LITTLE_ENDIAN)
+
+ currentFileSize = buffer.long
+ val nameBytes = ByteArray(256)
+ buffer.get(nameBytes)
+
+ val nameString = String(nameBytes, StandardCharsets.UTF_8).trim { it <= ' ' }
+ currentFileName = nameString.substringBefore('\u0000')
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ val values = ContentValues().apply {
+ put(MediaStore.Downloads.DISPLAY_NAME, currentFileName)
+ put(MediaStore.Downloads.MIME_TYPE, "application/octet-stream")
+ put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
+ }
+ val uri = context.contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
+ if (uri != null) {
+ currentFileOutputStream = context.contentResolver.openOutputStream(uri)
+ currentFileUri = uri
+ Log.i("TcpFileTransfer", "Receiving file to public Downloads (MediaStore): $currentFileName ($currentFileSize bytes)")
+ } else {
+ Log.e("TcpFileTransfer", "Failed to create MediaStore entry for $currentFileName")
+ }
+ } else {
+ val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
+ val file = File(downloadsDir, currentFileName)
+ Log.i("TcpFileTransfer", "Receiving file to public Downloads: ${file.absolutePath} ($currentFileSize bytes)")
+ currentFileOutputStream = file.outputStream()
+ currentFilePath = file
+ }
+ }
+
+ private fun handleFileData(payload: ByteArray) {
+ currentFileOutputStream?.write(payload)
+ }
+
+ private fun handleFileEnd() {
+ currentFileOutputStream?.close()
+ currentFileOutputStream = null
+ Log.i("TcpFileTransfer", "File received successfully")
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ currentFileUri?.let {
+ mainHandler.post { Toast.makeText(context, "已保存到 下载: $currentFileName", Toast.LENGTH_LONG).show() }
+ }
+ } else {
+ currentFilePath?.let {
+ mainHandler.post { Toast.makeText(context, "已保存: ${it.absolutePath}", Toast.LENGTH_LONG).show() }
+ }
+ }
+ }
+ private fun handleFolderHeader(payload: ByteArray) {
+ val name = String(payload, StandardCharsets.UTF_8).substringBefore('\u0000')
+ folderRootName = if (name.isNotBlank()) name else "AndroidFolder"
+ }
+ private fun handleDirEntry(payload: ByteArray) {
+ val rel = String(payload, StandardCharsets.UTF_8).substringBefore('\u0000').replace('\\', '/')
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+ val base = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
+ val dir = File(base, (folderRootName ?: "AndroidFolder") + "/" + rel)
+ dir.mkdirs()
+ }
+ }
+ private fun handleFileHeaderV2(payload: ByteArray) {
+ val buf = java.nio.ByteBuffer.wrap(payload).order(java.nio.ByteOrder.LITTLE_ENDIAN)
+ val size = buf.long
+ val pathLen = buf.short.toInt() and 0xFFFF
+ val pathBytes = ByteArray(pathLen)
+ buf.get(pathBytes)
+ val rel = String(pathBytes, StandardCharsets.UTF_8).replace('\\', '/')
+ currentFileSize = size
+ val lastSlash = rel.lastIndexOf('/')
+ val parent = if (lastSlash >= 0) rel.substring(0, lastSlash) else ""
+ val fileName = if (lastSlash >= 0) rel.substring(lastSlash + 1) else rel
+ currentFileName = fileName
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ val relPath = if (parent.isNotBlank()) (Environment.DIRECTORY_DOWNLOADS + "/" + (folderRootName ?: "AndroidFolder") + "/" + parent)
+ else (Environment.DIRECTORY_DOWNLOADS + "/" + (folderRootName ?: "AndroidFolder"))
+ val values = ContentValues().apply {
+ put(MediaStore.Downloads.DISPLAY_NAME, currentFileName)
+ put(MediaStore.Downloads.MIME_TYPE, "application/octet-stream")
+ put(MediaStore.Downloads.RELATIVE_PATH, relPath)
+ }
+ val uri = context.contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
+ if (uri != null) {
+ currentFileOutputStream = context.contentResolver.openOutputStream(uri)
+ currentFileUri = uri
+ } else {
+ currentFileOutputStream = null
+ }
+ } else {
+ val base = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
+ val dir = File(base, (folderRootName ?: "AndroidFolder") + (if (parent.isNotBlank()) "/$parent" else ""))
+ dir.mkdirs()
+ val f = File(dir, currentFileName)
+ currentFileOutputStream = f.outputStream()
+ currentFilePath = f
+ }
+ }
+
+ fun sendFile(uri: Uri) {
+ Thread {
+ try {
+ if (!isConnected || dos == null) {
+ Log.e("TcpFileTransfer", "Not connected")
+ return@Thread
+ }
+
+ val cursor = context.contentResolver.query(uri, null, null, null, null)
+ val nameIndex = cursor?.getColumnIndex(android.provider.OpenableColumns.DISPLAY_NAME)
+ val sizeIndex = cursor?.getColumnIndex(android.provider.OpenableColumns.SIZE)
+
+ cursor?.moveToFirst()
+ val fileName = cursor?.getString(nameIndex ?: 0) ?: "unknown_file"
+ val fileSize = cursor?.getLong(sizeIndex ?: 0) ?: 0L
+ cursor?.close()
+
+ val inputStream = context.contentResolver.openInputStream(uri) ?: return@Thread
+
+ dos!!.writeByte(0x10)
+ dos!!.writeInt(Integer.reverseBytes(264))
+
+ val buffer = java.nio.ByteBuffer.allocate(264)
+ buffer.order(java.nio.ByteOrder.LITTLE_ENDIAN)
+ buffer.putLong(fileSize)
+ val nameBytes = fileName.toByteArray(StandardCharsets.UTF_8)
+ val finalNameBytes = ByteArray(256)
+ System.arraycopy(nameBytes, 0, finalNameBytes, 0, Math.min(nameBytes.size, 256))
+ buffer.put(finalNameBytes)
+
+ dos!!.write(buffer.array())
+
+ val chunk = ByteArray(64 * 1024)
+ var bytesRead: Int
+ while (inputStream.read(chunk).also { bytesRead = it } != -1) {
+ dos!!.writeByte(0x11)
+ dos!!.writeInt(Integer.reverseBytes(bytesRead))
+ dos!!.write(chunk, 0, bytesRead)
+ }
+ inputStream.close()
+
+ dos!!.writeByte(0x12)
+ dos!!.writeInt(0)
+
+ Log.i("TcpFileTransfer", "File sent successfully")
+
+ } catch (e: Exception) {
+ Log.e("TcpFileTransfer", "Send file failed", e)
+ }
+ }.start()
+ }
+ fun sendFolder(uri: Uri) {
+ Thread {
+ try {
+ if (!isConnected || dos == null) {
+ Log.e("TcpFileTransfer", "Not connected")
+ return@Thread
+ }
+ val root = DocumentFile.fromTreeUri(context, uri) ?: return@Thread
+ val rootName = root.name ?: "AndroidFolder"
+ val nameBytes = rootName.toByteArray(StandardCharsets.UTF_8)
+ dos!!.writeByte(0x20)
+ dos!!.writeInt(Integer.reverseBytes(nameBytes.size))
+ dos!!.write(nameBytes)
+ fun sendEntry(doc: DocumentFile, base: String) {
+ val rel = if (base.isEmpty()) (doc.name ?: "") else (base + "/" + (doc.name ?: ""))
+ if (doc.isDirectory) {
+ val pathBytes = rel.toByteArray(StandardCharsets.UTF_8)
+ dos!!.writeByte(0x21)
+ dos!!.writeInt(Integer.reverseBytes(pathBytes.size))
+ dos!!.write(pathBytes)
+ doc.listFiles().forEach { child ->
+ sendEntry(child, rel)
+ }
+ } else {
+ val size = doc.length()
+ val pathBytes = rel.toByteArray(StandardCharsets.UTF_8)
+ val headerSize = 8 + 2 + pathBytes.size
+ dos!!.writeByte(0x22)
+ dos!!.writeInt(Integer.reverseBytes(headerSize))
+ val bb = java.nio.ByteBuffer.allocate(headerSize)
+ bb.order(java.nio.ByteOrder.LITTLE_ENDIAN)
+ bb.putLong(size)
+ bb.putShort(pathBytes.size.toShort())
+ bb.put(pathBytes)
+ dos!!.write(bb.array())
+ val input = context.contentResolver.openInputStream(doc.uri)
+ if (input != null) {
+ val chunk = ByteArray(64 * 1024)
+ var bytesRead: Int
+ while (input.read(chunk).also { bytesRead = it } != -1) {
+ dos!!.writeByte(0x11)
+ dos!!.writeInt(Integer.reverseBytes(bytesRead))
+ dos!!.write(chunk, 0, bytesRead)
+ }
+ input.close()
+ }
+ dos!!.writeByte(0x12)
+ dos!!.writeInt(0)
+ }
+ }
+ root.listFiles().forEach { sendEntry(it, "") }
+ dos!!.writeByte(0x23)
+ dos!!.writeInt(0)
+ Log.i("TcpFileTransfer", "Folder sent successfully")
+ } catch (e: Exception) {
+ Log.e("TcpFileTransfer", "Send folder failed", e)
+ }
+ }.start()
+ }
+
+ fun disconnect() {
+ isConnected = false
+ try {
+ socket?.close()
+ } catch (e: Exception) {}
+ }
+}
diff --git a/demo/windows_sender/CMakeLists.txt b/demo/windows_sender/CMakeLists.txt
index e2be758..5bcce15 100644
--- a/demo/windows_sender/CMakeLists.txt
+++ b/demo/windows_sender/CMakeLists.txt
@@ -19,6 +19,9 @@ set(SOURCES
NetworkSender.h
IddBridge.cpp
IddBridge.h
+ TcpServer.cpp
+ TcpServer.h
+ FileTransferProtocol.h
)
add_executable(WindowsSenderDemo ${SOURCES})
diff --git a/demo/windows_sender/FileTransferProtocol.h b/demo/windows_sender/FileTransferProtocol.h
new file mode 100644
index 0000000..f5d7c13
--- /dev/null
+++ b/demo/windows_sender/FileTransferProtocol.h
@@ -0,0 +1,46 @@
+#pragma once
+
+#include
+
+// Port for TCP Control & File Transfer
+constexpr int FILE_TRANSFER_PORT = 8889;
+
+enum class PacketType : uint8_t {
+ // Control Events
+ Handshake = 0x01,
+ MouseEvent = 0x02,
+ KeyboardEvent = 0x03,
+
+ // File Transfer
+ FileHeader = 0x10, // Metadata: Name, Size
+ FileData = 0x11, // Chunk of data
+ FileEnd = 0x12, // End of transfer
+ FileAck = 0x13, // Acknowledge receipt
+ FileError = 0x14 // Error during transfer
+
+ ,
+ FolderHeader = 0x20,
+ DirEntry = 0x21,
+ FileHeaderV2 = 0x22,
+ FolderEnd = 0x23
+};
+
+#pragma pack(push, 1)
+
+struct CommonHeader {
+ uint8_t type;
+ uint32_t payloadSize;
+};
+
+// Payload for FileHeader (Type 0x10)
+struct FileMetadata {
+ uint64_t fileSize;
+ char fileName[256]; // UTF-8 encoded
+};
+
+// Payload for FileAck (Type 0x13)
+struct FileAckPayload {
+ uint8_t status; // 0 = OK, 1 = Error
+};
+
+#pragma pack(pop)
diff --git a/demo/windows_sender/TcpServer.cpp b/demo/windows_sender/TcpServer.cpp
new file mode 100644
index 0000000..e7c7c22
--- /dev/null
+++ b/demo/windows_sender/TcpServer.cpp
@@ -0,0 +1,418 @@
+#include "TcpServer.h"
+#include "FileTransferProtocol.h"
+#include
+#include
+#include
+#include
+
+#pragma comment(lib, "ws2_32.lib")
+
+namespace fs = std::filesystem;
+
+TcpServer::TcpServer() = default;
+
+TcpServer::~TcpServer() {
+ Stop();
+}
+
+bool TcpServer::Start(int port) {
+ if (running_) return true;
+
+ // Initialize Winsock
+ WSADATA wsaData;
+ if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
+ std::cerr << "WSAStartup failed" << std::endl;
+ return false;
+ }
+
+ listenSocket_ = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
+ if (listenSocket_ == INVALID_SOCKET) {
+ std::cerr << "Socket creation failed" << std::endl;
+ WSACleanup();
+ return false;
+ }
+
+ sockaddr_in serverAddr;
+ serverAddr.sin_family = AF_INET;
+ serverAddr.sin_addr.s_addr = INADDR_ANY;
+ serverAddr.sin_port = htons(port);
+
+ if (bind(listenSocket_, (sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
+ std::cerr << "Bind failed" << std::endl;
+ closesocket(listenSocket_);
+ WSACleanup();
+ return false;
+ }
+
+ if (listen(listenSocket_, 1) == SOCKET_ERROR) {
+ std::cerr << "Listen failed" << std::endl;
+ closesocket(listenSocket_);
+ WSACleanup();
+ return false;
+ }
+
+ running_ = true;
+ acceptThread_ = std::thread(&TcpServer::AcceptLoop, this);
+
+ std::cout << "TCP Server started on port " << port << std::endl;
+ return true;
+}
+
+void TcpServer::Stop() {
+ running_ = false;
+ if (listenSocket_ != INVALID_SOCKET) {
+ closesocket(listenSocket_);
+ listenSocket_ = INVALID_SOCKET;
+ }
+ if (clientSocket_ != INVALID_SOCKET) {
+ closesocket(clientSocket_);
+ clientSocket_ = INVALID_SOCKET;
+ }
+ if (acceptThread_.joinable()) acceptThread_.join();
+ if (clientThread_.joinable()) clientThread_.join();
+
+ // Close any open file stream
+ if (currentFileStream_) {
+ currentFileStream_->close();
+ delete currentFileStream_;
+ currentFileStream_ = nullptr;
+ }
+}
+
+void TcpServer::AcceptLoop() {
+ while (running_) {
+ sockaddr_in clientAddr;
+ int clientAddrLen = sizeof(clientAddr);
+ SOCKET client = accept(listenSocket_, (sockaddr*)&clientAddr, &clientAddrLen);
+
+ if (client == INVALID_SOCKET) {
+ if (running_) std::cerr << "Accept failed" << std::endl;
+ break;
+ }
+
+ std::cout << "Client connected for File Transfer" << std::endl;
+
+ // Close previous client if any
+ if (clientSocket_ != INVALID_SOCKET) {
+ closesocket(clientSocket_);
+ if (clientThread_.joinable()) clientThread_.join();
+ }
+
+ clientSocket_ = client;
+ clientThread_ = std::thread(&TcpServer::ClientHandler, this, clientSocket_);
+ }
+}
+
+void TcpServer::ClientHandler(SOCKET clientSocket) {
+ while (running_) {
+ CommonHeader header;
+ if (!ReceiveBytes(clientSocket, &header, sizeof(header))) {
+ std::cout << "Client disconnected" << std::endl;
+ break;
+ }
+
+ std::vector payload(header.payloadSize);
+ if (header.payloadSize > 0) {
+ if (!ReceiveBytes(clientSocket, payload.data(), header.payloadSize)) {
+ break;
+ }
+ }
+
+ PacketType type = static_cast(header.type);
+ switch (type) {
+ case PacketType::FileHeader:
+ HandleFileHeader(clientSocket, payload);
+ break;
+ case PacketType::FileData:
+ HandleFileData(payload);
+ break;
+ case PacketType::FileEnd:
+ HandleFileEnd();
+ break;
+ case PacketType::FolderHeader:
+ HandleFolderHeader(payload);
+ break;
+ case PacketType::DirEntry:
+ HandleDirEntry(payload);
+ break;
+ case PacketType::FileHeaderV2:
+ HandleFileHeaderV2(payload);
+ break;
+ case PacketType::FolderEnd:
+ break;
+ default:
+ break;
+ }
+ }
+ closesocket(clientSocket);
+ clientSocket_ = INVALID_SOCKET;
+}
+
+bool TcpServer::ReceiveBytes(SOCKET sock, void* buffer, int size) {
+ char* ptr = (char*)buffer;
+ int remaining = size;
+ while (remaining > 0) {
+ int received = recv(sock, ptr, remaining, 0);
+ if (received <= 0) return false;
+ ptr += received;
+ remaining -= received;
+ }
+ return true;
+}
+
+bool TcpServer::SendBytes(SOCKET sock, const void* buffer, int size) {
+ const char* ptr = (const char*)buffer;
+ int remaining = size;
+ while (remaining > 0) {
+ int sent = send(sock, ptr, remaining, 0);
+ if (sent <= 0) return false;
+ ptr += sent;
+ remaining -= sent;
+ }
+ return true;
+}
+
+void TcpServer::HandleFileHeader(SOCKET sock, const std::vector& payload) {
+ if (payload.size() < sizeof(FileMetadata)) return;
+ const FileMetadata* meta = reinterpret_cast(payload.data());
+
+ std::lock_guard lock(fileMutex_);
+
+ // Save to Desktop by default
+ std::string desktopPath = getenv("USERPROFILE");
+ desktopPath += "\\Desktop\\";
+
+ currentFileName_ = desktopPath + std::string(meta->fileName);
+ currentFileSize_ = meta->fileSize;
+ receivedBytes_ = 0;
+
+ if (currentFileStream_) {
+ delete currentFileStream_;
+ }
+ currentFileStream_ = new std::ofstream(currentFileName_, std::ios::binary);
+
+ std::cout << "Receiving file: " << currentFileName_ << " (" << currentFileSize_ << " bytes)" << std::endl;
+}
+
+void TcpServer::HandleFileData(const std::vector& payload) {
+ std::lock_guard lock(fileMutex_);
+ if (currentFileStream_ && currentFileStream_->is_open()) {
+ currentFileStream_->write((const char*)payload.data(), payload.size());
+ receivedBytes_ += payload.size();
+
+ // Optional: Progress log
+ // std::cout << "\rProgress: " << (receivedBytes_ * 100 / currentFileSize_) << "%" << std::flush;
+ }
+}
+
+void TcpServer::HandleFileEnd() {
+ std::lock_guard lock(fileMutex_);
+ if (currentFileStream_) {
+ currentFileStream_->close();
+ delete currentFileStream_;
+ currentFileStream_ = nullptr;
+ std::cout << "\nFile received successfully!" << std::endl;
+
+ if (fileReceivedCallback_) {
+ fileReceivedCallback_(currentFileName_);
+ }
+ }
+}
+
+bool TcpServer::SendFile(const std::string& filePath) {
+ if (clientSocket_ == INVALID_SOCKET) {
+ std::cerr << "No client connected" << std::endl;
+ return false;
+ }
+
+ std::ifstream file(filePath, std::ios::binary | std::ios::ate);
+ if (!file.is_open()) {
+ std::cerr << "Cannot open file: " << filePath << std::endl;
+ return false;
+ }
+
+ uint64_t fileSize = file.tellg();
+ file.seekg(0, std::ios::beg);
+
+ fs::path p(filePath);
+ std::string filename = p.filename().string();
+
+ // 1. Send Header
+ CommonHeader header;
+ header.type = (uint8_t)PacketType::FileHeader;
+ header.payloadSize = sizeof(FileMetadata);
+
+ FileMetadata meta;
+ meta.fileSize = fileSize;
+ strncpy_s(meta.fileName, filename.c_str(), sizeof(meta.fileName) - 1);
+
+ if (!SendBytes(clientSocket_, &header, sizeof(header))) return false;
+ if (!SendBytes(clientSocket_, &meta, sizeof(meta))) return false;
+
+ // 2. Send Data
+ const int CHUNK_SIZE = 64 * 1024; // 64KB
+ std::vector buffer(CHUNK_SIZE);
+
+ uint64_t sent = 0;
+ while (sent < fileSize) {
+ file.read(buffer.data(), CHUNK_SIZE);
+ int bytesRead = (int)file.gcount();
+
+ CommonHeader dataHeader;
+ dataHeader.type = (uint8_t)PacketType::FileData;
+ dataHeader.payloadSize = bytesRead;
+
+ if (!SendBytes(clientSocket_, &dataHeader, sizeof(dataHeader))) return false;
+ if (!SendBytes(clientSocket_, buffer.data(), bytesRead)) return false;
+
+ sent += bytesRead;
+ std::cout << "\rSending: " << (sent * 100 / fileSize) << "%" << std::flush;
+ }
+ std::cout << std::endl;
+
+ // 3. Send End
+ CommonHeader endHeader;
+ endHeader.type = (uint8_t)PacketType::FileEnd;
+ endHeader.payloadSize = 0;
+ SendBytes(clientSocket_, &endHeader, sizeof(endHeader));
+
+ std::cout << "File sent successfully" << std::endl;
+ return true;
+}
+
+void TcpServer::SetFileReceivedCallback(FileReceivedCallback cb) {
+ fileReceivedCallback_ = cb;
+}
+bool TcpServer::SendFolder(const std::string& folderPath) {
+ if (clientSocket_ == INVALID_SOCKET) {
+ std::cerr << "No client connected" << std::endl;
+ return false;
+ }
+ fs::path root(folderPath);
+ if (!fs::exists(root) || !fs::is_directory(root)) {
+ std::cerr << "Invalid folder: " << folderPath << std::endl;
+ return false;
+ }
+ std::string rootName = root.filename().string();
+ {
+ CommonHeader hdr;
+ hdr.type = (uint8_t)PacketType::FolderHeader;
+ hdr.payloadSize = (uint32_t)rootName.size();
+ if (!SendBytes(clientSocket_, &hdr, sizeof(hdr))) return false;
+ if (hdr.payloadSize > 0) {
+ if (!SendBytes(clientSocket_, rootName.data(), (int)rootName.size())) return false;
+ }
+ }
+ auto norm = [](std::string s) {
+ for (auto& c : s) if (c == '\\') c = '/';
+ return s;
+ };
+ for (auto it = fs::recursive_directory_iterator(root); it != fs::recursive_directory_iterator(); ++it) {
+ const fs::path p = it->path();
+ fs::path rel = fs::relative(p, root);
+ std::string relStr = norm(rel.string());
+ if (it->is_directory()) {
+ CommonHeader hdr;
+ hdr.type = (uint8_t)PacketType::DirEntry;
+ hdr.payloadSize = (uint32_t)relStr.size();
+ if (!SendBytes(clientSocket_, &hdr, sizeof(hdr))) return false;
+ if (hdr.payloadSize > 0) {
+ if (!SendBytes(clientSocket_, relStr.data(), (int)relStr.size())) return false;
+ }
+ } else if (it->is_regular_file()) {
+ uint64_t fileSize = (uint64_t)fs::file_size(p);
+ std::vector header;
+ uint16_t pathLen = (uint16_t)std::min(relStr.size(), 65535);
+ header.resize(8 + 2 + pathLen);
+ std::memcpy(header.data(), &fileSize, 8);
+ std::memcpy(header.data() + 8, &pathLen, 2);
+ std::memcpy(header.data() + 10, relStr.data(), pathLen);
+ CommonHeader hdr;
+ hdr.type = (uint8_t)PacketType::FileHeaderV2;
+ hdr.payloadSize = (uint32_t)header.size();
+ if (!SendBytes(clientSocket_, &hdr, sizeof(hdr))) return false;
+ if (!SendBytes(clientSocket_, header.data(), (int)header.size())) return false;
+ std::ifstream file(p, std::ios::binary);
+ if (!file.is_open()) continue;
+ const int CHUNK_SIZE = 64 * 1024;
+ std::vector buffer(CHUNK_SIZE);
+ while (file) {
+ file.read(buffer.data(), CHUNK_SIZE);
+ int bytesRead = (int)file.gcount();
+ if (bytesRead <= 0) break;
+ CommonHeader dh;
+ dh.type = (uint8_t)PacketType::FileData;
+ dh.payloadSize = bytesRead;
+ if (!SendBytes(clientSocket_, &dh, sizeof(dh))) { file.close(); return false; }
+ if (!SendBytes(clientSocket_, buffer.data(), bytesRead)) { file.close(); return false; }
+ }
+ file.close();
+ CommonHeader endH;
+ endH.type = (uint8_t)PacketType::FileEnd;
+ endH.payloadSize = 0;
+ if (!SendBytes(clientSocket_, &endH, sizeof(endH))) return false;
+ }
+ }
+ CommonHeader fin;
+ fin.type = (uint8_t)PacketType::FolderEnd;
+ fin.payloadSize = 0;
+ if (!SendBytes(clientSocket_, &fin, sizeof(fin))) return false;
+ std::cout << "Folder sent successfully" << std::endl;
+ return true;
+}
+
+void TcpServer::HandleFolderHeader(const std::vector& payload) {
+ std::string desktopPath = getenv("USERPROFILE");
+ desktopPath += "\\Desktop\\";
+ std::string rootName;
+ if (!payload.empty()) {
+ rootName.assign(reinterpret_cast(payload.data()), payload.size());
+ size_t pos = rootName.find('\0');
+ if (pos != std::string::npos) rootName = rootName.substr(0, pos);
+ } else {
+ rootName = "AndroidFolder";
+ }
+ baseFolderRoot_ = desktopPath + rootName;
+ try {
+ fs::create_directories(baseFolderRoot_);
+ } catch (...) {}
+ std::cout << "Receiving folder: " << baseFolderRoot_ << std::endl;
+}
+
+void TcpServer::HandleDirEntry(const std::vector& payload) {
+ if (baseFolderRoot_.empty()) return;
+ std::string rel;
+ rel.assign(reinterpret_cast(payload.data()), payload.size());
+ size_t pos = rel.find('\0');
+ if (pos != std::string::npos) rel = rel.substr(0, pos);
+ std::string path = baseFolderRoot_ + "\\" + rel;
+ try {
+ fs::create_directories(path);
+ } catch (...) {}
+ std::cout << "Create dir: " << path << std::endl;
+}
+
+void TcpServer::HandleFileHeaderV2(const std::vector& payload) {
+ if (payload.size() < sizeof(uint64_t) + sizeof(uint16_t)) return;
+ const uint8_t* p = payload.data();
+ uint64_t sz = *reinterpret_cast(p);
+ p += sizeof(uint64_t);
+ uint16_t pathLen = *reinterpret_cast(p);
+ p += sizeof(uint16_t);
+ if (payload.size() < sizeof(uint64_t) + sizeof(uint16_t) + pathLen) return;
+ std::string rel(reinterpret_cast(p), pathLen);
+ std::string full = baseFolderRoot_.empty() ? rel : (baseFolderRoot_ + "\\" + rel);
+ {
+ std::lock_guard lock(fileMutex_);
+ if (currentFileStream_) {
+ delete currentFileStream_;
+ currentFileStream_ = nullptr;
+ }
+ currentFileName_ = full;
+ currentFileSize_ = sz;
+ receivedBytes_ = 0;
+ fs::create_directories(fs::path(full).parent_path());
+ currentFileStream_ = new std::ofstream(full, std::ios::binary);
+ }
+ std::cout << "Receiving file: " << full << " (" << sz << " bytes)" << std::endl;
+}
diff --git a/demo/windows_sender/TcpServer.h b/demo/windows_sender/TcpServer.h
new file mode 100644
index 0000000..4063c9e
--- /dev/null
+++ b/demo/windows_sender/TcpServer.h
@@ -0,0 +1,59 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+class TcpServer {
+public:
+ TcpServer();
+ ~TcpServer();
+
+ bool Start(int port);
+ void Stop();
+
+ // Send a file to the connected client
+ bool SendFile(const std::string& filePath);
+ bool SendFolder(const std::string& folderPath);
+
+ // Set callback for received files
+ // Callback signature: (filename, data) -> void
+ // Note: For large files, we should probably stream to disk directly,
+ // but for simplicity in this demo, we might just notify path.
+ using FileReceivedCallback = std::function;
+ void SetFileReceivedCallback(FileReceivedCallback cb);
+
+private:
+ void AcceptLoop();
+ void ClientHandler(SOCKET clientSocket);
+ bool ReceiveBytes(SOCKET sock, void* buffer, int size);
+ bool SendBytes(SOCKET sock, const void* buffer, int size);
+
+ // File receiving logic
+ void HandleFileHeader(SOCKET sock, const std::vector& payload);
+ void HandleFileData(const std::vector& payload);
+ void HandleFileEnd();
+ void HandleFolderHeader(const std::vector& payload);
+ void HandleDirEntry(const std::vector& payload);
+ void HandleFileHeaderV2(const std::vector& payload);
+
+ SOCKET listenSocket_ = INVALID_SOCKET;
+ SOCKET clientSocket_ = INVALID_SOCKET; // Only support 1 client for now
+ std::thread acceptThread_;
+ std::thread clientThread_;
+ std::atomic running_ = false;
+
+ FileReceivedCallback fileReceivedCallback_;
+
+ // Current receiving state
+ std::string currentFileName_;
+ uint64_t currentFileSize_ = 0;
+ uint64_t receivedBytes_ = 0;
+ std::ofstream* currentFileStream_ = nullptr;
+ std::mutex fileMutex_;
+ std::string baseFolderRoot_;
+};
diff --git a/demo/windows_sender/main.cpp b/demo/windows_sender/main.cpp
index 4ffe753..d46c301 100644
--- a/demo/windows_sender/main.cpp
+++ b/demo/windows_sender/main.cpp
@@ -2,6 +2,7 @@
#include "ScreenCapture.h"
#include "IddBridge.h"
#include "VideoEncoder.h"
+#include "TcpServer.h"
#include
#include
#include
@@ -18,6 +19,8 @@ int main(int argc, char* argv[]) {
int outputIndex = -1;
bool iddProducer = false;
int producerOutputIndex = -1;
+ std::string fileToSend = "";
+ std::string folderToSend = "";
if (argc > 1) ipStr = argv[1];
if (argc > 2) port = std::stoi(argv[2]);
@@ -36,6 +39,10 @@ int main(int argc, char* argv[]) {
try {
producerOutputIndex = std::stoi(arg.substr(std::string("--producer-output=").size()));
} catch (...) {}
+ } else if (arg.rfind("--send-file=", 0) == 0) {
+ fileToSend = arg.substr(std::string("--send-file=").size());
+ } else if (arg.rfind("--send-folder=", 0) == 0) {
+ folderToSend = arg.substr(std::string("--send-folder=").size());
}
}
@@ -69,6 +76,39 @@ int main(int argc, char* argv[]) {
}
}
+ // Start TCP Server
+ TcpServer tcpServer;
+ if (tcpServer.Start(8889)) {
+ // Wait for client to connect if we need to send a file immediately
+ if (!fileToSend.empty()) {
+ std::cout << "Waiting for client to connect to send file: " << fileToSend << "..." << std::endl;
+ // Simple polling wait (in a real app, use events/condition variables)
+ // But TcpServer::SendFile checks if client is connected.
+ // We'll try to send in a loop or thread.
+ std::thread([&tcpServer, fileToSend]() {
+ // Wait for up to 30 seconds for a client
+ for (int i = 0; i < 300; ++i) {
+ if (tcpServer.SendFile(fileToSend)) {
+ break;
+ }
+ std::this_thread::sleep_for(std::chrono::milliseconds(100));
+ }
+ }).detach();
+ } else if (!folderToSend.empty()) {
+ std::cout << "Waiting for client to connect to send folder: " << folderToSend << "..." << std::endl;
+ std::thread([&tcpServer, folderToSend]() {
+ for (int i = 0; i < 300; ++i) {
+ if (tcpServer.SendFolder(folderToSend)) {
+ break;
+ }
+ std::this_thread::sleep_for(std::chrono::milliseconds(100));
+ }
+ }).detach();
+ }
+ } else {
+ std::cerr << "Failed to start TCP Server on port 8889" << std::endl;
+ }
+
// Debug: Open file to save H.264 stream if filename is provided
std::ofstream outFile;
if (!outputFileName.empty()) {