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()) {