Skip to content

ADR-00X: VLC Watch Connectivity Architecture Proposal

ADR-00X: VLC Watch Connectivity Architecture Proposal

Status

Aspect Details
Decision Status Pending
Version 1.2
Initial Draft August 26, 2025
Last Updated September 1, 2025
Implementation Status POC Complete, Deep Integration Pending
Context Watch Connectivity Bridge Implementation
PoC/draft MR #!1531

Problem Statement

VLC's watchOS app requires reliable bidirectional communication with the companion iPhone app to enable features like remote playback control, media library browsing, and file synchronization. The challenge is establishing a robust connectivity bridge that handles session management, message routing, and UI updates across both platforms while maintaining clean separation of concerns and providing a foundation for future media-specific features. Basic connectivity implementations are prone to race conditions, especially for special cases requiring real-time communication and state idempotency such as playback synchronization and simultaneous control inputs from other devices.

Decision

We implement a notification-based connectivity bridge architecture using Apple's WatchConnectivity framework with comprehensive technical infrastructure for both current POC capabilities and future production features.

Architecture Overview

The communication system follows a delegate-observer pattern with three core components:

graph TB
    subgraph "iOS App"
        iOS_UI[iOS Sandbox UI]
        iOS_Service[VLCWatchConnectivityService]
        iOS_Delegator[VLCWatchConnectivityDelegator]
        iOS_NotificationCenter[NotificationCenter]
    end
    
    subgraph "watchOS App"  
        WatchOS_UI[SwiftUI Views]
        WatchOS_Service[VLCWatchConnectivityService]
        WatchOS_Delegator[VLCWatchConnectivityDelegator]
        WatchOS_NotificationCenter[NotificationCenter]
        WatchOS_StatusModel[ConnectivityStatusModel]
    end
    
    subgraph "Watch Connectivity Framework"
        WCSession[WCSession.default]
    end
    
    iOS_UI --> iOS_Service
    iOS_Service --> WCSession
    WCSession --> iOS_Delegator
    iOS_Delegator --> iOS_NotificationCenter
    iOS_NotificationCenter --> iOS_UI
    
    WatchOS_UI --> WatchOS_Service
    WatchOS_Service --> WCSession
    WCSession --> WatchOS_Delegator
    WatchOS_Delegator --> WatchOS_NotificationCenter
    WatchOS_NotificationCenter --> WatchOS_UI
    WatchOS_NotificationCenter --> WatchOS_StatusModel

Current Implementation Components

iOS Components

  • VLCWatchConnectivityService: Session lifecycle management and message routing

    • Singleton pattern (shared) for app-wide access
    • Session activation in UIApplicationDelegate.didFinishLaunchingWithOptions
    • Message sending with configurable reply/error handlers
    • Notification observers for reachability state changes
  • VLCWatchConnectivitySandboxViewController: Sandbox for PoC development & testing purposes

watchOS Components

  • VLCWatchConnectivityService: Optimized session management for watch constraints

    • Early activation in app delegate init() for background launch optimization (able to handle background tasks even though watch app is not opened)
    • Platform-aware message sending with reachability checks
    • SwiftUI environment injection support
  • ConnectivityStatusModel: Reactive binding state for PoC Development & testing purposes

    • @Published properties with automatic UI updates
    • Combine publishers for notification-driven state changes

Core Message Handling Infrastructure

  • VLCWatchConnectivityDelegator: Centralized WCSessionDelegate implementation
    • Complete delegate coverage: Handles all 9 official WCSessionDelegate methods
    • Platform-specific processing: Device signature injection using #if os(iOS)/#elseif os(watchOS) to validate this payload truly being echo-ed (ping-pong)
    • Thread-safe notification dispatch: Main queue notification posting for critical & non-critical updates
    • Error context preservation: Failed transfers include error details in notification payload

Data Flow Sequence Diagrams

1. App Context Update Flow (Current Implementation)

