#import "CrossDeskRTCBridge.h" #import #import #include #include #include #include #include #include #include #include #include #include #include #include "device_controller.h" #include "file_transfer_protocol.h" #include "stream_names.h" #include "minirtc.h" namespace { using crossdesk::ControlType; using crossdesk::FileTransferAck; using crossdesk::KeyFlag; using crossdesk::MouseFlag; using crossdesk::RemoteAction; using crossdesk::ServiceCommandFlag; using crossdesk::kFileAckMagic; using crossdesk::protocol::kAudioStream; using crossdesk::protocol::kClipboardStream; using crossdesk::protocol::kControlStream; using crossdesk::protocol::kDataStream; using crossdesk::protocol::kFileChunkSize; using crossdesk::protocol::kFileFeedbackStream; using crossdesk::protocol::kFileStream; using crossdesk::protocol::kKeyboardStream; using crossdesk::protocol::kMaxClipboardBytes; using crossdesk::protocol::kMouseStream; struct IncomingFile { std::string name; std::string path; uint64_t total_size = 0; uint64_t received = 0; FILE *handle = nullptr; }; struct OutgoingFile { std::string name; uint64_t total_size = 0; }; enum class PeerRole { Identity, Controller }; struct CallbackContext { __weak CrossDeskRTCBridge *owner = nil; PeerRole role = PeerRole::Identity; uint64_t generation = 0; }; struct RTCState { PeerPtr *identity_peer = nullptr; PeerPtr *controller_peer = nullptr; CallbackContext identity_context; CallbackContext controller_context; std::string signal_host; int signal_port = 0; int turn_port = 0; bool enable_srtp = false; bool hardware_acceleration = true; bool identity_ready = false; bool identity_recovery_attempted = false; std::string identity_with_password; std::string identity_base; std::string controller_login; std::string pending_remote_id; std::string pending_password; std::string transmission_id; std::string log_path; std::unordered_map incoming_files; std::unordered_map outgoing_files; }; std::atomic g_next_file_id{1}; char kRTCQueueSpecificKey; bool IsReusableIdentity(const std::string &identity) { const size_t separator = identity.find('@'); return separator != std::string::npos && separator > 0 && separator + 1 < identity.size(); } void CopyCString(char *destination, size_t capacity, const std::string &source) { if (!destination || capacity == 0) return; std::strncpy(destination, source.c_str(), capacity - 1); destination[capacity - 1] = '\0'; } std::string BaseIdentity(const std::string &identity) { const size_t separator = identity.find('@'); return separator == std::string::npos ? identity : identity.substr(0, separator); } std::string MouseJSON(float x, float y, int wheel, int flag) { RemoteAction action{}; action.type = ControlType::mouse; action.m = {std::clamp(x, 0.0f, 1.0f), std::clamp(y, 0.0f, 1.0f), wheel, static_cast(flag)}; return action.to_json(); } std::string KeyJSON(NSUInteger key_code, bool is_down) { RemoteAction action{}; action.type = ControlType::keyboard; action.k = {static_cast(key_code), 0, false, is_down ? KeyFlag::key_down : KeyFlag::key_up}; return action.to_json(); } struct KeyMapping { NSUInteger key_code = 0; bool shift = false; bool valid = false; }; KeyMapping MapASCII(unsigned char character) { if (character >= 'a' && character <= 'z') { return {static_cast(character - 'a' + 'A'), false, true}; } if (character >= 'A' && character <= 'Z') { return {static_cast(character), true, true}; } if (character >= '0' && character <= '9') { return {static_cast(character), false, true}; } switch (character) { case ' ': return {0x20, false, true}; case '\n': return {0x0D, false, true}; case '\t': return {0x09, false, true}; case '-': return {0xBD, false, true}; case '_': return {0xBD, true, true}; case '=': return {0xBB, false, true}; case '+': return {0xBB, true, true}; case '[': return {0xDB, false, true}; case '{': return {0xDB, true, true}; case ']': return {0xDD, false, true}; case '}': return {0xDD, true, true}; case '\\': return {0xDC, false, true}; case '|': return {0xDC, true, true}; case ';': return {0xBA, false, true}; case ':': return {0xBA, true, true}; case '\'': return {0xDE, false, true}; case '"': return {0xDE, true, true}; case ',': return {0xBC, false, true}; case '<': return {0xBC, true, true}; case '.': return {0xBE, false, true}; case '>': return {0xBE, true, true}; case '/': return {0xBF, false, true}; case '?': return {0xBF, true, true}; case '`': return {0xC0, false, true}; case '~': return {0xC0, true, true}; case '!': return {'1', true, true}; case '@': return {'2', true, true}; case '#': return {'3', true, true}; case '$': return {'4', true, true}; case '%': return {'5', true, true}; case '^': return {'6', true, true}; case '&': return {'7', true, true}; case '*': return {'8', true, true}; case '(': return {'9', true, true}; case ')': return {'0', true, true}; default: return {}; } } void DispatchMain(dispatch_block_t block) { if ([NSThread isMainThread]) { block(); } else { dispatch_async(dispatch_get_main_queue(), block); } } } // namespace @interface CrossDeskRTCBridge () - (void)handleSignalState:(CrossDeskSignalState)state role:(PeerRole)role; - (void)handleSignalMessage:(const char *)message size:(size_t)size role:(PeerRole)role; - (void)handleProvisionedIdentity:(const std::string &)identity; - (void)handleConnectionState:(CrossDeskConnectionState)state remoteID:(const std::string &)remoteID generation:(uint64_t)generation; - (BOOL)isControllerGenerationActive:(uint64_t)generation; - (BOOL)isControllerGenerationCurrent:(uint64_t)generation; - (void)handleVideoFrame:(const XVideoFrame *)frame sourceID:(const char *)sourceID sourceIDSize:(size_t)sourceIDSize; - (void)enqueueVideoPixelBuffer:(CVPixelBufferRef)pixelBuffer width:(NSInteger)width height:(NSInteger)height; - (void)resetVideoDelivery; - (void)handleAudio:(const char *)data size:(size_t)size; - (void)handleData:(const char *)data size:(size_t)size sourceID:(const char *)sourceID sourceIDSize:(size_t)sourceIDSize; - (void)handleStats:(const XNetTrafficStats *)stats mode:(TraversalMode)mode; - (void)sendMessage:(const std::string &)message reliable:(BOOL)reliable stream:(const char *)stream; - (void)createIdentityPeer; - (void)createControllerPeer; - (void)destroyIdentityPeer; - (void)destroyControllerPeer; - (void)requestKeyFrame; @end namespace { void OnVideoFrame(const XVideoFrame *frame, const char *, size_t, const char *source_id, size_t source_id_size, void *user_data) { auto *context = static_cast(user_data); CrossDeskRTCBridge *owner = context ? context->owner : nil; if (owner && context->role == PeerRole::Controller && frame && [owner isControllerGenerationActive:context->generation]) { [owner handleVideoFrame:frame sourceID:source_id sourceIDSize:source_id_size]; } } void OnAudioBuffer(const char *data, size_t size, const char *, size_t, const char *, size_t, void *user_data) { auto *context = static_cast(user_data); CrossDeskRTCBridge *owner = context ? context->owner : nil; if (owner && context->role == PeerRole::Controller && data && size > 0 && [owner isControllerGenerationActive:context->generation]) { [owner handleAudio:data size:size]; } } void OnDataBuffer(const char *data, size_t size, const char *, size_t, const char *source_id, size_t source_id_size, void *user_data) { auto *context = static_cast(user_data); CrossDeskRTCBridge *owner = context ? context->owner : nil; if (owner && context->role == PeerRole::Controller && [owner isControllerGenerationActive:context->generation]) { [owner handleData:data size:size sourceID:source_id sourceIDSize:source_id_size]; } } void OnSignalState(SignalStatus status, const char *, size_t, void *user_data) { auto *context = static_cast(user_data); CrossDeskRTCBridge *owner = context ? context->owner : nil; if (owner && (context->role == PeerRole::Identity || [owner isControllerGenerationActive:context->generation])) { [owner handleSignalState:static_cast(status) role:context->role]; } } void OnSignalMessage(const char *message, size_t size, void *user_data) { auto *context = static_cast(user_data); CrossDeskRTCBridge *owner = context ? context->owner : nil; if (owner && message && size > 0) { [owner handleSignalMessage:message size:size role:context->role]; } } void OnConnectionState(ConnectionStatus status, const char *remote_id, size_t remote_id_size, void *user_data) { auto *context = static_cast(user_data); CrossDeskRTCBridge *owner = context ? context->owner : nil; if (!owner || context->role != PeerRole::Controller || ![owner isControllerGenerationActive:context->generation]) { return; } const std::string identifier(remote_id ? remote_id : "", remote_id_size); [owner handleConnectionState:static_cast(status) remoteID:identifier generation:context->generation]; } void OnNetworkStats(const char *peer_id, size_t peer_id_size, TraversalMode mode, const XNetTrafficStats *stats, const char *, size_t, void *user_data) { auto *context = static_cast(user_data); CrossDeskRTCBridge *owner = context ? context->owner : nil; if (!owner) return; if (context->role == PeerRole::Identity && mode == TraversalMode::UnknownMode && peer_id && peer_id_size > 0) { [owner handleProvisionedIdentity:std::string(peer_id, peer_id_size)]; } else if (context->role == PeerRole::Controller && stats && [owner isControllerGenerationActive:context->generation]) { [owner handleStats:stats mode:mode]; } } Params MakeParams(const RTCState &state, const std::string &user_id, CallbackContext *context) { Params params{}; params.use_cfg_file = false; CopyCString(params.signal_server_ip, sizeof(params.signal_server_ip), state.signal_host); params.signal_server_port = state.signal_port; CopyCString(params.stun_server_ip, sizeof(params.stun_server_ip), state.signal_host); params.stun_server_port = state.turn_port; CopyCString(params.turn_server_ip, sizeof(params.turn_server_ip), state.signal_host); params.turn_server_port = state.turn_port; params.turn_server_username[0] = '\0'; params.turn_server_password[0] = '\0'; CopyCString(params.log_path, sizeof(params.log_path), state.log_path); params.hardware_acceleration = state.hardware_acceleration; params.native_video_output = true; params.av1_encoding = false; params.turn_mode = TurnMode::TurnAutoUdpTcp; params.enable_srtp = state.enable_srtp; params.video_content_type = VideoContentType::ScreenContent; params.video_quality = VideoQuality::QualityHigh; params.video_frame_rate = 60; params.video_degradation_preference = VideoDegradationPreference::MaintainFrameRate; params.on_receive_video_buffer = nullptr; params.on_receive_audio_buffer = OnAudioBuffer; params.on_receive_data_buffer = OnDataBuffer; params.on_receive_video_frame = OnVideoFrame; params.on_signal_status = OnSignalState; params.on_signal_message = OnSignalMessage; params.on_connection_status = OnConnectionState; params.on_net_status_report = OnNetworkStats; params.user_id = user_id.c_str(); params.user_data = context; return params; } } // namespace @implementation CrossDeskRTCBridge { std::unique_ptr _state; dispatch_queue_t _rtcQueue; CVPixelBufferPoolRef _videoPool; size_t _videoPoolWidth; size_t _videoPoolHeight; std::mutex _videoPoolMutex; std::mutex _pendingVideoMutex; CVPixelBufferRef _pendingVideoBuffer; NSInteger _pendingVideoWidth; NSInteger _pendingVideoHeight; bool _videoDeliveryScheduled; uint64_t _videoDeliveryGeneration; std::mutex _fileMutex; std::atomic _selectedDisplay; std::atomic _receivedVideoFrames; std::atomic _controllerGenerationCounter; std::atomic _activeControllerGeneration; } - (instancetype)init { self = [super init]; if (self) { _state = std::make_unique(); _state->identity_context.owner = self; _state->identity_context.role = PeerRole::Identity; _state->controller_context.owner = self; _state->controller_context.role = PeerRole::Controller; _rtcQueue = dispatch_queue_create("cn.crossdesk.mobile.minirtc", DISPATCH_QUEUE_SERIAL); dispatch_queue_set_specific(_rtcQueue, &kRTCQueueSpecificKey, _state.get(), nullptr); _videoPool = nullptr; _videoPoolWidth = 0; _videoPoolHeight = 0; _pendingVideoBuffer = nullptr; _pendingVideoWidth = 0; _pendingVideoHeight = 0; _videoDeliveryScheduled = false; _videoDeliveryGeneration = 0; _selectedDisplay.store(0); _receivedVideoFrames.store(0); _controllerGenerationCounter.store(0); _activeControllerGeneration.store(0); } return self; } - (void)dealloc { _activeControllerGeneration.store(0); _state->identity_context.owner = nil; _state->controller_context.owner = nil; if (dispatch_get_specific(&kRTCQueueSpecificKey) == _state.get()) { [self destroyControllerPeer]; [self destroyIdentityPeer]; } else { dispatch_sync(_rtcQueue, ^{ [self destroyControllerPeer]; [self destroyIdentityPeer]; }); } [self resetVideoDelivery]; std::lock_guard lock(_videoPoolMutex); if (_videoPool) { CVPixelBufferPoolRelease(_videoPool); _videoPool = nullptr; } } - (void)configureWithSignalHost:(NSString *)host signalPort:(NSInteger)signalPort turnPort:(NSInteger)turnPort enableSRTP:(BOOL)enableSRTP { NSString *trimmed = [host stringByTrimmingCharactersInSet: NSCharacterSet.whitespaceAndNewlineCharacterSet]; const char *host_c_string = trimmed.UTF8String; const std::string host_value = host_c_string ? host_c_string : ""; if (host_value.empty() || signalPort <= 0 || turnPort <= 0) return; NSString *cache = NSSearchPathForDirectoriesInDomains( NSCachesDirectory, NSUserDomainMask, YES) .firstObject; NSString *logDirectory = [cache stringByAppendingPathComponent: @"CrossDeskMobile/MiniRTC"]; [[NSFileManager defaultManager] createDirectoryAtPath:logDirectory withIntermediateDirectories:YES attributes:nil error:nil]; const char *log_c_string = logDirectory.UTF8String; const std::string log_path = log_c_string ? log_c_string : "logs"; dispatch_async(_rtcQueue, ^{ const bool unchanged = self->_state->signal_host == host_value && self->_state->signal_port == signalPort && self->_state->turn_port == turnPort && self->_state->enable_srtp == enableSRTP && self->_state->identity_peer != nullptr; if (unchanged) return; [self destroyControllerPeer]; [self destroyIdentityPeer]; self->_state->signal_host = host_value; self->_state->signal_port = static_cast(signalPort); self->_state->turn_port = static_cast(turnPort); self->_state->enable_srtp = enableSRTP; self->_state->log_path = log_path; self->_state->identity_ready = false; self->_state->identity_recovery_attempted = false; self->_state->identity_with_password.clear(); self->_state->identity_base.clear(); NSString *key = [NSString stringWithFormat:@"CrossDeskIdentity.%@.%ld", trimmed, (long)signalPort]; NSString *cached = [[NSUserDefaults standardUserDefaults] stringForKey:key]; if (cached.length > 0) { const std::string cached_identity = cached.UTF8String ?: ""; if (IsReusableIdentity(cached_identity)) { self->_state->identity_with_password = cached_identity; self->_state->identity_base = BaseIdentity(cached_identity); DispatchMain(^{ id delegate = self.delegate; if ([delegate respondsToSelector: @selector(rtcBridge:didProvisionIdentity:)]) { [delegate rtcBridge:self didProvisionIdentity:cached]; } }); } else { // A bare ID cannot authenticate after its first signaling session. // Older iOS builds stored it anyway, causing every later launch to be // rejected as "Incorrect password". Drop it and request a fresh // server-issued ID/password pair. [[NSUserDefaults standardUserDefaults] removeObjectForKey:key]; } } [self createIdentityPeer]; }); } - (void)setHardwareAccelerationEnabled:(BOOL)enabled { dispatch_async(_rtcQueue, ^{ self->_state->hardware_acceleration = enabled; }); } - (void)requestPresenceForRemoteIDs:(NSArray *)remoteIDs { NSArray *owned_ids = [remoteIDs copy]; dispatch_async(_rtcQueue, ^{ if (!self->_state->identity_peer || !self->_state->identity_ready || self->_state->identity_base.empty()) { return; } NSMutableArray *devices = [NSMutableArray arrayWithCapacity:owned_ids.count]; for (NSString *remote_id in owned_ids) { if (![remote_id isKindOfClass:NSString.class]) continue; NSString *trimmed = [remote_id stringByTrimmingCharactersInSet: NSCharacterSet.whitespaceAndNewlineCharacterSet]; if (trimmed.length > 0) [devices addObject:trimmed]; } NSString *user_id = [NSString stringWithUTF8String:self->_state->identity_base.c_str()]; NSDictionary *request = @{ @"type" : @"recent_connections_presence", @"user_id" : user_id ?: @"", @"devices" : devices, }; NSData *payload = [NSJSONSerialization dataWithJSONObject:request options:0 error:nil]; if (!payload) return; SendSignalMessage(self->_state->identity_peer, static_cast(payload.bytes), payload.length); }); } - (void)connectToRemoteID:(NSString *)remoteID password:(NSString *)password { NSString *cleanID = [remoteID stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet]; NSString *cleanPassword = [password stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet]; const char *identifier_c_string = cleanID.UTF8String; const char *password_c_string = cleanPassword.UTF8String; const std::string identifier = identifier_c_string ? identifier_c_string : ""; const std::string password_value = password_c_string ? password_c_string : ""; if (identifier.empty()) return; dispatch_async(_rtcQueue, ^{ self->_state->pending_remote_id = identifier; self->_state->pending_password = password_value; [self destroyControllerPeer]; if (self->_state->identity_ready && !self->_state->identity_base.empty()) { [self createControllerPeer]; } }); } - (void)disconnect { dispatch_async(_rtcQueue, ^{ self->_state->pending_remote_id.clear(); self->_state->pending_password.clear(); [self destroyControllerPeer]; }); } - (void)requestKeyFrame { dispatch_async(_rtcQueue, ^{ if (self->_state->controller_peer) { const std::string stream = "Display" + std::to_string(self->_selectedDisplay.load() + 1); RequestVideoKeyFrame(self->_state->controller_peer, stream.c_str()); } }); } - (void)sendPointerAtX:(float)x y:(float)y action:(CrossDeskPointerAction)action { const std::string message = MouseJSON(x, y, 0, static_cast(action)); // Movement is latency-sensitive and can be superseded by newer positions. // Button transitions must not be lost or reordered, otherwise a tap can be // applied at a stale pointer position or leave a button logically pressed. // MiniRTC reliability is configured per stream, so transitions use the // existing reliable control stream while movement stays on the mouse stream. const BOOL is_movement = action == CrossDeskPointerActionMove; #if DEBUG if (!is_movement) { NSLog(@"CrossDesk pointer action=%ld normalized=(%.5f, %.5f)", static_cast(action), x, y); } #endif [self sendMessage:message reliable:!is_movement stream:is_movement ? kMouseStream : kControlStream]; } - (void)sendScrollX:(float)x y:(float)y deltaX:(NSInteger)deltaX deltaY:(NSInteger)deltaY { const bool vertical = std::abs(deltaY) >= std::abs(deltaX); const NSInteger raw_delta = vertical ? deltaY : deltaX; if (raw_delta == 0) return; const int flag = vertical ? 7 : 8; const int wheel = raw_delta > 0 ? 1 : -1; const std::string message = MouseJSON(x, y, wheel, flag); [self sendMessage:message reliable:NO stream:kMouseStream]; } - (void)sendWindowsKeyCode:(NSUInteger)keyCode isDown:(BOOL)isDown { [self sendMessage:KeyJSON(keyCode, isDown) reliable:YES stream:kKeyboardStream]; } - (void)sendText:(NSString *)text { NSData *utf8 = [text dataUsingEncoding:NSUTF8StringEncoding]; if (!utf8.length) return; const std::string bytes(static_cast(utf8.bytes), utf8.length); dispatch_async(_rtcQueue, ^{ if (!self->_state->controller_peer) return; for (unsigned char character : bytes) { KeyMapping mapping = MapASCII(character); if (!mapping.valid) continue; if (mapping.shift) { const std::string shift_down = KeyJSON(0x10, true); SendReliableDataFrame(self->_state->controller_peer, shift_down.data(), shift_down.size(), kKeyboardStream); } const std::string key_down = KeyJSON(mapping.key_code, true); const std::string key_up = KeyJSON(mapping.key_code, false); SendReliableDataFrame(self->_state->controller_peer, key_down.data(), key_down.size(), kKeyboardStream); SendReliableDataFrame(self->_state->controller_peer, key_up.data(), key_up.size(), kKeyboardStream); if (mapping.shift) { const std::string shift_up = KeyJSON(0x10, false); SendReliableDataFrame(self->_state->controller_peer, shift_up.data(), shift_up.size(), kKeyboardStream); } } }); } - (void)switchToDisplay:(NSInteger)displayIndex { if (displayIndex < 0 || displayIndex >= 8) return; [self resetVideoDelivery]; _selectedDisplay.store(static_cast(displayIndex)); _receivedVideoFrames.store(0); RemoteAction action{}; action.type = ControlType::display_id; action.d = static_cast(displayIndex); [self sendMessage:action.to_json() reliable:YES stream:kControlStream]; [self requestKeyFrame]; } - (void)setAudioEnabled:(BOOL)enabled { RemoteAction action{}; action.type = ControlType::audio_capture; action.a = enabled; [self sendMessage:action.to_json() reliable:YES stream:kControlStream]; } - (void)sendClipboardText:(NSString *)text { NSData *utf8 = [text dataUsingEncoding:NSUTF8StringEncoding]; if (!utf8.length || utf8.length > kMaxClipboardBytes) return; const std::string bytes(static_cast(utf8.bytes), utf8.length); [self sendMessage:bytes reliable:YES stream:kClipboardStream]; } - (void)sendFileAtURL:(NSURL *)fileURL { if (!fileURL.isFileURL) return; NSURL *url = [fileURL copy]; // Acquire the document picker's sandbox extension before its completion // callback returns; the actual reads happen on the serialized RTC queue. const BOOL scoped = [url startAccessingSecurityScopedResource]; dispatch_async(_rtcQueue, ^{ if (!self->_state->controller_peer) { if (scoped) [url stopAccessingSecurityScopedResource]; return; } const char *path_bytes = url.path.fileSystemRepresentation; FILE *input = path_bytes ? std::fopen(path_bytes, "rb") : nullptr; if (!input) { if (scoped) [url stopAccessingSecurityScopedResource]; return; } if (fseeko(input, 0, SEEK_END) != 0) { std::fclose(input); if (scoped) [url stopAccessingSecurityScopedResource]; return; } const off_t end = ftello(input); rewind(input); if (end < 0) { std::fclose(input); if (scoped) [url stopAccessingSecurityScopedResource]; return; } NSString *last_component = url.lastPathComponent.length > 0 ? url.lastPathComponent : @"file"; NSData *name_data = [last_component dataUsingEncoding:NSUTF8StringEncoding]; if (name_data.length == 0 || name_data.length > std::numeric_limits::max()) { std::fclose(input); if (scoped) [url stopAccessingSecurityScopedResource]; return; } const uint32_t file_id = g_next_file_id.fetch_add(1); const uint64_t total_size = static_cast(end); { std::lock_guard lock(self->_fileMutex); self->_state->outgoing_files[file_id] = { std::string(last_component.UTF8String ?: "file"), total_size}; } DispatchMain(^{ id delegate = self.delegate; if ([delegate respondsToSelector: @selector(rtcBridge:didUpdateFileTransfer:progress:sending:)]) { [delegate rtcBridge:self didUpdateFileTransfer:last_component progress:0 sending:YES]; } }); std::vector payload(kFileChunkSize); uint64_t offset = 0; bool first = true; int send_result = 0; do { const size_t to_read = static_cast( std::min(kFileChunkSize, total_size - offset)); const size_t bytes_read = to_read > 0 ? std::fread(payload.data(), 1, to_read, input) : 0; if (to_read > 0 && bytes_read == 0) { send_result = -1; break; } const bool last = offset + bytes_read >= total_size; const std::string file_name(last_component.UTF8String ?: "file"); const std::string* file_name_pointer = first ? &file_name : nullptr; std::vector chunk = crossdesk::protocol::EncodeFileChunk( file_id, offset, total_size, payload.data(), static_cast(bytes_read), file_name_pointer, first, last); if (chunk.empty()) { send_result = -1; break; } send_result = SendReliableDataFrame(self->_state->controller_peer, chunk.data(), chunk.size(), kFileStream); offset += bytes_read; first = false; if (send_result != 0) break; } while (offset < total_size); std::fclose(input); if (scoped) [url stopAccessingSecurityScopedResource]; if (send_result != 0) { { std::lock_guard lock(self->_fileMutex); self->_state->outgoing_files.erase(file_id); } DispatchMain(^{ id delegate = self.delegate; if ([delegate respondsToSelector: @selector(rtcBridge:didUpdateFileTransfer:progress:sending:)]) { [delegate rtcBridge:self didUpdateFileTransfer:last_component progress:-1 sending:YES]; } }); } }); } - (void)sendSecureAttentionSequence { RemoteAction action{}; action.type = ControlType::service_command; action.c.flag = ServiceCommandFlag::send_sas; [self sendMessage:action.to_json() reliable:YES stream:kControlStream]; } - (void)sendMessage:(const std::string &)message reliable:(BOOL)reliable stream:(const char *)stream { // This method always hops to the RTC queue. Own both values before the // caller's stack frame disappears; several callers pass local/temporary // std::strings. const std::string owned_message(message); const std::string owned_stream(stream ? stream : ""); dispatch_async(_rtcQueue, ^{ if (!self->_state->controller_peer) return; if (reliable) { SendReliableDataFrame(self->_state->controller_peer, owned_message.data(), owned_message.size(), owned_stream.c_str()); } else { SendDataFrame(self->_state->controller_peer, owned_message.data(), owned_message.size(), owned_stream.c_str()); } }); } - (void)createIdentityPeer { if (_state->identity_peer || _state->signal_host.empty()) return; Params params = MakeParams(*_state, _state->identity_with_password, &_state->identity_context); _state->identity_peer = CreatePeer(¶ms); if (!_state->identity_peer || Init(_state->identity_peer) != 0) { [self destroyIdentityPeer]; [self handleSignalState:CrossDeskSignalStateFailed role:PeerRole::Identity]; } } - (void)createControllerPeer { if (_state->controller_peer || _state->pending_remote_id.empty() || _state->identity_base.empty()) { return; } const uint64_t generation = _controllerGenerationCounter.fetch_add(1) + 1; _state->controller_context.generation = generation; _activeControllerGeneration.store(generation); _state->controller_login = "C-" + _state->identity_base; Params params = MakeParams(*_state, _state->controller_login, &_state->controller_context); _state->controller_peer = CreatePeer(¶ms); if (!_state->controller_peer) { _activeControllerGeneration.store(0); [self handleConnectionState:CrossDeskConnectionStateFailed remoteID:_state->pending_remote_id generation:generation]; return; } // This native iOS peer is controller-only and does not publish a local // display. Remote display receivers are created from the host's SDP. AddAudioStream(_state->controller_peer, kAudioStream); AddDataStream(_state->controller_peer, kDataStream, false); AddDataStream(_state->controller_peer, kMouseStream, false); AddDataStream(_state->controller_peer, kKeyboardStream, true); AddDataStream(_state->controller_peer, kControlStream, true); AddDataStream(_state->controller_peer, kFileStream, true); AddDataStream(_state->controller_peer, kFileFeedbackStream, true); AddDataStream(_state->controller_peer, kClipboardStream, true); if (Init(_state->controller_peer) != 0) { [self destroyControllerPeer]; [self handleConnectionState:CrossDeskConnectionStateFailed remoteID:_state->pending_remote_id generation:generation]; return; } const std::string join_id = _state->pending_remote_id + "@" + _state->pending_password; JoinConnection(_state->controller_peer, join_id.c_str()); // Joining authenticates with "remote-id@password", while leaving is routed // by the remote peer ID alone (the same contract used by the desktop app). _state->transmission_id = _state->pending_remote_id; } - (void)destroyIdentityPeer { if (_state->identity_peer) { DestroyPeer(&_state->identity_peer); } _state->identity_ready = false; } - (void)destroyControllerPeer { _activeControllerGeneration.store(0); if (_state->controller_peer) { if (!_state->transmission_id.empty()) { LeaveConnection(_state->controller_peer, _state->transmission_id.c_str()); } DestroyPeer(&_state->controller_peer); } _state->controller_login.clear(); _state->transmission_id.clear(); { std::lock_guard lock(_fileMutex); for (auto &entry : _state->incoming_files) { if (entry.second.handle) std::fclose(entry.second.handle); } _state->incoming_files.clear(); _state->outgoing_files.clear(); } _selectedDisplay.store(0); _receivedVideoFrames.store(0); [self resetVideoDelivery]; } - (void)handleSignalState:(CrossDeskSignalState)state role:(PeerRole)role { dispatch_async(_rtcQueue, ^{ if (role == PeerRole::Identity) { self->_state->identity_ready = state == CrossDeskSignalStateConnected; if (state == CrossDeskSignalStateFailed && !self->_state->identity_with_password.empty() && !self->_state->identity_recovery_attempted) { // The saved credential can become invalid after a server-side reset or // an interrupted password change. Retry once with an empty identity so // the signaling server provisions a new credential. self->_state->identity_recovery_attempted = true; NSString *host = [NSString stringWithUTF8String: self->_state->signal_host.c_str()]; NSString *key = [NSString stringWithFormat:@"CrossDeskIdentity.%@.%d", host, self->_state->signal_port]; [[NSUserDefaults standardUserDefaults] removeObjectForKey:key]; [self destroyIdentityPeer]; self->_state->identity_with_password.clear(); self->_state->identity_base.clear(); [self createIdentityPeer]; } if (self->_state->identity_ready && !self->_state->identity_base.empty() && !self->_state->pending_remote_id.empty() && !self->_state->controller_peer) { [self createControllerPeer]; } } // The home-page signal indicator represents the long-lived identity peer. // A controller peer is created and destroyed for each session; forwarding // its shutdown would incorrectly make the server look disconnected. if (role == PeerRole::Identity) { DispatchMain(^{ id delegate = self.delegate; if ([delegate respondsToSelector: @selector(rtcBridge:didChangeSignalState:)]) { [delegate rtcBridge:self didChangeSignalState:state]; } }); } }); } - (void)handleSignalMessage:(const char *)message size:(size_t)size role:(PeerRole)role { constexpr size_t kMaxPresenceMessageSize = 256 * 1024; if (role != PeerRole::Identity || !message || size == 0 || size > kMaxPresenceMessageSize) { return; } NSData *payload = [NSData dataWithBytes:message length:size]; id decoded = [NSJSONSerialization JSONObjectWithData:payload options:0 error:nil]; if (![decoded isKindOfClass:NSDictionary.class]) return; NSDictionary *object = static_cast(decoded); NSString *type = object[@"type"]; if (![type isKindOfClass:NSString.class]) return; NSMutableDictionary *presence = [NSMutableDictionary dictionary]; if ([type isEqualToString:@"presence"]) { NSArray *devices = object[@"devices"]; if (![devices isKindOfClass:NSArray.class]) return; for (id value in devices) { if (![value isKindOfClass:NSDictionary.class]) continue; NSDictionary *device = static_cast(value); NSString *remote_id = device[@"id"]; NSNumber *online = device[@"online"]; if ([remote_id isKindOfClass:NSString.class] && remote_id.length > 0 && [online isKindOfClass:NSNumber.class]) { presence[remote_id] = @([online boolValue]); } } } else if ([type isEqualToString:@"presence_update"]) { NSString *remote_id = object[@"id"]; NSNumber *online = object[@"online"]; if ([remote_id isKindOfClass:NSString.class] && remote_id.length > 0 && [online isKindOfClass:NSNumber.class]) { presence[remote_id] = @([online boolValue]); } } else { return; } if (presence.count == 0) return; NSDictionary *result = [presence copy]; DispatchMain(^{ id delegate = self.delegate; if ([delegate respondsToSelector: @selector(rtcBridge:didReceivePresence:)]) { [delegate rtcBridge:self didReceivePresence:result]; } }); } - (void)handleProvisionedIdentity:(const std::string &)identity { if (identity.empty()) return; const std::string identity_copy = identity; dispatch_async(_rtcQueue, ^{ self->_state->identity_with_password = identity_copy; self->_state->identity_base = BaseIdentity(identity_copy); NSString *host = [NSString stringWithUTF8String: self->_state->signal_host.c_str()]; NSString *key = [NSString stringWithFormat:@"CrossDeskIdentity.%@.%d", host, self->_state->signal_port]; NSString *value = [NSString stringWithUTF8String:identity_copy.c_str()]; if (IsReusableIdentity(identity_copy)) { [[NSUserDefaults standardUserDefaults] setObject:value forKey:key]; } else { // Keep a passwordless identity only for this process. It is valid for // the current login but is not safe to reuse on the next app launch. [[NSUserDefaults standardUserDefaults] removeObjectForKey:key]; } DispatchMain(^{ id delegate = self.delegate; if ([delegate respondsToSelector:@selector(rtcBridge:didProvisionIdentity:)]) { [delegate rtcBridge:self didProvisionIdentity:value]; } }); }); } - (void)handleConnectionState:(CrossDeskConnectionState)state remoteID:(const std::string &)remoteID generation:(uint64_t)generation { if (![self isControllerGenerationCurrent:generation]) return; const std::string remote_copy = remoteID; if (state == CrossDeskConnectionStateConnected) { dispatch_async(_rtcQueue, ^{ if (![self isControllerGenerationActive:generation] || !self->_state->controller_peer) { return; } constexpr char client_info[] = "{\"type\":\"client_info\",\"version\":\"ios-native\"," "\"platform\":\"ios\"}"; SendSignalMessage(self->_state->controller_peer, client_info, sizeof(client_info) - 1); const std::string controller_name = self->_state->identity_base.empty() ? "iPhone" : "iPhone-" + self->_state->identity_base; RemoteAction action{}; action.type = ControlType::host_infomation; CopyCString(action.i.host_name, sizeof(action.i.host_name), controller_name); action.i.host_name_size = std::strlen(action.i.host_name); action.i.display_list = nullptr; action.i.display_num = 0; action.i.left = nullptr; action.i.top = nullptr; action.i.right = nullptr; action.i.bottom = nullptr; const std::string host_info = action.to_json(); SendReliableDataFrame(self->_state->controller_peer, host_info.data(), host_info.size(), kControlStream); const std::string stream = "Display" + std::to_string(self->_selectedDisplay.load() + 1); RequestVideoKeyFrame(self->_state->controller_peer, stream.c_str()); }); } DispatchMain(^{ if (![self isControllerGenerationCurrent:generation]) return; if (state == CrossDeskConnectionStateConnected && ![self isControllerGenerationActive:generation]) { return; } NSString *identifier = [NSString stringWithUTF8String:remote_copy.c_str()]; id delegate = self.delegate; if ([delegate respondsToSelector: @selector(rtcBridge:didChangeConnectionState:remoteID:)]) { [delegate rtcBridge:self didChangeConnectionState:state remoteID:identifier]; } }); } - (BOOL)isControllerGenerationActive:(uint64_t)generation { return generation != 0 && _activeControllerGeneration.load() == generation; } - (BOOL)isControllerGenerationCurrent:(uint64_t)generation { if (generation == 0 || _controllerGenerationCounter.load() != generation) { return NO; } const uint64_t active = _activeControllerGeneration.load(); return active == 0 || active == generation; } - (void)handleVideoFrame:(const XVideoFrame *)frame sourceID:(const char *)sourceID sourceIDSize:(size_t)sourceIDSize { if (!frame || frame->width == 0 || frame->height == 0) return; if (sourceID && sourceIDSize > 0) { const std::string source(sourceID, sourceIDSize); const std::string expected = "Display" + std::to_string(_selectedDisplay.load() + 1); if (source != expected) return; } const bool has_native_pixel_buffer = frame->native_handle && frame->native_handle_type == XVideoFrameNativeHandleCVPixelBuffer; if (has_native_pixel_buffer) { CVPixelBufferRef pixel_buffer = static_cast(frame->native_handle); [self enqueueVideoPixelBuffer:pixel_buffer width:frame->width height:frame->height]; return; } if (!frame->data) return; const size_t y_size = static_cast(frame->width) * frame->height; const size_t required_size = y_size + y_size / 2; if (frame->size < required_size) return; CVPixelBufferRef pixel_buffer = nullptr; CVReturn result = kCVReturnError; { std::lock_guard lock(_videoPoolMutex); if (!_videoPool || _videoPoolWidth != frame->width || _videoPoolHeight != frame->height) { if (_videoPool) CVPixelBufferPoolRelease(_videoPool); _videoPool = nullptr; _videoPoolWidth = frame->width; _videoPoolHeight = frame->height; NSDictionary *poolAttributes = @{ (NSString *)kCVPixelBufferPoolMinimumBufferCountKey : @3, }; NSDictionary *pixelAttributes = @{ (NSString *)kCVPixelBufferWidthKey : @(frame->width), (NSString *)kCVPixelBufferHeightKey : @(frame->height), (NSString *)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange), (NSString *)kCVPixelBufferIOSurfacePropertiesKey : @{}, (NSString *)kCVPixelBufferMetalCompatibilityKey : @YES, }; CVPixelBufferPoolCreate(kCFAllocatorDefault, (__bridge CFDictionaryRef)poolAttributes, (__bridge CFDictionaryRef)pixelAttributes, &_videoPool); } if (_videoPool) { result = CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, _videoPool, &pixel_buffer); } } if (result != kCVReturnSuccess || !pixel_buffer) return; CVPixelBufferLockBaseAddress(pixel_buffer, 0); const uint8_t *source_y = reinterpret_cast(frame->data); const uint8_t *source_uv = source_y + y_size; uint8_t *destination_y = static_cast( CVPixelBufferGetBaseAddressOfPlane(pixel_buffer, 0)); uint8_t *destination_uv = static_cast( CVPixelBufferGetBaseAddressOfPlane(pixel_buffer, 1)); const size_t y_stride = CVPixelBufferGetBytesPerRowOfPlane(pixel_buffer, 0); const size_t uv_stride = CVPixelBufferGetBytesPerRowOfPlane(pixel_buffer, 1); for (size_t row = 0; row < frame->height; ++row) { std::memcpy(destination_y + row * y_stride, source_y + row * frame->width, frame->width); } for (size_t row = 0; row < frame->height / 2; ++row) { std::memcpy(destination_uv + row * uv_stride, source_uv + row * frame->width, frame->width); } CVPixelBufferUnlockBaseAddress(pixel_buffer, 0); CVBufferSetAttachment(pixel_buffer, kCVImageBufferYCbCrMatrixKey, kCVImageBufferYCbCrMatrix_ITU_R_601_4, kCVAttachmentMode_ShouldPropagate); const NSInteger frame_width = frame->width; const NSInteger frame_height = frame->height; if (_receivedVideoFrames.load() == 0) { uint8_t minimum_luma = std::numeric_limits::max(); uint8_t maximum_luma = std::numeric_limits::min(); uint64_t luma_total = 0; size_t luma_samples = 0; const size_t row_step = std::max(1, frame->height / 96); const size_t column_step = std::max(1, frame->width / 96); for (size_t row = 0; row < frame->height; row += row_step) { for (size_t column = 0; column < frame->width; column += column_step) { const uint8_t luma = source_y[row * frame->width + column]; minimum_luma = std::min(minimum_luma, luma); maximum_luma = std::max(maximum_luma, luma); luma_total += luma; ++luma_samples; } } const double average_luma = luma_samples ? static_cast(luma_total) / static_cast(luma_samples) : 0.0; NSLog(@"CrossDesk first-frame luma min=%u max=%u average=%.1f", minimum_luma, maximum_luma, average_luma); } [self enqueueVideoPixelBuffer:pixel_buffer width:frame_width height:frame_height]; CVPixelBufferRelease(pixel_buffer); } - (void)enqueueVideoPixelBuffer:(CVPixelBufferRef)pixelBuffer width:(NSInteger)width height:(NSInteger)height { if (!pixelBuffer) return; CVPixelBufferRetain(pixelBuffer); bool should_schedule = false; uint64_t generation = 0; { std::lock_guard lock(_pendingVideoMutex); if (_pendingVideoBuffer) { CVPixelBufferRelease(_pendingVideoBuffer); } _pendingVideoBuffer = pixelBuffer; _pendingVideoWidth = width; _pendingVideoHeight = height; generation = _videoDeliveryGeneration; if (!_videoDeliveryScheduled) { _videoDeliveryScheduled = true; should_schedule = true; } } if (!should_schedule) return; dispatch_async(dispatch_get_main_queue(), ^{ CVPixelBufferRef latest_buffer = nullptr; NSInteger latest_width = 0; NSInteger latest_height = 0; { std::lock_guard lock(self->_pendingVideoMutex); if (generation != self->_videoDeliveryGeneration) return; latest_buffer = self->_pendingVideoBuffer; latest_width = self->_pendingVideoWidth; latest_height = self->_pendingVideoHeight; self->_pendingVideoBuffer = nullptr; self->_pendingVideoWidth = 0; self->_pendingVideoHeight = 0; self->_videoDeliveryScheduled = false; } if (!latest_buffer) return; const uint64_t frame_count = ++self->_receivedVideoFrames; if (frame_count == 1 || frame_count % 300 == 0) { NSLog(@"CrossDesk delivered latest frame %llu (%ld x %ld)", static_cast(frame_count), (long)latest_width, (long)latest_height); } id delegate = self.delegate; if ([delegate respondsToSelector: @selector(rtcBridge:didReceivePixelBuffer:width:height:)]) { [delegate rtcBridge:self didReceivePixelBuffer:latest_buffer width:latest_width height:latest_height]; } CVPixelBufferRelease(latest_buffer); }); } - (void)resetVideoDelivery { CVPixelBufferRef pending_buffer = nullptr; { std::lock_guard lock(_pendingVideoMutex); ++_videoDeliveryGeneration; pending_buffer = _pendingVideoBuffer; _pendingVideoBuffer = nullptr; _pendingVideoWidth = 0; _pendingVideoHeight = 0; _videoDeliveryScheduled = false; } if (pending_buffer) CVPixelBufferRelease(pending_buffer); } - (void)handleAudio:(const char *)data size:(size_t)size { if (!data || size < sizeof(int16_t) || size % sizeof(int16_t) != 0) return; NSData *pcm = [NSData dataWithBytes:data length:size]; DispatchMain(^{ id delegate = self.delegate; if ([delegate respondsToSelector: @selector(rtcBridge:didReceiveAudioPCM:)]) { [delegate rtcBridge:self didReceiveAudioPCM:pcm]; } }); } - (void)handleData:(const char *)data size:(size_t)size sourceID:(const char *)sourceID sourceIDSize:(size_t)sourceIDSize { if (!data || size == 0 || !sourceID) return; const std::string source(sourceID, sourceIDSize); if (source == kControlStream) { RemoteAction action{}; if (!action.from_json(std::string(data, size)) || action.type != ControlType::host_infomation) { return; } NSString *host_name = [[NSString alloc] initWithBytes:action.i.host_name length:action.i.host_name_size encoding:NSUTF8StringEncoding]; NSMutableArray *display_names = [NSMutableArray arrayWithCapacity:action.i.display_num]; NSMutableArray *display_sizes = [NSMutableArray arrayWithCapacity:action.i.display_num]; for (std::size_t index = 0; index < action.i.display_num; ++index) { NSString *name = action.i.display_list && action.i.display_list[index] ? [NSString stringWithUTF8String:action.i.display_list[index]] : nil; [display_names addObject:name ?: @""]; const int width = action.i.left && action.i.right ? std::max(action.i.right[index] - action.i.left[index], 0) : 0; const int height = action.i.top && action.i.bottom ? std::max(action.i.bottom[index] - action.i.top[index], 0) : 0; [display_sizes addObject:[NSValue valueWithCGSize:CGSizeMake(width, height)]]; } crossdesk::FreeRemoteAction(action); DispatchMain(^{ id delegate = self.delegate; if ([delegate respondsToSelector: @selector(rtcBridge:didReceiveHostName:displayNames: displaySizes:)]) { [delegate rtcBridge:self didReceiveHostName:host_name ?: @"" displayNames:display_names displaySizes:display_sizes]; } }); return; } if (source == kMouseStream) { RemoteAction action{}; if (!action.from_json(std::string(data, size)) || action.type != ControlType::cursor_state) { return; } const BOOL visible = action.cs.visible; const NSInteger shape = static_cast(action.cs.shape); const BOOL position_update = action.cs.position_update; const BOOL position_valid = action.cs.position_valid; const float position_x = action.cs.x; const float position_y = action.cs.y; const float visual_offset_x = action.cs.visual_offset_x; const float visual_offset_y = action.cs.visual_offset_y; const NSInteger display_index = action.cs.display_id; const uint32_t sequence = action.cs.seq; DispatchMain(^{ id delegate = self.delegate; if ([delegate respondsToSelector: @selector(rtcBridge:didReceiveCursorVisible:shape: positionUpdate:positionValid:x:y: visualOffsetX:visualOffsetY:displayIndex: sequence:)]) { [delegate rtcBridge:self didReceiveCursorVisible:visible shape:shape positionUpdate:position_update positionValid:position_valid x:position_x y:position_y visualOffsetX:visual_offset_x visualOffsetY:visual_offset_y displayIndex:display_index sequence:sequence]; } }); return; } if (source == kClipboardStream) { if (size > kMaxClipboardBytes || std::memchr(data, '\0', size)) return; NSString *text = [[NSString alloc] initWithBytes:data length:size encoding:NSUTF8StringEncoding]; if (!text) return; DispatchMain(^{ id delegate = self.delegate; if ([delegate respondsToSelector: @selector(rtcBridge:didReceiveClipboardText:)]) { [delegate rtcBridge:self didReceiveClipboardText:text]; } }); return; } if (source == kFileFeedbackStream) { FileTransferAck ack{}; if (!crossdesk::protocol::DecodeFileTransferAck(data, size, &ack)) return; NSString *name = nil; double progress = 0; { std::lock_guard lock(_fileMutex); auto it = _state->outgoing_files.find(ack.file_id); if (it == _state->outgoing_files.end()) return; name = [NSString stringWithUTF8String:it->second.name.c_str()]; progress = ack.total_size == 0 ? 1 : std::clamp(static_cast(ack.acked_offset) / static_cast(ack.total_size), 0.0, 1.0); if ((ack.flags & 0x03) != 0) { if ((ack.flags & 0x02) != 0) progress = -1; _state->outgoing_files.erase(it); } } DispatchMain(^{ id delegate = self.delegate; if ([delegate respondsToSelector: @selector(rtcBridge:didUpdateFileTransfer:progress:sending:)]) { [delegate rtcBridge:self didUpdateFileTransfer:name ?: @"file" progress:progress sending:YES]; } }); return; } if (source != kFileStream) return; crossdesk::protocol::FileChunkView chunk; if (!crossdesk::protocol::DecodeFileChunk(data, size, &chunk)) return; const crossdesk::FileChunkHeader& header = chunk.header; NSURL *completed_url = nil; NSString *progress_name = nil; double progress = 0; FileTransferAck ack{}; ack.magic = kFileAckMagic; ack.file_id = header.file_id; ack.acked_offset = header.offset; ack.total_size = header.total_size; { std::lock_guard lock(_fileMutex); auto it = _state->incoming_files.find(header.file_id); if (it == _state->incoming_files.end()) { if ((header.flags & 0x01) == 0) return; NSString *raw_name = !chunk.file_name.empty() ? [[NSString alloc] initWithBytes:chunk.file_name.data() length:chunk.file_name.size() encoding:NSUTF8StringEncoding] : nil; NSString *safe_name = raw_name.lastPathComponent; safe_name = [safe_name stringByReplacingOccurrencesOfString:@"\\" withString:@"_"]; safe_name = [safe_name stringByReplacingOccurrencesOfString:@":" withString:@"_"]; if (safe_name.length == 0 || [safe_name isEqualToString:@"."] || [safe_name isEqualToString:@".."]) { safe_name = [NSString stringWithFormat:@"received_%u", header.file_id]; } NSURL *documents = [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].firstObject; NSURL *directory = [documents URLByAppendingPathComponent:@"Received" isDirectory:YES]; [[NSFileManager defaultManager] createDirectoryAtURL:directory withIntermediateDirectories:YES attributes:nil error:nil]; NSURL *target = [directory URLByAppendingPathComponent:safe_name]; if ([[NSFileManager defaultManager] fileExistsAtPath:target.path]) { NSString *stem = safe_name.stringByDeletingPathExtension; NSString *extension = safe_name.pathExtension; NSString *unique = extension.length > 0 ? [NSString stringWithFormat:@"%@_%u.%@", stem, header.file_id, extension] : [NSString stringWithFormat:@"%@_%u", stem, header.file_id]; target = [directory URLByAppendingPathComponent:unique]; safe_name = unique; } FILE *output = std::fopen(target.path.fileSystemRepresentation, "wb+"); if (!output) return; IncomingFile incoming; incoming.name = safe_name.UTF8String ?: "file"; incoming.path = target.path.fileSystemRepresentation ?: ""; incoming.total_size = header.total_size; incoming.handle = output; _state->incoming_files.emplace(header.file_id, std::move(incoming)); it = _state->incoming_files.find(header.file_id); } IncomingFile &incoming = it->second; bool write_ok = incoming.total_size == header.total_size && fseeko(incoming.handle, static_cast(header.offset), SEEK_SET) == 0; if (write_ok && chunk.payload_size > 0) { write_ok = std::fwrite(chunk.payload, 1, chunk.payload_size, incoming.handle) == chunk.payload_size; } if (!write_ok) { ack.flags |= 0x02; std::fclose(incoming.handle); _state->incoming_files.erase(it); } else { ack.acked_offset = header.offset + chunk.payload_size; incoming.received = std::max(incoming.received, ack.acked_offset); progress_name = [NSString stringWithUTF8String:incoming.name.c_str()]; progress = incoming.total_size == 0 ? 1 : std::clamp(static_cast(incoming.received) / static_cast(incoming.total_size), 0.0, 1.0); const bool completed = (header.flags & 0x02) != 0 || incoming.received >= incoming.total_size; if (completed) { ack.flags |= 0x01; std::fflush(incoming.handle); std::fclose(incoming.handle); completed_url = [NSURL fileURLWithPath: [NSString stringWithUTF8String:incoming.path.c_str()]]; _state->incoming_files.erase(it); } } } dispatch_async(_rtcQueue, ^{ if (self->_state->controller_peer) { const auto encoded_ack = crossdesk::protocol::EncodeFileTransferAck(ack); SendReliableDataFrame(self->_state->controller_peer, encoded_ack.data(), encoded_ack.size(), kFileFeedbackStream); } }); if (progress_name) { DispatchMain(^{ id delegate = self.delegate; if ([delegate respondsToSelector: @selector(rtcBridge:didUpdateFileTransfer:progress:sending:)]) { [delegate rtcBridge:self didUpdateFileTransfer:progress_name progress:progress sending:NO]; } if (completed_url && [delegate respondsToSelector: @selector(rtcBridge:didReceiveFileAtURL:)]) { [delegate rtcBridge:self didReceiveFileAtURL:completed_url]; } }); } } - (void)handleStats:(const XNetTrafficStats *)stats mode:(TraversalMode)mode { if (!stats) return; const NSUInteger bitrate = stats->total_inbound_stats.bitrate; const float loss = stats->video_inbound_stats.loss_rate; const BOOL using_turn = mode == TraversalMode::Relay; DispatchMain(^{ id delegate = self.delegate; if ([delegate respondsToSelector: @selector(rtcBridge:didUpdateBitrate:lossRate:usingTURN:)]) { [delegate rtcBridge:self didUpdateBitrate:bitrate lossRate:loss usingTURN:using_turn]; } }); } @end