投屏问题备忘
此处仅会提及遇到的具体问题及处理方式,相关SDK及控件具体的使用方式不做介绍。
背景需求:iOS端和TV端,播放、暂停、进度调节 互控且同步
有点乱,个人备忘
AirPlay
iOS下调取AirPlay Picker 的相关控件,关于这些苹果相关的介绍极少。
MPVolumeView
乐播投屏 【乐联、DLNA、公网】
乐播投屏SDK
【AirPlay问题】
-
AppleTV投屏后没有声音。
原因:AVPlayer
muted
被设置为YES
-
投屏视频播放完,电视会自动断开,此时会存在两种令人费解的情况
a. currentItem 释放
b. currentItem 未被释放
在此情形下,尝试调用play
方法无效。原因:后续追查相关机制
解决方案:重置 AVPlayer及相关资源,注意相关观察者移除及释放。
这里有个有意思的地方是,AVPlayer 一开始并未释放,只是处理了相关资源(item及观察者),在(播放到视频结尾处)重复播放的时候 前两次都没有问题,但是第三次开始addPeriodicTimeObserverForInterval
的block回调就不在执行了。
最终处理方式是连同 AVPlayer 均销毁并重新生成实例,费解。 -
视频播放完成后的诡异状态变更
在使用iOS10.0 新增的AVPlayerTimeControlStatus
做播放暂停状态判断时,发现正常的播放 和 暂停 都是没有问题的。 但是在视频播放到结尾处时,观察者方法中 会获得如下状态变更:
--> AVPlayerTimeControlStatusPaused --> AVPlayerTimeControlStatusPlaying
这里疑惑,为什么播放结束后 还会有个播放中的状态回调?
为了防止该状态对相关逻辑造成干扰,过滤了如下状态:
//注册结束播放通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(airPlayDidPlayEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:self.airPlayPlayer.currentItem];
#pragma mark - AirPlay
- (void)airPlayDidPlayEnd:(NSNotification *)notification{
self.status = LBLelinkPlayStatusCommpleted;
// 以下为错误尝试
// [self.airPlayPlayer pause];
// if (self.strUrlCache) {
// //重置播放资源 AVplayer 播放完成后资源会被销毁
// AVURLAsset *asset = [AVURLAsset assetWithURL:[NSURL URLWithString:self.strUrlCache]];
// AVPlayerItem *item = [[AVPlayerItem alloc] initWithAsset:asset];
// [self.airPlayPlayer replaceCurrentItemWithPlayerItem:item];
// }
//replaceCurrentItemWithPlayerItem: 特别注意!!! 该方法如在非主线程使用会引起崩溃
//逻辑变更...
....
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
...相关逻辑 略
if (object == self.airPlayPlayer && [keyPath isEqualToString:@"timeControlStatus"]) {
if (self.status == LBLelinkPlayStatusCommpleted) {
//播放完成状态 过滤
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之后才能够监听到暂停后继续播放的状态,ios10.0之前监测不到这个状态
//但是可以监听到开始播放的状态 AVPlayerStatus status监听这个属性。
SYLog(@"another ....");
}
...相关逻辑 略
return;
}
...略
}
- 播放进度记录的处理
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]);
//这里相当于一个定时器,视频播放完成后,播放进度会一直累加。
if (currentTime > duration) {
currentTime = duration;
}
//状态过滤 播放完成及暂停状态下,跳过处理
if (self.status == LBLelinkPlayStatusCommpleted ||self.status == LBLelinkPlayStatusPause) {
return;
}
self.currentTime = currentTime;
...略
}
}];
【乐播相关问题】
- 投屏设备搜索回调 过慢或无回调
//搜索设备
- (void)searchDevices:(searchBlock)searchBlock{
self.searchBlock = searchBlock;
//如果已有搜索设备则使用之前的搜索结果先行回调 (WIFI 处于链接情况下)
if (self.arrLelinkServices.count > 0 && SparkReachabilityIsWIFI) {
self.searchBlock(self.arrLelinkServices, nil);
}
//搜索状态标记,用于尝试重启搜索
if (!self.isSearchStart) {
self.isSearchStart = YES;
}
//启动搜索
[self.lelinkBrowser searchForLelinkService];
}
//停止搜索
- (void)stopSearchDevices{
[self.lelinkBrowser stop];
self.isSearchStart = NO;
//重置尝试次数
self.intRetrySearchLimit = 0;
}
// 搜索到服务时,会调用此代理方法,将设备列表在此方法中回调出来
// 注意:如果不调用stop,则当有服务信息和状态更新以及新服务加入网络或服务退出网络时,会调用此代理将新的设备列表回调出来
- (void)lelinkBrowser:(LBLelinkBrowser *)browser didFindLelinkServices:(NSArray<LBLelinkService *> *)services {
SYLog(@"搜索到设备数 %zd", services.count);
//本地保存 设备数组
self.arrLelinkServices = services;
// 更新UI
// ...
self.searchBlock(services, nil);
//如果搜索设备为空 则手动重启搜索服务
if (services.count == 0 && self.isSearchStart) {
SYLog(@"%@",@"[Info]: 搜索服务,进行尝试模式...");
//设置尝试次数 2次
if (self.intRetrySearchLimit >= 2) {
return;
}
self.intRetrySearchLimit += 1;
//搜索 刷新
[browser searchForLelinkService];
}
}
- 设备连接 多设备投屏连接时,防止回调错乱
- (void)connectDeviceLinkService:(LBLelinkService *)linkService connectBlock:(connectBlock)connectBlock{
self.connectBlock = connectBlock;
//服务连接 检查服务是否可用
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]: 服务不可用。。。。");
return;
}
//新增逻辑 如果播放器 存在播放资源或播放中,则停止播放
if(self.lelinkPlayer.lelinkConnection.isConnected){
[self.lelinkPlayer stop];
[self.lelinkPlayer.lelinkConnection disConnect];
}
self.lelinkConnection.lelinkService = linkService;
[self.lelinkConnection connect];
}
- 播放状态回调 关联UI变更逻辑
#pragma mark - LBLelinkPlayerDelegate
// 播放错误代理回调,根据错误信息进行相关的处理
- (void)lelinkPlayer:(LBLelinkPlayer *)player onError:(NSError *)error {
if (error) {
SYLog(@"%@",error);
self.castBlock(LBLelinkPlayStatusError, nil, error);
}
}
// 播放状态代理回调
- (void)lelinkPlayer:(LBLelinkPlayer *)player playStatus:(LBLelinkPlayStatus)playStatus {
//4G下 断开链接后 回调延迟... 过滤方法 【可能会触发云投屏功能】
if (!self.linkService || !self.lelinkConnection.isConnected) {
return;
}
SYLog(@"%lu",(unsigned long)playStatus);
self.status = playStatus;
self.castBlock(playStatus, nil, nil);
...略
}
// 播放进度信息回调
- (void)lelinkPlayer:(LBLelinkPlayer *)player progressInfo:(LBLelinkProgressInfo *)progressInfo {
//4G下 断开链接后 回调延迟... 过滤方法
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);
//本地记录进度
self.currentTime = progressInfo.currentTime;
... 略
}