sequenceDiagram
    participant iOS_UI as iOS Sandbox
    participant iOS_Service as VLCWatchConnectivityService
    participant WCSession
    participant Watch_Delegator as VLCWatchConnectivityDelegator
    participant Watch_NC as watchOS NotificationCenter
    participant Watch_UI as watchOS SwiftUI
    
    iOS_UI->>iOS_Service: sendApplicationContext(context)
    iOS_Service->>WCSession: updateApplicationContext(context)
    iOS_Service->>iOS_UI: Success/Error feedback
    
    WCSession-->>Watch_Delegator: didReceiveApplicationContext
    Watch_Delegator->>Watch_Delegator: Add receivedAt timestamp
    Watch_Delegator->>Watch_NC: Post notification (.dataDidFlow)
    Watch_NC->>Watch_UI: Update ConnectivityStatusModel

2. Message with Reply Flow (Current Implementation)

sequenceDiagram
    participant iOS_UI as iOS Sandbox
    participant iOS_Service as VLCWatchConnectivityService
    participant WCSession
    participant Watch_Delegator as VLCWatchConnectivityDelegator
    participant Watch_UI as watchOS UI
    
    iOS_UI->>iOS_Service: sendMessage(message, replyHandler, errorHandler)
    iOS_Service->>iOS_Service: Check WCSession.isReachable
    iOS_Service->>WCSession: sendMessage(message, replyHandler, errorHandler)
    
    WCSession-->>Watch_Delegator: didReceiveMessage(message, replyHandler)
    Watch_Delegator->>Watch_Delegator: Add platform signature & timestamp
    Watch_Delegator->>Watch_UI: Post notification (.dataDidFlow)
    Watch_Delegator->>WCSession: replyHandler(enrichedMessage)
    
    WCSession-->>iOS_Service: Reply received in original replyHandler
    iOS_Service->>iOS_UI: Success callback with reply data

Communication Interface Analysis

Interface Comparison Table

Interface Latency Reliability Size Limit Background Support Queuing Best For
Interactive Messages Sub-second Requires reachability <64KB No No Real-time controls
User Info Transfer Minutes to hours High (queued + retry) <1MB Yes FIFO queue Metadata sync
Application Context When apps active Medium (overwrite) <1KB Yes Latest only Current state
File Transfer Variable Medium (no resume) <50MB practical Yes No queue Media files

Detailed Interface Analysis

Interactive Messages (sendMessage) - Real-time Communication

Current Implementation:

func sendMessage(_ message: [String: Any], 
                replyHandler: (([String: Any]) -> Void)? = nil, 
                errorHandler: ((Error) -> Void)? = nil) {
    guard WCSession.isSupported() && WCSession.default.isReachable else { return }
    WCSession.default.sendMessage(message, replyHandler: replyHandler, errorHandler: errorHandler)
}
Pros Cons
Instant bidirectional communication Requires both apps reachable
Reply handlers enable request-response No queuing for failed messages
Low overhead for small payloads Can fail mid-conversation
Immediate error feedback Race conditions on rapid sends

VLC Use Cases:

// Real-time playback controls
{
  "type": "playback_control",
  "action": "play|pause|next|previous|seek",
  "position": 142.5,
  "timestamp": 1234567890
}

// Volume adjustment with immediate feedback  
{
  "type": "volume_control",
  "volume": 0.8,
  "timestamp": 1234567890
}

// Live seeking during playback
{
  "type": "seek_control", 
  "position": 95.2,
  "duration": 180.0,
  "timestamp": 1234567890
}

User Info Transfer (transferUserInfo) - Reliable Background Sync

Pros Cons
Works when devices not simultaneously active Unpredictable delivery timing
Automatic retry on failures No delivery order guarantees
Persistent until delivered Duplicate handling required
Handles larger payloads efficiently Storage pressure on failures

VLC Use Cases:

// Complete playlist sync with metadata
{
  "type": "playlist_sync",
  "playlistId": 456,
  "items": [
    {
      "id": 789,
      "title": "Song Title",
      "artist": "Artist Name", 
      "duration": 210.5,
      "artworkHash": "sha256:abc123..."
    }
  ],
  "lastModified": 1234567890
}

// Media library metadata chunks
{
  "type": "library_sync",
  "chunk": 1,
  "totalChunks": 10,
  "items": [/* media items */],
  "timestamp": 1234567890
}

Application Context (updateApplicationContext) - Current State Sync

Pros Cons
Always represents current state Only latest state preserved
Automatic delivery when available No delivery confirmation
Lightweight for small state objects Limited payload size
Atomic updates (all or nothing) No historical state tracking

VLC Use Cases:

