diff --git a/docs/AIRPLAY_IMPLEMENTATION.md b/docs/AIRPLAY_IMPLEMENTATION.md deleted file mode 100644 index 6beb6ab0..00000000 --- a/docs/AIRPLAY_IMPLEMENTATION.md +++ /dev/null @@ -1,261 +0,0 @@ -# AirPlay Implementation Guide - -## Overview - -This document outlines the implementation approach for AirPlay support in the unified casting system. AirPlay detection and control requires native iOS development as the current React Native library (`@douglowder/expo-av-route-picker-view`) only provides a UI picker, not state detection. - -## Current State - -### What's Working -- ✅ Unified casting architecture supports both Chromecast and AirPlay -- ✅ `useCasting` hook has AirPlay protocol type -- ✅ UI components are protocol-agnostic -- ✅ AirPlay UI picker available via `ExpoAvRoutePickerView` - -### What's Missing -- ❌ AirPlay connection state detection -- ❌ AirPlay playback control (play/pause/seek) -- ❌ AirPlay progress monitoring -- ❌ AirPlay volume control - -## Implementation Approaches - -### Option 1: Native Module for AVAudioSession (Recommended) - -**Pros:** -- Most reliable for detection -- Works for both audio and video -- Provides route change notifications - -**Cons:** -- Requires Objective-C/Swift development -- Additional native code to maintain - -**Implementation:** - -1. Create native module: `modules/expo-airplay-detector` - -```objective-c -// ios/ExpoAirPlayDetector.h -#import - -@interface ExpoAirPlayDetector : EXExportedModule -@end - -// ios/ExpoAirPlayDetector.m -#import "ExpoAirPlayDetector.h" -#import - -@implementation ExpoAirPlayDetector - -EX_EXPORT_MODULE(ExpoAirPlayDetector) - -- (NSArray *)supportedEvents { - return @[@"onRouteChange"]; -} - -- (void)startObserving { - [[NSNotificationCenter defaultCenter] - addObserver:self - selector:@selector(handleRouteChange:) - name:AVAudioSessionRouteChangeNotification - object:nil]; -} - -- (void)stopObserving { - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - -- (void)handleRouteChange:(NSNotification *)notification { - AVAudioSessionRouteDescription *currentRoute = - [[AVAudioSession sharedInstance] currentRoute]; - - BOOL isAirPlayActive = NO; - NSString *deviceName = @""; - - for (AVAudioSessionPortDescription *output in currentRoute.outputs) { - if ([output.portType isEqualToString:AVAudioSessionPortAirPlay]) { - isAirPlayActive = YES; - deviceName = output.portName; - break; - } - } - - [self sendEventWithName:@"onRouteChange" body:@{ - @"isAirPlayActive": @(isAirPlayActive), - @"deviceName": deviceName - }]; -} - -EX_EXPORT_METHOD_AS(isAirPlayActive, - isAirPlayActive:(EXPromiseResolveBlock)resolve - reject:(EXPromiseRejectBlock)reject) { - AVAudioSessionRouteDescription *currentRoute = - [[AVAudioSession sharedInstance] currentRoute]; - - for (AVAudioSessionPortDescription *output in currentRoute.outputs) { - if ([output.portType isEqualToString:AVAudioSessionPortAirPlay]) { - resolve(@YES); - return; - } - } - resolve(@NO); -} - -@end -``` - -2. Create TypeScript wrapper: - -```typescript -// modules/expo-airplay-detector/index.ts -import { EventEmitter, NativeModulesProxy } from 'expo-modules-core'; - -const emitter = new EventEmitter(NativeModulesProxy.ExpoAirPlayDetector); - -export function isAirPlayActive(): Promise { - return NativeModulesProxy.ExpoAirPlayDetector.isAirPlayActive(); -} - -export function addRouteChangeListener( - listener: (event: { isAirPlayActive: boolean; deviceName: string }) => void -) { - return emitter.addListener('onRouteChange', listener); -} -``` - -3. Integrate into `useCasting`: - -```typescript -import { addRouteChangeListener, isAirPlayActive } from '@/modules/expo-airplay-detector'; - -// In useCasting hook -const [airplayConnected, setAirplayConnected] = useState(false); - -useEffect(() => { - if (Platform.OS !== 'ios') return; - - // Initial check - isAirPlayActive().then(setAirplayConnected); - - // Listen for changes - const subscription = addRouteChangeListener((event) => { - setAirplayConnected(event.isAirPlayActive); - if (event.isAirPlayActive) { - setState(prev => ({ - ...prev, - currentDevice: { - id: 'airplay', - name: event.deviceName, - protocol: 'airplay', - }, - })); - } - }); - - return () => subscription.remove(); -}, []); -``` - -### Option 2: AVPlayer Integration - -**Pros:** -- Already using AVPlayer for video playback -- Access to `isExternalPlaybackActive` property -- Can control playback via existing player - -**Cons:** -- Requires modifying video player implementation -- Only works for video, not audio -- Tightly coupled to player lifecycle - -**Implementation:** - -1. Expose AVPlayer state in video player component -2. Pass state up to casting system via context or props -3. Monitor `AVPlayer.isExternalPlaybackActive` - -```typescript -// In video player component -useEffect(() => { - const checkAirPlay = () => { - // Requires native module to access AVPlayer.isExternalPlaybackActive - // or use react-native-video's onExternalPlaybackChange callback - }; - - const interval = setInterval(checkAirPlay, 1000); - return () => clearInterval(interval); -}, []); -``` - -### Option 3: MPVolumeView-Based Detection - -**Pros:** -- Uses existing iOS APIs -- No additional dependencies - -**Cons:** -- Unreliable (volume view can be hidden) -- Poor UX (requires accessing MPVolumeView) -- Deprecated approach - -**Not Recommended** - -## Recommended Implementation Steps - -1. **Phase 1: Native Module** (1-2 days) - - Create `expo-airplay-detector` module - - Implement route change detection - - Add TypeScript bindings - - Test on physical iOS device - -2. **Phase 2: Integration** (1 day) - - Wire detector into `useCasting` hook - - Update state management - - Test protocol switching - -3. **Phase 3: Controls** (2-3 days) - - For video: Use AVPlayer controls via existing player - - For audio: Implement AVAudioSession controls - - Add seek, volume, play/pause methods - -4. **Phase 4: Progress Sync** (1 day) - - Monitor playback progress - - Report to Jellyfin API - - Update UI state - -## Testing Requirements - -- Test with physical AirPlay devices (Apple TV, HomePod, AirPlay speakers) -- Test with AirPlay 2 multi-room -- Test handoff between Chromecast and AirPlay -- Test background playback -- Test route changes (headphones → AirPlay → speaker) - -## Alternative: Third-Party Libraries - -Consider these libraries if native development is not feasible: -- `react-native-track-player` - Has AirPlay support built-in -- `react-native-video` - Provides `onExternalPlaybackChange` callback -- Custom fork of `@douglowder/expo-av-route-picker-view` with state detection - -## Timeline Estimate - -- **Native Module Approach**: 4-5 days -- **AVPlayer Integration**: 2-3 days -- **Third-Party Library**: 1-2 days (integration + testing) - -## Next Steps - -1. Choose implementation approach based on team's iOS development capacity -2. Set up development environment with physical AirPlay device -3. Create proof-of-concept for route detection -4. Integrate into existing casting system -5. Comprehensive testing across devices - -## Resources - -- [AVAudioSession Documentation](https://developer.apple.com/documentation/avfoundation/avaudiosession) -- [AVPlayer External Playback](https://developer.apple.com/documentation/avfoundation/avplayer/1388982-isexternalplaybackactive) -- [Expo Modules API](https://docs.expo.dev/modules/overview/) -- [AirPlay 2 Technical Documentation](https://developer.apple.com/documentation/avfoundation/airplay_2)