mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-23 03:28:11 +00:00
- Expose RemoteMediaClient from useCasting for advanced operations - Implement episode fetching from Jellyfin API for TV shows - Add next episode detection with countdown UI showing episode name - Wire audio/subtitle track changes to RemoteMediaClient.setActiveTrackIds - Wire playback speed to RemoteMediaClient.setPlaybackRate - Add tap-to-seek functionality to progress bar - Update segment skip buttons to use remoteMediaClient seek wrapper - Create comprehensive AirPlay implementation documentation All casting system features are now complete before PR submission.
7.2 KiB
7.2 KiB
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
- ✅
useCastinghook 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:
- Create native module:
modules/expo-airplay-detector
// ios/ExpoAirPlayDetector.h
#import <ExpoModulesCore/ExpoModulesCore.h>
@interface ExpoAirPlayDetector : EXExportedModule <EXEventEmitter>
@end
// ios/ExpoAirPlayDetector.m
#import "ExpoAirPlayDetector.h"
#import <AVFoundation/AVFoundation.h>
@implementation ExpoAirPlayDetector
EX_EXPORT_MODULE(ExpoAirPlayDetector)
- (NSArray<NSString *> *)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
- Create TypeScript wrapper:
// modules/expo-airplay-detector/index.ts
import { EventEmitter, NativeModulesProxy } from 'expo-modules-core';
const emitter = new EventEmitter(NativeModulesProxy.ExpoAirPlayDetector);
export function isAirPlayActive(): Promise<boolean> {
return NativeModulesProxy.ExpoAirPlayDetector.isAirPlayActive();
}
export function addRouteChangeListener(
listener: (event: { isAirPlayActive: boolean; deviceName: string }) => void
) {
return emitter.addListener('onRouteChange', listener);
}
- Integrate into
useCasting:
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
isExternalPlaybackActiveproperty - Can control playback via existing player
Cons:
- Requires modifying video player implementation
- Only works for video, not audio
- Tightly coupled to player lifecycle
Implementation:
- Expose AVPlayer state in video player component
- Pass state up to casting system via context or props
- Monitor
AVPlayer.isExternalPlaybackActive
// 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
-
Phase 1: Native Module (1-2 days)
- Create
expo-airplay-detectormodule - Implement route change detection
- Add TypeScript bindings
- Test on physical iOS device
- Create
-
Phase 2: Integration (1 day)
- Wire detector into
useCastinghook - Update state management
- Test protocol switching
- Wire detector into
-
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
-
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-inreact-native-video- ProvidesonExternalPlaybackChangecallback- Custom fork of
@douglowder/expo-av-route-picker-viewwith 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
- Choose implementation approach based on team's iOS development capacity
- Set up development environment with physical AirPlay device
- Create proof-of-concept for route detection
- Integrate into existing casting system
- Comprehensive testing across devices