// Current now playing information
{
  "type": "now_playing",
  "currentMedia": {
    "id": 12345,
    "title": "Current Song",
    "artist": "Current Artist",
    "album": "Current Album", 
    "position": 142.5,
    "duration": 180.0,
    "isPlaying": true,
    "volume": 0.7
  },
  "lastUpdated": 1234567890
}

File Transfer (transferFile) - Large Content Sync

Current Implementation Status: Foundation only - reception handling implemented

Pros Cons
Handles large media files No chunking for >50MB files
Background transfer support No resume on failure
Progress monitoring available Synchronous file handling required
System manages transfer queue No automatic storage cleanup

VLC Use Cases:

// Audio file transfer for offline playback
{
  "type": "media_transfer",
  "mediaId": 789,
  "filename": "song.mp3",
  "fileSize": 8457600,
  "checksum": "sha256:def456...",
  "priority": "high"
}

Technical Deep Dive

VLCWatchConnectivityDelegator Implementation Details

Core Responsibilities:

  • Implements all 9 WCSessionDelegate methods
  • Converts raw Watch Connectivity data into enriched notification payloads
  • Manages thread-safe notification posting
  • Handles platform-specific operations with conditional compilation

Key Method Analysis:

// Current implementation - VLCWatchConnectivityDelegator.swift:37-42
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) {
    var customObject: [String: Any] = applicationContext
    customObject["receivedAt"] = Date().description
    postNotificationOnMainQueueAsync(name: .dataDidFlow, object: customObject)
}

// Current implementation - Reply handler with platform signatures
func session(_ session: WCSession, didReceiveMessage message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) {
    self.session(session, didReceiveMessage: message)
    
    var repliedMessage = message
    #if os(iOS)
    repliedMessage["replyFrom"] = "iOS"
    repliedMessage["signature"] = UIDevice.current.identifierForVendor?.uuidString ?? "unknown"	
    #elseif os(watchOS)
    repliedMessage["replyFrom"] = "watchOS"
    repliedMessage["signature"] = WKInterfaceDevice.current().identifierForVendor?.uuidString ?? "unknown"
    #endif
    replyHandler(repliedMessage)
}

Session Lifecycle Management

iOS Integration Pattern:

@objcMembers
final class VLCWatchConnectivityService: NSObject, UIApplicationDelegate {
    static let shared = VLCWatchConnectivityService()
    
    func application(_ application: UIApplication, 
                    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]?) -> Bool {
        if WCSession.isSupported() {
            WCSession.default.delegate = sessionDelegate
            WCSession.default.activate()
        }
        bindNotificationCenter()
        return true
    }
}

watchOS Integration Pattern:

final class VLCWatchConnectivityService: NSObject, WKApplicationDelegate {
    override init() {
        super.init()
        
        if WCSession.isSupported() {
            // Early activation for background launch optimization
            WCSession.default.delegate = sessionDelegate
            WCSession.default.activate()
        }
    }
}

Notification System Architecture

Current Notification Extensions:

// Connectivity+Notification.swift
extension Notification.Name {
    static let dataDidFlow = Notification.Name("DataDidFlow")
    static let activationDidComplete = Notification.Name("ActivationDidComplete")
    static let reachabilityDidChange = Notification.Name("ReachabilityDidChange")
}

Thread-Safe Notification Dispatch:

// VLCWatchConnectivityDelegator.swift:185-189
private func postNotificationOnMainQueueAsync(name: NSNotification.Name, object: [String: Any]? = nil) {
    DispatchQueue.main.async {
        NotificationCenter.default.post(name: name, object: object)
    }
}

Connection Establishment & Session Handshake

Session Establishment Sequence

