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
- Singleton pattern (
-
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
- Early activation in app delegate
-
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
: CentralizedWCSessionDelegate
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
sendMessage
) - Real-time Communication
Interactive Messages (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)
}
|
|
---|---|
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
}
transferUserInfo
) - Reliable Background Sync
User Info Transfer (
|
|
---|---|
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
}
updateApplicationContext
) - Current State Sync
Application Context (
|
|
---|---|
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
}
transferFile
) - Large Content Sync
File Transfer (Current Implementation Status: Foundation only - reception handling implemented
|
|
---|---|
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