此處僅會(huì)提及遇到的具體問(wèn)題及處理方式,相關(guān)SDK及控件具體的使用方式不做介紹。
背景需求:iOS端和TV端,播放、暫停、進(jìn)度調(diào)節(jié) 互控且同步
有點(diǎn)亂,個(gè)人備忘
AirPlay
iOS下調(diào)取AirPlay Picker 的相關(guān)控件,關(guān)于這些蘋(píng)果相關(guān)的介紹極少。
MPVolumeView
樂(lè)播投屏 【樂(lè)聯(lián)、DLNA、公網(wǎng)】
樂(lè)播投屏SDK
【AirPlay問(wèn)題】
AppleTV投屏后沒(méi)有聲音。
原因:AVPlayer
muted
被設(shè)置為YES
-
投屏視頻播放完,電視會(huì)自動(dòng)斷開(kāi),此時(shí)會(huì)存在兩種令人費(fèi)解的情況
a. currentItem 釋放
b. currentItem 未被釋放
在此情形下,嘗試調(diào)用play
方法無(wú)效。原因:后續(xù)追查相關(guān)機(jī)制
解決方案:重置 AVPlayer及相關(guān)資源,注意相關(guān)觀(guān)察者移除及釋放。
這里有個(gè)有意思的地方是,AVPlayer 一開(kāi)始并未釋放,只是處理了相關(guān)資源(item及觀(guān)察者),在(播放到視頻結(jié)尾處)重復(fù)播放的時(shí)候 前兩次都沒(méi)有問(wèn)題,但是第三次開(kāi)始addPeriodicTimeObserverForInterval
的block回調(diào)就不在執(zhí)行了。
最終處理方式是連同 AVPlayer 均銷(xiāo)毀并重新生成實(shí)例,費(fèi)解。 視頻播放完成后的詭異狀態(tài)變更
在使用iOS10.0 新增的AVPlayerTimeControlStatus
做播放暫停狀態(tài)判斷時(shí),發(fā)現(xiàn)正常的播放 和 暫停 都是沒(méi)有問(wèn)題的。 但是在視頻播放到結(jié)尾處時(shí),觀(guān)察者方法中 會(huì)獲得如下?tīng)顟B(tài)變更:
--> AVPlayerTimeControlStatusPaused --> AVPlayerTimeControlStatusPlaying
這里疑惑,為什么播放結(jié)束后 還會(huì)有個(gè)播放中的狀態(tài)回調(diào)?
為了防止該狀態(tài)對(duì)相關(guān)邏輯造成干擾,過(guò)濾了如下?tīng)顟B(tài):
//注冊(cè)結(jié)束播放通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(airPlayDidPlayEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:self.airPlayPlayer.currentItem];
#pragma mark - AirPlay
- (void)airPlayDidPlayEnd:(NSNotification *)notification{
self.status = LBLelinkPlayStatusCommpleted;
// 以下為錯(cuò)誤嘗試
// [self.airPlayPlayer pause];
// if (self.strUrlCache) {
// //重置播放資源 AVplayer 播放完成后資源會(huì)被銷(xiāo)毀
// AVURLAsset *asset = [AVURLAsset assetWithURL:[NSURL URLWithString:self.strUrlCache]];
// AVPlayerItem *item = [[AVPlayerItem alloc] initWithAsset:asset];
// [self.airPlayPlayer replaceCurrentItemWithPlayerItem:item];
// }
//replaceCurrentItemWithPlayerItem: 特別注意!!! 該方法如在非主線(xiàn)程使用會(huì)引起崩潰
//邏輯變更...
....
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
...相關(guān)邏輯 略
if (object == self.airPlayPlayer && [keyPath isEqualToString:@"timeControlStatus"]) {
if (self.status == LBLelinkPlayStatusCommpleted) {
//播放完成狀態(tài) 過(guò)濾
return;
}
if (@available(iOS 10.0, *)) {
AVPlayerTimeControlStatus status = [[change objectForKey:NSKeyValueChangeNewKey]integerValue];
if (status == AVPlayerTimeControlStatusPaused) {
// do something
SYLog(@"AVPlayerTimeControlStatusWaitingToPlayAtSpecifiedRate");
self.status = LBLelinkPlayStatusPause;
}
if (status == AVPlayerTimeControlStatusPlaying) {
self.status = LBLelinkPlayStatusPlaying;
}
} else {
// Fallback on earlier versions
// ios10.0之后才能夠監(jiān)聽(tīng)到暫停后繼續(xù)播放的狀態(tài),ios10.0之前監(jiān)測(cè)不到這個(gè)狀態(tài)
//但是可以監(jiān)聽(tīng)到開(kāi)始播放的狀態(tài) AVPlayerStatus status監(jiān)聽(tīng)這個(gè)屬性。
SYLog(@"another ....");
}
...相關(guān)邏輯 略
return;
}
...略
}
- 播放進(jìn)度記錄的處理
self.airPlayTimeObserve = [self.airPlayPlayer addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(0.25, 10) queue:nil usingBlock:^(CMTime time){
@strongify(self)
if(!self){
return;
}
if(!self.isAirPlay){
return;
}
if(self.airPlayPlayer.status != AVPlayerStatusReadyToPlay){
return;
}
AVPlayerItem *currentItem = self.airPlayPlayer.currentItem;
if(currentItem.duration.timescale != 0){
NSInteger currentTime = (NSInteger)CMTimeGetSeconds([currentItem currentTime]);
NSInteger duration = (NSInteger)CMTimeGetSeconds([currentItem duration]);
//這里相當(dāng)于一個(gè)定時(shí)器,視頻播放完成后,播放進(jìn)度會(huì)一直累加。
if (currentTime > duration) {
currentTime = duration;
}
//狀態(tài)過(guò)濾 播放完成及暫停狀態(tài)下,跳過(guò)處理
if (self.status == LBLelinkPlayStatusCommpleted ||self.status == LBLelinkPlayStatusPause) {
return;
}
self.currentTime = currentTime;
...略
}
}];
【樂(lè)播相關(guān)問(wèn)題】
- 投屏設(shè)備搜索回調(diào) 過(guò)慢或無(wú)回調(diào)
//搜索設(shè)備
- (void)searchDevices:(searchBlock)searchBlock{
self.searchBlock = searchBlock;
//如果已有搜索設(shè)備則使用之前的搜索結(jié)果先行回調(diào) (WIFI 處于鏈接情況下)
if (self.arrLelinkServices.count > 0 && SparkReachabilityIsWIFI) {
self.searchBlock(self.arrLelinkServices, nil);
}
//搜索狀態(tài)標(biāo)記,用于嘗試重啟搜索
if (!self.isSearchStart) {
self.isSearchStart = YES;
}
//啟動(dòng)搜索
[self.lelinkBrowser searchForLelinkService];
}
//停止搜索
- (void)stopSearchDevices{
[self.lelinkBrowser stop];
self.isSearchStart = NO;
//重置嘗試次數(shù)
self.intRetrySearchLimit = 0;
}
// 搜索到服務(wù)時(shí),會(huì)調(diào)用此代理方法,將設(shè)備列表在此方法中回調(diào)出來(lái)
// 注意:如果不調(diào)用stop,則當(dāng)有服務(wù)信息和狀態(tài)更新以及新服務(wù)加入網(wǎng)絡(luò)或服務(wù)退出網(wǎng)絡(luò)時(shí),會(huì)調(diào)用此代理將新的設(shè)備列表回調(diào)出來(lái)
- (void)lelinkBrowser:(LBLelinkBrowser *)browser didFindLelinkServices:(NSArray<LBLelinkService *> *)services {
SYLog(@"搜索到設(shè)備數(shù) %zd", services.count);
//本地保存 設(shè)備數(shù)組
self.arrLelinkServices = services;
// 更新UI
// ...
self.searchBlock(services, nil);
//如果搜索設(shè)備為空 則手動(dòng)重啟搜索服務(wù)
if (services.count == 0 && self.isSearchStart) {
SYLog(@"%@",@"[Info]: 搜索服務(wù),進(jìn)行嘗試模式...");
//設(shè)置嘗試次數(shù) 2次
if (self.intRetrySearchLimit >= 2) {
return;
}
self.intRetrySearchLimit += 1;
//搜索 刷新
[browser searchForLelinkService];
}
}
- 設(shè)備連接 多設(shè)備投屏連接時(shí),防止回調(diào)錯(cuò)亂
- (void)connectDeviceLinkService:(LBLelinkService *)linkService connectBlock:(connectBlock)connectBlock{
self.connectBlock = connectBlock;
//服務(wù)連接 檢查服務(wù)是否可用
if (!linkService.isLelinkServiceAvailable) {
NSLog(@"service name : %@",linkService.lelinkServiceName);
NSString *strDomain = @"com.feng.car.ErrorDomain";
NSString *desc = NSLocalizedString(@"lelinkServiceAvailable state no...", @"");
NSDictionary *userInfo = @{NSLocalizedDescriptionKey: desc};
NSError *error = [NSError errorWithDomain:strDomain code:-20002 userInfo:userInfo];
self.connectBlock(nil, NO, error);
SYLog(@"%@",@"[Info]: 服務(wù)不可用。。。。");
return;
}
//新增邏輯 如果播放器 存在播放資源或播放中,則停止播放
if(self.lelinkPlayer.lelinkConnection.isConnected){
[self.lelinkPlayer stop];
[self.lelinkPlayer.lelinkConnection disConnect];
}
self.lelinkConnection.lelinkService = linkService;
[self.lelinkConnection connect];
}
- 播放狀態(tài)回調(diào) 關(guān)聯(lián)UI變更邏輯
#pragma mark - LBLelinkPlayerDelegate
// 播放錯(cuò)誤代理回調(diào),根據(jù)錯(cuò)誤信息進(jìn)行相關(guān)的處理
- (void)lelinkPlayer:(LBLelinkPlayer *)player onError:(NSError *)error {
if (error) {
SYLog(@"%@",error);
self.castBlock(LBLelinkPlayStatusError, nil, error);
}
}
// 播放狀態(tài)代理回調(diào)
- (void)lelinkPlayer:(LBLelinkPlayer *)player playStatus:(LBLelinkPlayStatus)playStatus {
//4G下 斷開(kāi)鏈接后 回調(diào)延遲... 過(guò)濾方法 【可能會(huì)觸發(fā)云投屏功能】
if (!self.linkService || !self.lelinkConnection.isConnected) {
return;
}
SYLog(@"%lu",(unsigned long)playStatus);
self.status = playStatus;
self.castBlock(playStatus, nil, nil);
...略
}
// 播放進(jìn)度信息回調(diào)
- (void)lelinkPlayer:(LBLelinkPlayer *)player progressInfo:(LBLelinkProgressInfo *)progressInfo {
//4G下 斷開(kāi)鏈接后 回調(diào)延遲... 過(guò)濾方法
if (!self.linkService || !self.lelinkConnection.isConnected) {
return;
}
self.castBlock(self.status, progressInfo, nil);
SYLog(@"current time = %ld, duration = %ld",(long)progressInfo.currentTime,(long)progressInfo.duration);
//本地記錄進(jìn)度
self.currentTime = progressInfo.currentTime;
... 略
}