iOS实现投屏功能
一、投屏原理简介
一般手机要投屏到智能电视上,都要求手机和智能电视在一个二层局域网内,比如手机和智能电视同时连接到家里同一个Wi-Fi网络,亦可以电视机连接有线网络,而手机连接Wi-Fi网络,只要有线是接在同一个无线路由器上就可以了。
这里简单介绍下几大投屏协议:
AirPlay
AirPlay是Apple的无线显示标准。它允许您将视频从iPhone,iPad或Mac投屏Apple TV或者Android电视盒子上(需要电视盒子安装支持AirPlay的投屏软件,比如乐播投屏)。使用AirPlay,您可以显在iPhone上启动视频并将其“推送”到电视上,或者在iPad上玩游戏并在电视上镜像显示。
苹果公司的AirPlay标准具有足够的灵活性,可以以两种不同的方式工作。可以用镜像方式将整个设备屏幕镜像显示到电视机上(包括状态栏、菜单等)。也可以使用更智能的流模式。例如,您可以在iPhone上的应用程序中播放视频,然后使用iPhone上的播放控件来控制电视上的视频。即使在摆弄iPhone屏幕上的播放控件时,它们也不会出现在电视上,可以仅流式传输您想要在显示屏上看到的内容,而不是复制整个手机屏幕(包括状态栏、菜单等)。
AirPlay主要局限性在于-仅适用于Apple设备,不过现在很多电视盒子都支持AirPlay协议,所以用苹果手机或者平板,可以很容易投屏到智能电视上。
Miracast
Miracast是Wi-Fi联盟制定的Wi-Fi投屏行业标准,实质上是对Apple AirPlay的回应。Miracast支持内置在Android 4.2+和Windows 8.1、Windows 10。允许Android智能手机、Windows平板电脑和笔记本电脑以及其他设备以无线方式传输到兼容Miracast的接收器比如智能电视、平板电脑等。当前已经有很多电视盒子都支持Miracast协议,比如小米盒子、荣耀盒子等等,小米手机、华为的手机也都支持Miracast协议,配合小米盒子、荣耀盒子即可实现投屏。
各品牌设备该功能名称可能不同,比如:无线显示、屏幕共享、多屏互动、Screen Mirroring等。可以看这个乐播关于设备的入口收集部分:https://www.lebo.cn/news/AboutNewsContent?id=667
Miracast相比AirPlay来讲,有缺点也有优点,优点在于:
内置在Andorid和Windows中,不要求必须是苹果的终端设备。
Miracast可以在没有无线路由器的时候也能很好的工作,也就是说手机可以直接通过Wi-Fi连接到电视的Wi-Fi网卡上进行投屏(Wi-Fi Direct技术),在没有无线路由器的时候是比较方便的。
缺点在于:
只支持屏幕镜像模式投屏,而不支持流模式的投屏。当你在投屏的时候手机整个屏幕(包括状态栏等)会复制到电视机上,并且要始终保持手机屏幕是处于播放和显示状态。苹果的AirPlay则可以允许你在手机上一边浏览网页,一边通过电视播放手机中的视频。
Miracast毕竟是一种行业标准,各个厂家实现良莠不齐,不同设备之间投屏可能出现体验不佳的问题。
另一个问题是该标准不要求设备必须带有“ Miracast”品牌的商标。制造商已将其Miracast实现称为其他东西。例如,LG称其Miracast支持为“ SmartShare”,三星称其为“ AllShare Cast”,索尼称其为“屏幕镜像”,而松下称其为“显示镜像”。
DLNA
DLNA代表“数字生活网络联盟”。DLNA使用通用即插即用(UPnP)协议。DLNA并不是真正的无线显示解决方案。相反,它只是一种在一个设备上获取内容并在另一台设备上播放内容的方法。也就是说他不是真正的投屏技术。
我们手机上爱奇艺APP、腾讯视频APP,在打开视频后,右上角有一个【TV】的小图标,你点击这个小图标,就会弹出“正在搜寻可投屏设备”,将会显示同一个Wi-Fi网络下能够发现的投屏设备,选择投屏的电视机后,电视机就会播放对应的视频。这里有一个注意点,就是当你在手机上是VIP会员时,你要想将VIP视频通过DLNA投屏到智能电视上时,是没法投屏的,因为爱奇艺或者腾讯将限制这种操作,避免手机VIP用户通过投屏来实现电视机播放VIP视频,原因就是DLNA协议要求最终还是需要智能电视自己去爱奇艺的视频服务器获取视频,进而被爱奇艺视频服务器禁止。
投屏APP
最后就是很多专门投屏的投屏APP,这些APP要么是实现了上面几种协议,要么是自己实现一套私有协议。手机和智能电视都要安装这些APP,否则无法投屏。而前面几个协议都是标准协议,操作系统内置,无需安装。比较著名的投屏APP有乐播投屏、APowerMirror等,使用都很方便,一般是通过扫描智能电视显示的二维码来实现投屏到特定电视机上。这些投屏APP的另外一个好处就是:不局限在同一个局域网内,可以跨三层网络、甚至广域网。
二、投屏功能开发
在这里我们选择用来保利威的官方Demo为例,之所以用它为例,是因为他也是一家视频提供商,并且提供了视频加密服务,也就是说,他可以做到提供主流视频厂商那样的VIP视频服务,并且其允许投屏。我们可以查看官方文档,借此探究iOS投屏的开发实现。其基本都封装好了,我们可以复制过来改改就能应用到自己项目上,也可以参考实现。
1.准备工作
1.1 注册第三方投屏SDK(可选)
第三方SDK往往和电视厂家有一定的合作,会内置支持,或者提供对应的电视端APP,可以拥有更良好的投屏体验。如果要自己实现投屏的话,还需要对实现协议对接,甚至还要开发对应的接收端APP,工作量上就大了不少。由于保利威的demo投屏功能是基于乐播的,如果需要集成到自己项目上,就需要在乐播上注册绑定包名生成key。当然我们直接运行demo,里面就内置了对应的key,体验的话可以忽略这一步。
1.2 准备一台iPhone、一台安卓手机
准备一台iPhone作为发送端、安卓手机作为接受端。接收端需要安装乐播的apk,乐播apk在安卓应用市场就能找到,如果应用市场没有,也可以去乐播官网进行下载乐播投屏电视版。
1.3 下载Demo工程
本文是基于Github Demo项目讲解,所以可以直接下载他们的Github项目运行体验。下载地址:
https://github.com/polyv/polyv-ios-vod-sdk
demo默认隐藏了投屏功能,所以需要解开注释
#ifdef PLVCastFeature
#import "PLVCastBusinessManager.h" // 若需投屏功能,请解开此注释
#endif
然后,我们将两台手机(发送端和接收端),分别打开对应的APP,将其置于同一个wifi(局域网)之下,就可以开始投屏了。
2.投屏代码
投屏模块位于PolyvVodSDKDemo的Classes文件夹中。
2.1 初始化
从官方文档中可以知道初始化要设置AppSecret。这是乐播提供的服务中,把投屏sdk与包名绑定了,如果更换了包名我们就要重新注册,否则包名错误就会导致校验失败。然后会因此无法搜索到设备。
+ (void)getCastAuthorization{
if ([self authorizationInfoIsLegal] == NO) {
NSLog(@"PLVCastManager - 注册信息不足,如需投屏功能,请在'PLVCastManager.h'中填写");
return;
}
[LBLelinkKit enableLog:NO];
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSError * error = nil;
BOOL result = [LBLelinkKit authWithAppid:LBAPPID secretKey:LBSECRETKEY error:&error];
if (result) {
NSLog(@"PLVCastManager - 授权成功");
}else{
NSLog(@"PLVCastManager - 授权失败:error = %@",error);
}
});
});
}
2.2 三大核心回调
// 设备搜索发现设备回调
- (void)plvCastManager_findServices:(NSArray*)servicesArray;
// 设备搜索状态变更回调
- (void)plvCastManager_searchStateHadChanged:(BOOL)searchIsStart;
// 设备连接回调
// 若是断开状态,可根据isPassiveDisconnect来判断是否是被动断开;
// 若是连接状态,则此参数isPassiveDisconnect默认传NO,请忽视
- (void)plvCastManager_connectServicesResult:(BOOL)isConnected serviceModel:(PLVCastServiceModel *)serviceModel passiveDisconnect:(BOOL)isPassiveDisconnect;
// 设备搜索发现设备回调
- (void)plvCastManager_findServices:(NSArray *)servicesArray{
if(servicesArray.count==0)return;
NSMutableArray <PLVCastCellInfoModel *> * mArr = [[NSMutableArray alloc]init];
for(PLVCastServiceModel* plv_sinservicesArray) {
PLVCastCellInfoModel * m = [[PLVCastCellInfoModel alloc]init];
m.type = PLVCastCellType_Device;
m.deviceName= plv_s.deviceName;
m.isConnecting = plv_s.isConnecting;
[mArraddObject:m];
}
[self.castListV reloadServicesListWithModelArray:mArr];
}
// 设备搜索状态变更回调
- (void)plvCastManager_searchStateHadChanged:(BOOL)searchIsStart{
if(searchIsStart ==NO) {// 搜索已停止
self.castListV.showSearching = NO;
[self.castListV stopRefreshBtnRotate];
[self.castListVreloadList];
}else{ // 搜索已启动
self.castListV.showSearching = YES;
[self.castListV startRefreshBtnRotate];
[self.castListVreloadList];
}
}
// 设备连接状态回调
- (void)plvCastManager_connectServicesResult:(BOOL)isConnected serviceModel:(nonnull PLVCastServiceModel *)serviceModel passiveDisconnect:(BOOL)isPassiveDisconnect{
if(isConnected) {
NSIntegerquality =self.player.quality;
quality = quality ==0? (quality +1) : quality;// 若自动档则+1流畅
PLVVodVideo* video =self.player.video;
if([videoisKindOfClass: [PLVVodLocalVideoclass]]){// 需先读取到video模型缓存
__weaktypeof(self) weakSelf =self;
[PLVVodVideorequestVideoPriorityCacheWithVid:video.vidcompletion:^(PLVVodVideo*video,NSError*error) {
// 开始投屏
[weakSelf.castManagerstartPlayWithVideo:videoquality:qualitystartPosition:self.player.currentPlaybackTime];
// 设置清晰度数量 初始所选清晰度
weakSelf.castControllView.qualityOptionCount= video.hlsVideos.count;
weakSelf.castControllView.currentQualityIndex= quality;
}];
}else{ // 无需读取video模型缓存
// 开始投屏
[self.castManager startPlayWithVideo:video quality:quality startPosition:self.player.currentPlaybackTime];
// 设置清晰度数量 初始所选清晰度
self.castControllView.qualityOptionCount = self.player.video.hlsVideos.count;
self.castControllView.currentQualityIndex = quality;
}
}else{
if(isPassiveDisconnect) {// 被动断开
// 更新投屏控制界面状态
self.castControllView.status = PLVCastCVStatus_Disconnect;
}
}
}
2.3 投屏播放
- (void)startPlayWithVideo:(PLVVodVideo *)video quality:(NSInteger)quality startPosition:(NSTimeInterval)startPosition {
NSString *urlString = [video transformCastMediaURLStringWithQuality:quality];
if (urlString == nil || [urlString isKindOfClass: [NSString class]] == NO || urlString.length == 0) {
NSLog(@"PLVCastManager - 播放链接非法 链接:%@",urlString);
return;
}
// 创建播放内容对象
LBLelinkPlayerItem *item = [[LBLelinkPlayerItem alloc] init];
item.mediaType = LBLelinkMediaTypeVideoOnline;
item.mediaURLString = urlString;
item.startPosition = startPosition;
NSString *versionInfo = [NSString stringWithFormat:@"PolyviOSScreencast%@",PLVVodSdkVersion];
item.headerInfo = @{@"user-agent":versionInfo};
if (video.keepSource || video.isPlain) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.lbplayer playWithItem:item];
});
}else{
__weak typeof(self) weakSelf = self;
[PLVVodPlayerUtil requestCastKeyIvWitVideo:video quality:quality completion:^(NSString * _Nullable key, NSString * _Nullable iv, NSError * _Nullable error) {
if (error == nil) {
if ((key == nil && iv == nil) == NO) {
item.aesModel = [LBPlayerAesModel new];
item.aesModel.model = @"1";
item.aesModel.key = key;
item.aesModel.iv = iv;
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
/** 注意,为了适配接收端的bug,播放之前先stop,否则当先推送音乐再推送视频的时候会导致连接被断开 */
[weakSelf.lbplayer stop];
[weakSelf.lbplayer playWithItem:item];
});
}else{
NSLog(@"PLVCastManager - 投加密视频失败 :%@",error);
}
}];
}
}
三、投屏拓展
AirPlay是苹果独家的投屏技术,在你的app里如果不借助第三方sdk实现投屏,需要借助MPVolumeView或AVRoutePickerView。
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
/// 初始化播放器
NSURL *url = [NSURL URLWithString:@"[http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4](http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4)"];
_playerVC = [[AVPlayerViewController alloc] init];
_playerVC.player = [AVPlayer playerWithURL:url];
[self.view addSubview:_playerVC.view];
_playerVC.view.frame = CGRectMake(20, 100, 320, 220);
[_playerVC.player play];
/// 初始化AirPlay按钮
if (@available(iOS 11.0, *)) {
AVRoutePickerView *routePickerView = [[AVRoutePickerView alloc]initWithFrame:CGRectMake(100, 350, 30, 30)];
/// 活跃状态颜色
routePickerView.activeTintColor = [UIColor redColor];
/// 设置代理
routePickerView.delegate = self;
[self.view addSubview:routePickerView];
} else {
MPVolumeView *volumeView = [[MPVolumeView alloc] initWithFrame:CGRectMake(20, 350, 30, 30)];
volumeView.showsVolumeSlider = NO;
volumeView.backgroundColor = UIColor.blueColor;
[self.view addSubview:volumeView];
}
}
/// AirPlay界面弹出时回调
- (void)routePickerViewWillBeginPresentingRoutes:(AVRoutePickerView *)routePickerView API_AVAILABLE(ios(11.0)){
}
/// AirPlay界面结束时回调
- (void)routePickerViewDidEndPresentingRoutes:(AVRoutePickerView *)routePickerView API_AVAILABLE(ios(11.0)){
}