一薄声、投屏原理簡介
一般手機要投屏到智能電視上,都要求手機和智能電視在一個二層局域網(wǎng)內(nèi)题画,比如手機和智能電視同時連接到家里同一個Wi-Fi網(wǎng)絡默辨,亦可以電視機連接有線網(wǎng)絡,而手機連接Wi-Fi網(wǎng)絡苍息,只要有線是接在同一個無線路由器上就可以了缩幸。
這里簡單介紹下幾大投屏協(xié)議:
AirPlay
AirPlay是Apple的無線顯示標準。它允許您將視頻從iPhone竞思,iPad或Mac投屏Apple TV或者Android電視盒子上(需要電視盒子安裝支持AirPlay的投屏軟件表谊,比如樂播投屏)。使用AirPlay盖喷,您可以顯在iPhone上啟動視頻并將其“推送”到電視上爆办,或者在iPad上玩游戲并在電視上鏡像顯示。
蘋果公司的AirPlay標準具有足夠的靈活性课梳,可以以兩種不同的方式工作距辆。可以用鏡像方式將整個設備屏幕鏡像顯示到電視機上(包括狀態(tài)欄惦界、菜單等)挑格。也可以使用更智能的流模式。例如沾歪,您可以在iPhone上的應用程序中播放視頻漂彤,然后使用iPhone上的播放控件來控制電視上的視頻。即使在擺弄iPhone屏幕上的播放控件時灾搏,它們也不會出現(xiàn)在電視上挫望,可以僅流式傳輸您想要在顯示屏上看到的內(nèi)容,而不是復制整個手機屏幕(包括狀態(tài)欄狂窑、菜單等)媳板。
AirPlay主要局限性在于-僅適用于Apple設備,不過現(xiàn)在很多電視盒子都支持AirPlay協(xié)議泉哈,所以用蘋果手機或者平板蛉幸,可以很容易投屏到智能電視上破讨。
Miracast
Miracast是Wi-Fi聯(lián)盟制定的Wi-Fi投屏行業(yè)標準,實質(zhì)上是對Apple AirPlay的回應奕纫。Miracast支持內(nèi)置在Android 4.2+和Windows 8.1提陶、Windows 10。允許Android智能手機匹层、Windows平板電腦和筆記本電腦以及其他設備以無線方式傳輸?shù)郊嫒軲iracast的接收器比如智能電視隙笆、平板電腦等。當前已經(jīng)有很多電視盒子都支持Miracast協(xié)議升筏,比如小米盒子撑柔、榮耀盒子等等,小米手機您访、華為的手機也都支持Miracast協(xié)議铅忿,配合小米盒子、榮耀盒子即可實現(xiàn)投屏洋只。
各品牌設備該功能名稱可能不同辆沦,比如:無線顯示、屏幕共享识虚、多屏互動肢扯、Screen Mirroring等〉4福可以看這個樂播關于設備的入口收集部分:https://www.lebo.cn/news/AboutNewsContent?id=667
Miracast相比AirPlay來講蔚晨,有缺點也有優(yōu)點,優(yōu)點在于:
內(nèi)置在Andorid和Windows中肛循,不要求必須是蘋果的終端設備铭腕。
Miracast可以在沒有無線路由器的時候也能很好的工作,也就是說手機可以直接通過Wi-Fi連接到電視的Wi-Fi網(wǎng)卡上進行投屏(Wi-Fi Direct技術)多糠,在沒有無線路由器的時候是比較方便的累舷。
缺點在于:
只支持屏幕鏡像模式投屏,而不支持流模式的投屏夹孔。當你在投屏的時候手機整個屏幕(包括狀態(tài)欄等)會復制到電視機上被盈,并且要始終保持手機屏幕是處于播放和顯示狀態(tài)。蘋果的AirPlay則可以允許你在手機上一邊瀏覽網(wǎng)頁搭伤,一邊通過電視播放手機中的視頻只怎。
Miracast畢竟是一種行業(yè)標準,各個廠家實現(xiàn)良莠不齊怜俐,不同設備之間投屏可能出現(xiàn)體驗不佳的問題身堡。
另一個問題是該標準不要求設備必須帶有“ Miracast”品牌的商標。制造商已將其Miracast實現(xiàn)稱為其他東西拍鲤。例如贴谎,LG稱其Miracast支持為“ SmartShare”汞扎,三星稱其為“ AllShare Cast”,索尼稱其為“屏幕鏡像”擅这,而松下稱其為“顯示鏡像”佩捞。
DLNA
DLNA代表“數(shù)字生活網(wǎng)絡聯(lián)盟”。DLNA使用通用即插即用(UPnP)協(xié)議蕾哟。DLNA并不是真正的無線顯示解決方案。相反莲蜘,它只是一種在一個設備上獲取內(nèi)容并在另一臺設備上播放內(nèi)容的方法谭确。也就是說他不是真正的投屏技術。
我們手機上愛奇藝APP票渠、騰訊視頻APP逐哈,在打開視頻后,右上角有一個【TV】的小圖標问顷,你點擊這個小圖標昂秃,就會彈出“正在搜尋可投屏設備”,將會顯示同一個Wi-Fi網(wǎng)絡下能夠發(fā)現(xiàn)的投屏設備杜窄,選擇投屏的電視機后肠骆,電視機就會播放對應的視頻。這里有一個注意點塞耕,就是當你在手機上是VIP會員時蚀腿,你要想將VIP視頻通過DLNA投屏到智能電視上時,是沒法投屏的扫外,因為愛奇藝或者騰訊將限制這種操作莉钙,避免手機VIP用戶通過投屏來實現(xiàn)電視機播放VIP視頻,原因就是DLNA協(xié)議要求最終還是需要智能電視自己去愛奇藝的視頻服務器獲取視頻筛谚,進而被愛奇藝視頻服務器禁止磁玉。
投屏APP
最后就是很多專門投屏的投屏APP,這些APP要么是實現(xiàn)了上面幾種協(xié)議驾讲,要么是自己實現(xiàn)一套私有協(xié)議蚊伞。手機和智能電視都要安裝這些APP,否則無法投屏蝎毡。而前面幾個協(xié)議都是標準協(xié)議厚柳,操作系統(tǒng)內(nèi)置,無需安裝沐兵。比較著名的投屏APP有樂播投屏别垮、APowerMirror等,使用都很方便扎谎,一般是通過掃描智能電視顯示的二維碼來實現(xiàn)投屏到特定電視機上碳想。這些投屏APP的另外一個好處就是:不局限在同一個局域網(wǎng)內(nèi)烧董,可以跨三層網(wǎng)絡、甚至廣域網(wǎng)胧奔。
二逊移、投屏功能開發(fā)
在這里我們選擇用來保利威的官方Demo為例,之所以用它為例龙填,是因為他也是一家視頻提供商胳泉,并且提供了視頻加密服務,也就是說岩遗,他可以做到提供主流視頻廠商那樣的VIP視頻服務扇商,并且其允許投屏。我們可以查看官方文檔宿礁,借此探究iOS投屏的開發(fā)實現(xiàn)案铺。其基本都封裝好了,我們可以復制過來改改就能應用到自己項目上梆靖,也可以參考實現(xiàn)控汉。
1.準備工作
1.1 注冊第三方投屏SDK(可選)
第三方SDK往往和電視廠家有一定的合作,會內(nèi)置支持返吻,或者提供對應的電視端APP姑子,可以擁有更良好的投屏體驗。如果要自己實現(xiàn)投屏的話测僵,還需要對實現(xiàn)協(xié)議對接壁酬,甚至還要開發(fā)對應的接收端APP,工作量上就大了不少恨课。由于保利威的demo投屏功能是基于樂播的舆乔,如果需要集成到自己項目上,就需要在樂播上注冊綁定包名生成key剂公。當然我們直接運行demo希俩,里面就內(nèi)置了對應的key,體驗的話可以忽略這一步纲辽。
1.2 準備一臺iPhone颜武、一臺安卓手機
準備一臺iPhone作為發(fā)送端、安卓手機作為接受端拖吼。接收端需要安裝樂播的apk鳞上,樂播apk在安卓應用市場就能找到,如果應用市場沒有吊档,也可以去樂播官網(wǎng)進行下載樂播投屏電視版获列。
1.3 下載Demo工程
本文是基于Github Demo項目講解硕糊,所以可以直接下載他們的Github項目運行體驗浆竭。下載地址:
https://github.com/polyv/polyv-ios-vod-sdk
demo默認隱藏了投屏功能,所以需要解開注釋
#ifdef PLVCastFeature
#import "PLVCastBusinessManager.h" // 若需投屏功能移怯,請解開此注釋
#endif
然后,我們將兩臺手機(發(fā)送端和接收端)这难,分別打開對應的APP舟误,將其置于同一個wifi(局域網(wǎng))之下,就可以開始投屏了姻乓。
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 三大核心回調(diào)
// 設備搜索發(fā)現(xiàn)設備回調(diào)
- (void)plvCastManager_findServices:(NSArray*)servicesArray;
// 設備搜索狀態(tài)變更回調(diào)
- (void)plvCastManager_searchStateHadChanged:(BOOL)searchIsStart;
// 設備連接回調(diào)
// 若是斷開狀態(tài)阀坏,可根據(jù)isPassiveDisconnect來判斷是否是被動斷開如暖;
// 若是連接狀態(tài),則此參數(shù)isPassiveDisconnect默認傳NO忌堂,請忽視
- (void)plvCastManager_connectServicesResult:(BOOL)isConnected serviceModel:(PLVCastServiceModel *)serviceModel passiveDisconnect:(BOOL)isPassiveDisconnect;
// 設備搜索發(fā)現(xiàn)設備回調(diào)
- (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];
}
// 設備搜索狀態(tài)變更回調(diào)
- (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];
}
}
// 設備連接狀態(tài)回調(diào)
- (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];
// 設置清晰度數(shù)量 初始所選清晰度
weakSelf.castControllView.qualityOptionCount= video.hlsVideos.count;
weakSelf.castControllView.currentQualityIndex= quality;
}];
}else{ // 無需讀取video模型緩存
// 開始投屏
[self.castManager startPlayWithVideo:video quality:quality startPosition:self.player.currentPlaybackTime];
// 設置清晰度數(shù)量 初始所選清晰度
self.castControllView.qualityOptionCount = self.player.video.hlsVideos.count;
self.castControllView.currentQualityIndex = quality;
}
}else{
if(isPassiveDisconnect) {// 被動斷開
// 更新投屏控制界面狀態(tài)
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;
}
// 創(chuàng)建播放內(nèi)容對象
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實現(xiàn)投屏棋嘲,需要借助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)];
/// 活躍狀態(tài)顏色
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界面彈出時回調(diào)
- (void)routePickerViewWillBeginPresentingRoutes:(AVRoutePickerView *)routePickerView API_AVAILABLE(ios(11.0)){
}
/// AirPlay界面結(jié)束時回調(diào)
- (void)routePickerViewDidEndPresentingRoutes:(AVRoutePickerView *)routePickerView API_AVAILABLE(ios(11.0)){
}