iOS實現(xiàn)投屏功能

一薄声、投屏原理簡介

一般手機要投屏到智能電視上,都要求手機和智能電視在一個二層局域網(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)投屏棋嘲,需要借助MPVolumeViewAVRoutePickerView酒唉。


- (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)){

}

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市沸移,隨后出現(xiàn)的幾起案子痪伦,更是在濱河造成了極大的恐慌,老刑警劉巖雹锣,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件网沾,死亡現(xiàn)場離奇詭異,居然都是意外死亡蕊爵,警方通過查閱死者的電腦和手機辉哥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來攒射,“玉大人证薇,你說我怎么就攤上這事度苔。” “怎么了浑度?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵寇窑,是天一觀的道長。 經(jīng)常有香客問我箩张,道長甩骏,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任先慷,我火速辦了婚禮饮笛,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘论熙。我一直安慰自己福青,他們只是感情好,可當我...
    茶點故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布脓诡。 她就那樣靜靜地躺著无午,像睡著了一般。 火紅的嫁衣襯著肌膚如雪祝谚。 梳的紋絲不亂的頭發(fā)上宪迟,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天,我揣著相機與錄音交惯,去河邊找鬼次泽。 笑死,一個胖子當著我的面吹牛席爽,可吹牛的內(nèi)容都是我干的意荤。 我是一名探鬼主播,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼只锻,長吁一口氣:“原來是場噩夢啊……” “哼袭异!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起炬藤,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤御铃,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后沈矿,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體上真,經(jīng)...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年羹膳,在試婚紗的時候發(fā)現(xiàn)自己被綠了睡互。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖就珠,靈堂內(nèi)的尸體忽然破棺而出寇壳,到底是詐尸還是另有隱情,我是刑警寧澤妻怎,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布壳炎,位于F島的核電站,受9級特大地震影響逼侦,放射性物質(zhì)發(fā)生泄漏匿辩。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一榛丢、第九天 我趴在偏房一處隱蔽的房頂上張望铲球。 院中可真熱鬧,春花似錦晰赞、人聲如沸稼病。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽然走。三九已至,卻和暖如春锨用,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背隘谣。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工增拥, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人寻歧。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓掌栅,卻偏偏與公主長得像,于是被迫代替她去往敵國和親码泛。 傳聞我的和親對象是個殘疾皇子猾封,可洞房花燭夜當晚...
    茶點故事閱讀 45,055評論 2 355

推薦閱讀更多精彩內(nèi)容