增加文件传输功能

This commit is contained in:
huanglinhuan
2025-12-22 14:49:47 +08:00
parent 065251f727
commit e3db1b57a0
13 changed files with 1014 additions and 11 deletions

View File

@@ -5,6 +5,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<application
android:allowBackup="true"

View File

@@ -4,13 +4,19 @@
#define LOG_TAG "ReceiverEngine"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
ReceiverEngine::ReceiverEngine(JNIEnv* env, jobject surface) {
ReceiverEngine::ReceiverEngine(JNIEnv* env, jobject surface, jobject callbackObj) {
env->GetJavaVM(&jvm_);
callbackObj_ = env->NewGlobalRef(callbackObj);
window_ = ANativeWindow_fromSurface(env, surface);
// Bind callback
receiver_.SetCallback([this](const std::vector<uint8_t>& 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);
}

View File

@@ -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<void(const std::string&, int)> cb);
private:
void OnFrameReceived(const std::vector<uint8_t>& 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<void(const std::string&, int)> sender_cb_;
JavaVM* jvm_ = nullptr;
jobject callbackObj_ = nullptr;
};

View File

@@ -1,6 +1,7 @@
#include "UdpReceiver.h"
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <android/log.h>
#include <cstring>
@@ -56,17 +57,36 @@ void UdpReceiver::SetCallback(OnFrameReceivedCallback callback) {
callback_ = callback;
}
void UdpReceiver::SetSenderDetectedCallback(OnSenderDetectedCallback callback) {
sender_callback_ = callback;
}
void UdpReceiver::ReceiveLoop() {
std::vector<uint8_t> 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

View File

@@ -17,6 +17,7 @@ struct FrameHeader {
// Callback for full frames
using OnFrameReceivedCallback = std::function<void(const std::vector<uint8_t>& frameData, const FrameHeader& header)>;
using OnSenderDetectedCallback = std::function<void(const std::string& ip, int port)>;
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<bool> running_{false};
std::thread worker_thread_;
OnFrameReceivedCallback callback_;
OnSenderDetectedCallback sender_callback_;
std::string last_sender_ip_;
// Fragmentation handling
struct Fragment {

View File

@@ -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<jlong>(engine);
}

View File

@@ -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)

View File

@@ -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) {}
}
}

View File

@@ -19,6 +19,9 @@ set(SOURCES
NetworkSender.h
IddBridge.cpp
IddBridge.h
TcpServer.cpp
TcpServer.h
FileTransferProtocol.h
)
add_executable(WindowsSenderDemo ${SOURCES})

View File

@@ -0,0 +1,46 @@
#pragma once
#include <cstdint>
// 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)

View File

@@ -0,0 +1,418 @@
#include "TcpServer.h"
#include "FileTransferProtocol.h"
#include <iostream>
#include <fstream>
#include <filesystem>
#include <ws2tcpip.h>
#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<uint8_t> payload(header.payloadSize);
if (header.payloadSize > 0) {
if (!ReceiveBytes(clientSocket, payload.data(), header.payloadSize)) {
break;
}
}
PacketType type = static_cast<PacketType>(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<uint8_t>& payload) {
if (payload.size() < sizeof(FileMetadata)) return;
const FileMetadata* meta = reinterpret_cast<const FileMetadata*>(payload.data());
std::lock_guard<std::mutex> 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<uint8_t>& payload) {
std::lock_guard<std::mutex> 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<std::mutex> 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<char> 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<uint8_t> header;
uint16_t pathLen = (uint16_t)std::min<size_t>(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<char> 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<uint8_t>& payload) {
std::string desktopPath = getenv("USERPROFILE");
desktopPath += "\\Desktop\\";
std::string rootName;
if (!payload.empty()) {
rootName.assign(reinterpret_cast<const char*>(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<uint8_t>& payload) {
if (baseFolderRoot_.empty()) return;
std::string rel;
rel.assign(reinterpret_cast<const char*>(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<uint8_t>& payload) {
if (payload.size() < sizeof(uint64_t) + sizeof(uint16_t)) return;
const uint8_t* p = payload.data();
uint64_t sz = *reinterpret_cast<const uint64_t*>(p);
p += sizeof(uint64_t);
uint16_t pathLen = *reinterpret_cast<const uint16_t*>(p);
p += sizeof(uint16_t);
if (payload.size() < sizeof(uint64_t) + sizeof(uint16_t) + pathLen) return;
std::string rel(reinterpret_cast<const char*>(p), pathLen);
std::string full = baseFolderRoot_.empty() ? rel : (baseFolderRoot_ + "\\" + rel);
{
std::lock_guard<std::mutex> 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;
}

View File

@@ -0,0 +1,59 @@
#pragma once
#include <string>
#include <vector>
#include <thread>
#include <atomic>
#include <functional>
#include <mutex>
#include <winsock2.h>
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(const std::string& filename)>;
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<uint8_t>& payload);
void HandleFileData(const std::vector<uint8_t>& payload);
void HandleFileEnd();
void HandleFolderHeader(const std::vector<uint8_t>& payload);
void HandleDirEntry(const std::vector<uint8_t>& payload);
void HandleFileHeaderV2(const std::vector<uint8_t>& payload);
SOCKET listenSocket_ = INVALID_SOCKET;
SOCKET clientSocket_ = INVALID_SOCKET; // Only support 1 client for now
std::thread acceptThread_;
std::thread clientThread_;
std::atomic<bool> 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_;
};

View File

@@ -2,6 +2,7 @@
#include "ScreenCapture.h"
#include "IddBridge.h"
#include "VideoEncoder.h"
#include "TcpServer.h"
#include <iostream>
#include <sstream>
#include <vector>
@@ -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()) {