增加文件传输功能
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,9 @@ set(SOURCES
|
||||
NetworkSender.h
|
||||
IddBridge.cpp
|
||||
IddBridge.h
|
||||
TcpServer.cpp
|
||||
TcpServer.h
|
||||
FileTransferProtocol.h
|
||||
)
|
||||
|
||||
add_executable(WindowsSenderDemo ${SOURCES})
|
||||
|
||||
46
demo/windows_sender/FileTransferProtocol.h
Normal file
46
demo/windows_sender/FileTransferProtocol.h
Normal 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)
|
||||
418
demo/windows_sender/TcpServer.cpp
Normal file
418
demo/windows_sender/TcpServer.cpp
Normal 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;
|
||||
}
|
||||
59
demo/windows_sender/TcpServer.h
Normal file
59
demo/windows_sender/TcpServer.h
Normal 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_;
|
||||
};
|
||||
@@ -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()) {
|
||||
|
||||
Reference in New Issue
Block a user