关于直播投屏解决方案

笔者所在公司项目都比较奇葩,产品要求app带投屏功能(即手机屏幕投射到电脑屏幕或者智能电视)。只好硬着头皮,查找各方资料解决问题。首先想到的就是咱们大苹果的Airplay功能了,当然很多做投屏的iOS端是基于AirPlay Protocol开发的,不过人家mac端的Airserver的确好用啊(不了解的小伙伴可以自行度娘),不过最后由于我们pc客户端集成Airserver遇到问题,这个方案就阉割了(此为方案一)。最后采用了录屏框架+直播推流方式实现(此为方案二)。


一、两种方案优缺点比较

方案一:airplay成熟的协议 投屏连接速度快 效果好 流畅 缺点是需要研究AirPlay Protocol PC端不好开发集成

方案二:录屏框架不完善(其实录屏的流也是来自开启虚拟airplay连接 把实时的屏幕流导出来) 连接速度慢 推流拉流有延迟 效果一般 流畅度一般 优点是可以直接推流到rtmp服务器 进行网络直播

  • 效果对比
方案二
方案一
  • 参考资料

Unofficial AirPlay Protocol Specification

Screen Recorder SDK+Airplay Mirroring SDK For Windows

LFLiveKit


二、方案一实现流程(参考)

首先,需要分析一下你的需求,如果只是想实现投屏功能 并没有PC的客户端的定制需求(既 客户端自定义链接投屏界面而不是通过上拉菜单点击airplay镜像按钮自己选择设备);

  • 附比较图
对比图

原理分析:通过导入MediaPlayer.framework,调用api获取airplay可连接的设备列表,遍历列表找到你想要链接的设备name,选中此设备进行镜像;通过注册屏幕连接通知 检测设备屏幕个数及是否存在镜像来判断是否正在投屏;

2.1 首先下载项目AirplaySelector把项目里面的MediaPlayer Headers文件夹导入自己项目,然后控制器导入头文件即可使用API;


#import "ViewController.h"

#import "MPAudioVideoRoutingViewController.h"

#import "MPAVRoutingController.h"

#import "MPAVSystemRoutingController.h"

#import "MPAudioVideoRoutingTableViewController.h"

#import "MPAudioDeviceController.h"

#import "MPAVRoute.h"

2.2 注册通知 检测屏幕连接状态

- (void)addNotification{
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(screenDidConnect:) name:UIScreenDidConnectNotification object:nil];
    
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(screenDidDisconnect:) name:UIScreenDidDisconnectNotification object:nil];
}

#pragma mark - Notifications Handler

- (void)screenDidConnect:(NSNotification *)notification
{
    NSLog(@"connect");
    
    [self checkConnected];
}

- (void)screenDidDisconnect:(NSNotification *)notification
{
    NSLog(@"disconnect");
    
    [self checkConnected];
}

  • 检查是否在投屏
- (bool)checkConnected {
    
    // we need to use the secondary screen since the mainScreen does not mirror itself
    if ([[UIScreen screens] count] > 1) {
        UIScreen *secondaryScreen = [[UIScreen screens] objectAtIndex:1];
        NSLog(@"%@", secondaryScreen.mirroredScreen); // is actually the mainScreen
        if(secondaryScreen.mirroredScreen != nil){
            //screen is being mirrored
            [self changeLabelText:@"投屏中"];
            [_linkBtn setImage:[UIImage imageNamed:@"btn_end"] forState:UIControlStateNormal];
            [_linkBtn setImage:[UIImage imageNamed:@"btn_end_on"] forState:UIControlStateHighlighted];
            return true;
        }
    }
    [self changeLabelText:@"投屏异常,请重试!"];
    [_linkBtn setImage:[UIImage imageNamed:@"try_again"] forState:UIControlStateNormal];
    [_linkBtn setImage:[UIImage imageNamed:@"try_again_on"] forState:UIControlStateHighlighted];
    return false;
}

2.3 遍历airplay列表 连接与你输入name相同的设备(_severName.text 即你输入的设备name)

- (void)searchAirplayDevices {
    
    MPAudioVideoRoutingTableViewController *tableViewController = [[MPAudioVideoRoutingTableViewController alloc] initWithType:0 displayMirroringRoutes:YES];
    
    MPAVRoutingController *tableRouteController = [tableViewController routingController];
    _tableRouteController = tableRouteController;
    
    [tableRouteController fetchAvailableRoutesWithCompletionHandler:^(NSArray *routes) {
        NSLog(@"* Tried %d times", i);
        for (MPAVRoute *route in routes) {
            MPAVRoute *displayRoute = [route wirelessDisplayRoute];
            NSLog(@"* display route %@", displayRoute);
            if (displayRoute) {
                
                if ([[displayRoute routeName] isEqualToString:_severName.text]) {
                    
                    mirrorFound = true;
                    
//                    NSString *text =[[NSString stringWithFormat:@"Found!! "] stringByAppendingString:[displayRoute routeName]];
                    
                    NSLog(@"found!! %@", [displayRoute routeName]);
                    [tableRouteController pickRoute:displayRoute];
                    
                }else{
                   
                    NSLog(@"* SauceAirplay not found!!");
                    
                }
            }
            
        }
        if (!mirrorFound){
            [self changeLabelText:@"投屏异常,请重试!"];
            [_linkBtn setImage:[UIImage imageNamed:@"try_again"] forState:UIControlStateNormal];
            [_linkBtn setImage:[UIImage imageNamed:@"try_again_on"] forState:UIControlStateHighlighted];
        }
    }];
    
}
  • 断开连接代码
 [_tableRouteController pickHandsetRoute];