sequenceDiagram
    participant iOS as iOS App
    participant iOS_WC as iOS WCSession
    participant System as System/Hardware
    participant Watch_WC as watchOS WCSession
    participant Watch as watchOS App
    
    Note over iOS, Watch: 1. Early Initialization
    iOS->>iOS_WC: WCSession.default.delegate = delegator
    Watch->>Watch_WC: WCSession.default.delegate = delegator (in init())
    iOS->>iOS_WC: WCSession.default.activate() (in didFinishLaunching)
    Watch->>Watch_WC: WCSession.default.activate() (in init())
    
    Note over iOS, Watch: 2. System-Level Pairing Discovery
    iOS_WC->>System: Check for paired Apple Watch
    Watch_WC->>System: Check for paired iPhone
    System-->>iOS_WC: Paired device found
    System-->>Watch_WC: Paired device found
    
    Note over iOS, Watch: 3. Session Activation
    iOS_WC-->>iOS: activationDidCompleteWith(.activated)
    Watch_WC-->>Watch: activationDidCompleteWith(.activated)
    
    Note over iOS, Watch: 4. Reachability Handshake
    iOS_WC->>Watch_WC: Establish communication channel
    Watch_WC-->>iOS_WC: Channel established
    iOS_WC-->>iOS: sessionReachabilityDidChange (isReachable = true)
    Watch_WC-->>Watch: sessionReachabilityDidChange (isReachable = true)

Multi-State Session Management

stateDiagram-v2
    [*] --> NotActivated
    NotActivated --> Activating: activate() called
    
    state iOS_Specific {
        Activating --> Inactive: Watch switching (iOS only)
        Inactive --> Activating: New watch paired
    }
    
    Activating --> Activated: Pairing successful
    Activated --> Activating: Connection lost/watch change
    
    state Activated {
        [*] --> NotReachable
        NotReachable --> Reachable: Bluetooth/WiFi connection
        Reachable --> NotReachable: Connection lost
    }

Incremental Development Areas

Message Protocol Formalization

  • Current State: Generic [String: Any] payloads without type validation
  • Required: These message contract protocol might extend to accommodate each feature implementation. but PO should cover (e.g., media playback state, volume level)

File Transfer Critical Issues

  • Synchronous File Handling Requirement: didReceive file: delegate must complete synchronously before system removes file URL
  • No Chunking Implementation: Files >50MB likely to fail or timeout (chunk when transferring, combine the binary as background task)
  • Missing Resume Logic: Failed transfers must restart completely (retry mechanism?)
  • Storage Management: No cleanup of transferred files on limited watchOS storage

State Consistency Challenges

  • No Conflict Resolution: Concurrent state updates from both platforms create inconsistencies (CRDT last-write wins might be the best fit for most of VLC use-cases)
  • Missing Authority Model: No designated "source of truth" for different data types (Payload object contracts e.g., media playback state, volume level)
  • Race Condition Potential: Rapid message exchanges can arrive out-of-order (CRDT LWW for state idempotency such as now playing feature)

Background Processing Limitations

  • No Transfer Queuing: Large operations block main thread during processing (chunking the binary while writing & combining those chunks in background)
  • Missing Retry Logic: Failed operations require manual retry, no exponential backoff
  • Power Optimization Absent: Continuous connectivity polling drains watchOS battery (TO BE DEEP DIVE LATER)
  • Memory Pressure: File transfers not optimized for watchOS memory constraints (chunking)

Implementation Analysis

Benefits

  • Comprehensive Foundation: Complete WCSessionDelegate coverage provides robust base for all future features
  • Platform-Agnostic Messaging: Unified interface abstracts platform-specific WCSession differences
  • Thread-Safe Architecture: Main queue notification dispatch prevents UI threading issues
  • Reactive UI Integration for SwiftUI: Combine publishers enable automatic UI updates
  • Development Infrastructure: Sandbox testing enables rapid iteration and debugging
  • Extensible Design: Notification-based system allows easy addition of new observers

Current Status

  • POC Status: Implementation provides foundation but lacks production-ready error handling (easy to address by wrapping payload into contracts)
  • Generic Message Types: No domain-specific VLC message protocols yet implemented (e.g., for media control)
  • Basic File Transfer: Critical file handling gaps prevent reliable media file sync (chunking not implemented)
  • Artwork Image Downsampling: Basic image downsampling for now playing music artwork.

Architectural Risks

  • Notification System Overuse: Single .dataDidFlow notification for all data types creates potential coupling
  • Session State Complexity: Multi-state management across platforms increases complexity
  • Error Context Loss: Generic error handling may lose specific failure context in production scenarios

Version: 1.2 Initial Draft: August 26, 2025 Last Updated: September 1, 2025
Implementation Status: POC Complete, Deep Integration Pending

Edited by Fahri Novald
To upload designs, you'll need to enable LFS and have an admin enable hashed storage. More information