這篇文章注重于如何實現(xiàn)AirPlay投屏功能.具體AirPlay的實現(xiàn)邏輯這里不再贅述,網(wǎng)上帖子很多
首先.想要呼出AirPlay列表的話,需要將MPVolumnView控件聲明且添加到UI.上使用之前需要引入頭文件<MediaPlayer/MediaPlayer.h>.后續(xù)如果有AirPlay設(shè)備可用并且MPVolumnView存在于UI中的話.即可呼出列表
MPVolumnView:一個系統(tǒng)內(nèi)置的控件.繼承自UIView.內(nèi)部自定義了三個控件:MPVolumeSlider(音量進度條)UILabel(顯示文字)MPButton(點擊呼出AirPlay選擇器)
①如何解決MPVolumnView添加到UI中之后高亮效果會出現(xiàn)系統(tǒng)原生icon的問題
答:設(shè)置不同State的image為ni即可.
code:
MPVolumnView *volumnView = [[MPVolumnView alloc] init];
//此處btnSender的實際類型為MPButton
UIButton *btnSender = [volumnView objectWithBlock:^BOOL(UIView *obj) {
return [obj isKindOfClass:[UIButton class]];
}];
[btnSender setImage:nil forState:UIControlStateNormal];
[btnSender setImage:nil forState:UIControlStateHighlighted];
[btnSender setImage:nil forState:UIControlStateSelected];
②如何解決在沒有可用設(shè)備情況下點擊MPVolumnView不會呼出AudioRoutePicker的問題
答:目前我找到的一個解決方案是完全無視MPVolumnView.將它在init時frame設(shè)置為0,0,0,0 然后在界面上自己添加一個UIButton.在它的點擊事件中給MPVolumnView的MPButton發(fā)送一個UIControlEventTouchUpInside來觸發(fā)MPVolumnView的_displayAudioRoutePicker事件
code:
//首先.自定義類.并且繼承MPVolumnView
@interface SparkMPVolumnView : MPVolumeView
@property(nonatomic,weak)UIButton *MPButton;
@end
@implementation SparkMPVolumnView
-(instancetype)init{
if(self = [self initWithFrame:CGRectZero]){
self.backgroundColor = [UIColor clearColor];
self.showsVolumeSlider = NO;
self.tag = kTVMPVolumeViewTag;
[self initMPButton];
}
return self;
}
//這個的目的是在AirPlay沒有任何設(shè)備時也能呼出Picker使用
- (void)initMPButton{
UIButton *btnSender = [self.subviews objectWithBlock:^BOOL(UIView *obj) {
return [obj isKindOfClass:[UIButton class]];
}];
[btnSender setImage:nil forState:UIControlStateNormal];
[btnSender setImage:nil forState:UIControlStateHighlighted];
[btnSender setImage:nil forState:UIControlStateSelected];
[btnSender setBounds:CGRectZero];
self.MPButton = btnSender;
}
//在自定義按鈕的按下事件中發(fā)送給MPButton
//此處sparkVolumView為一個SparkMPVolumnView實例
-(void)airPlayButtonAction:(UIButton *)sender{
[self.sparkVolumView.MPButton sendActionsForControlEvents:UIControlEventTouchUpInside];
}
③如何知道AirPlay的連接和斷開狀態(tài)?
答:其他資料大多是通過注冊MPVolumeViewWirelessRouteActiveDidChangeNotification來進行判斷的.但是這個通知有一個問題就是在存在多個MPVolumnView時會出現(xiàn)多次發(fā)送,并且如果在聲明MPVolumnView控件時是連接AirPlay設(shè)備的,它會先發(fā)送一個屬性wirelessRouteActive為NO的通知(即未連接任何設(shè)備),然后立馬發(fā)送一個wirelessRouteActive位YES的通知(連接到了設(shè)備).故我并未采用此方案.
我注冊了AVAudioSessionRouteChangeNotification通知.這個通知在音頻通道發(fā)生變化時會進行調(diào)用(例如插入/拔出耳機 揚聲器/聽筒切換 連接/斷開AirPlay設(shè)備).這個的通知是唯一且不重復(fù)的.在發(fā)生改變時[AVAudioSession sharedInstance]會發(fā)送此通知.在具體通知中通過獲取當(dāng)前的AirPlay通道名稱來判斷是否連接到了投屏設(shè)備
code:
-(void)registerNotification{
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(audioRouteHasChangedNotification:) name:AVAudioSessionRouteChangeNotification object:[AVAudioSession sharedInstance]];
}
- (void)audioRouteHasChangedNotification:(NSNotification *)notification{
NSString *airplayDeviceName = [self activeAirplayOutputRouteName];
BOOL isAirPlay = self.airplayDeviceName.length > 0;
}
//遍歷當(dāng)前設(shè)備所有通道.返回isEqualToString:AVAudioSessionPortAirPlay通道的具體名稱,如果名稱不為nil則為當(dāng)前連接到了AirPlay
- (NSString*)activeAirplayOutputRouteName
{
AVAudioSession* audioSession = [AVAudioSession sharedInstance];
AVAudioSessionRouteDescription* currentRoute = audioSession.currentRoute;
for (AVAudioSessionPortDescription* outputPort in currentRoute.outputs){
if ([outputPort.portType isEqualToString:AVAudioSessionPortAirPlay])
return outputPort.portName;
}
return nil;
}
④為什么播放過程中會電視端會出現(xiàn)暫停和退出播放的情況
答:AirPlay實現(xiàn)的原理是一個Socket通信.并且會在新的通道請求之后斷開之前的.所有在項目中你理論上是有多處會播放視頻的.只要調(diào)用一個新播放器的Play方法.設(shè)備端就會認(rèn)為你重新發(fā)起了一個Socket請求,斷開之前的播放并且發(fā)起新的視頻播放.故需要保證在AirPlay連接的情況下不調(diào)用任何視頻的暫停和播放代碼.
在我的項目中邏輯是比較復(fù)雜的,故實現(xiàn)方式是通過一個單例的Manager強引用一個AVPlayer,所有的AirPlay播放和暫停請求都通過它來實現(xiàn)..其他代碼只要保證在連接AirPlay的時候不進行操作就可以了.
⑤AirPlay在播放時播放新視頻為什么可能會投屏失敗?
答:這個我也無解.沒有查到相關(guān)的資料.現(xiàn)在的解決方案是在投放新視頻前檢測,如果只有有視頻,暫停播放并且將AVPlayer設(shè)置為nil.保證電視端退出投屏,隨后增加1.5f延時再進行播放.基本解決了投屏失敗的問題
其他問題歡迎大家在評論區(qū)討論,做AirPlay時趟了許多坑.希望這篇文章能幫助大家少走彎路.有不對的地方希望指出,第一時間修正