以上,即是通过输入设备name 自动连接airplay镜像方案 ,如果PC端需要集成airserver服务 请自行参考上面Airplay Mirroring SDK For Windows 我们公司windows端没搞定 如果你搞定了可以联系我 互相学习- -;


三、方案二实现流程

实现原理:利用XDWScreenRecorderSDK获取实时视频流,通过LFliveKit将视频流推流到rtmp服务器。然后pc端或者电视拉流直播;

  • 界面效果图
投屏

3.1 下载项目LiveScreenStreamForiOS,将项目下include文件夹导入自己项目中lib文件夹下的.a文件引入到项目中,通过pod导入入LFLiveKit第三方库或者自己手动导入;

截图

3.2 导入头文件 准守协议

#import "LivingViewController.h"
#import "LFLiveKit.h"
#import "XDWScreenRecorder.h"
#import "ScanResultModel.h"

@interface LivingViewController () <XDWScreenRecorderDelegate,CmdSocketDelegate>
@property(nonatomic, strong) LFLiveStreamInfo *liveStreamInfo;
@property(nonatomic, strong) LFLiveSession *lfLiveSession;
@property(nonatomic, strong) XDWScreenRecorder *screenRecorder;

@property (weak, nonatomic) IBOutlet UIImageView *screenType;
@property (weak, nonatomic) IBOutlet UILabel *typeLabel;
@property (weak, nonatomic) IBOutlet UIButton *screenBtn;
@property (weak, nonatomic) IBOutlet UIView *baseView;
@property (nonatomic, strong) UIButton *leftBtn;
@property (nonatomic, strong) UIButton *rightBtn;

@end
  • 配置screenRecorderlfLiveSession参数
- (XDWScreenRecorder *)screenRecorder {
    if (!_screenRecorder) {
        XDWScreenRecorderConfig *screenRecorderConfig = [[XDWScreenRecorderConfig alloc] init];
        screenRecorderConfig.videoSize = CGSizeMake(1080, 1920);
        screenRecorderConfig.framerate = 24;
        screenRecorderConfig.airTunesPort = 57000;
        screenRecorderConfig.airVideoPort = 8134;
        screenRecorderConfig.activeCode = "000000000";
        screenRecorderConfig.airPlayName = "ios";
        screenRecorderConfig.autoRotate = 0; //0 or 90 or 270
        
        _screenRecorder = [[XDWScreenRecorder alloc] initWithConfig:screenRecorderConfig];
        _screenRecorder.delegate = self;
    }
    
    return _screenRecorder;
}

- (LFLiveSession *)lfLiveSession {
    if (!_lfLiveSession) {
        
        _lfLiveSession = [[LFLiveSession alloc] initWithAudioConfiguration:[LFLiveAudioConfiguration defaultConfiguration]
                                                        videoConfiguration:[LFLiveVideoConfiguration defaultConfigurationForQuality:LFLiveVideoQuality_Medium2 outputImageOrientation:UIInterfaceOrientationPortrait]
                                                               captureType:LFLiveCaptureMaskAudioInputVideo];
        
    }
    
    return _lfLiveSession;
}

3.3 实现协议方法

#pragma mark - XDWScreenRecorderDelegate

- (void)screenRecorder:(XDWScreenRecorder *)screenRecorder didStartRecordingWithVideoSize:(CGSize)videoSize {
    //这里开启lflive
    self.liveStreamInfo.videoConfiguration.videoSize = videoSize;
    self.lfLiveSession.running = YES;
}

- (void)screenRecorder:(XDWScreenRecorder *)screenRecorder startError:(NSError *)error {
    //一般无法开启airplay或者其他错误的时候会调用此方法 逻辑自行处理
    //恢复按钮使用
    self.rightBtn.enabled = YES;
    self.leftBtn.enabled = YES;
    self.screenBtn.enabled = YES;
    
    [self changeScreenType:Reconnection];
}

- (void)screenRecorder:(XDWScreenRecorder *)screenRecorder videoBuffer:(CVPixelBufferRef)buffer timestamp:(NSTimeInterval)timestamp {
    //这里的buffer就是实时获取的视频流 通过lf推流到自己的rtmp服务器
    [self.lfLiveSession pushVideo:buffer];
}

- (void)screenRecorderDidStopRecording:(XDWScreenRecorder *)screenRecorder {
    //录屏结束时候调用
}
  • 进入后台
- (void)home{
    //挂起程序
    [[UIApplication sharedApplication] performSelector:@selector(suspend)];
}
  • 结束投屏
    [self.screenRecorder stop];
    
    [self.lfLiveSession stopLive];
    
    [self.lfLiveSession setRunning:NO];
    
    self.lfLiveSession.delegate = nil;

  • 由于各个项目业务逻辑不同 逻辑部分自行处理 还有一点需要注意 后台录屏请开启一下设置


    截图

结语

最近项目不紧 有时间会更新一些项目中遇到的困难 供大家参考 学习, 有兴趣的同学也可以一起交流;

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容