【目錄】
- 如何開發出一款仿映客直播APP專案實踐篇 -【原理篇】
- 如何開發出一款仿映客直播APP專案實踐篇 -【採集篇 】
- 如何開發出一款仿映客直播APP專案實踐篇 -【伺服器搭建+推流】
- 如何開發出一款仿映客直播APP專案實踐篇 -【播放篇】
【採集基本原理】
採集: 硬體(攝像頭)視訊影象
推流: 就是將採集到的音訊,視訊資料通過流媒體協議傳送到流媒體伺服器。
推流前的工作:採集,處理,編碼壓縮
推流中做的工作: 封裝,上傳
推流前的工作
推流——採集到的音訊,視訊資料通過流媒體協議傳送到流媒體伺服器
【視訊採集】
– 方法一:利用封裝庫LFLiveKit(推薦)
– 方法二:利用系統庫AVFoundation
接下來,我會分別貼上兩種方法程式碼
其實 LFLiveKit 已經實現了 後臺錄製、美顏功能、支援h264、AAC硬編碼,動態改變速率,RTMP傳輸等,對AVFoundation庫進行了封裝,我們真正開發的時候直接使用就很方便啦。另外也有:
LiveVideoCoreSDK : 實現了美顏直播和濾鏡功能,我們只要填寫RTMP服務地址,直接就可以進行推流啦。
PLCameraStreamingKit: 也是一個不錯的 RTMP 直播推流 SDK。
雖然推薦用 LFLiveKit 已包含採集、美顏、編碼、推流等功能,而為了進一步瞭解採集到推流完整過程,可以參觀方法二程式碼按自己的步子試著走走,詳細講解每個流程的原理。
方法一、利用LFLiveKit
xib上新增兩個Button 和一個Label (主要監聽連線狀態)
2.建立CaputuereLiveViewController.m類 註釋都寫在文件中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 |
// CaputuereLiveViewController.m // ZKKLiveAPP // // Created by Kevin on 16/11/12. // Copyright © 2016年 zhangkk. All rights reserved. // #import "CaputuereLiveViewController.h" #import @interface CaputuereLiveViewController (){ LFLiveSession *_session; } //總控制物件 @property(nonatomic,strong)LFLiveSession *session; // 推流狀態(下一篇推流時用到的) @property (weak, nonatomic) IBOutlet UILabel *linkStatusLb; //美顏 @property (weak, nonatomic) IBOutlet UIButton *beautyBtn; - (IBAction)beautyBtn:(UIButton *)sender; //切換攝像頭 @property (weak, nonatomic) IBOutlet UIButton *changCamreBtn; - (IBAction)changCamreBtn:(UIButton *)sender; - (IBAction)backBtn:(UIButton *)sender; @end @implementation CaputuereLiveViewController -(void )viewWillAppear:(BOOL)animated{ [super viewWillAppear:YES]; [UIApplication sharedApplication].statusBarHidden = YES; self.tabBarController.tabBar.hidden = YES; self.hidesBottomBarWhenPushed = YES; [self requestAccessForVideo];//請求視訊採集許可權 [self requestAccessForAudio];//請求音訊許可權 //開始錄製 [self startLive]; } - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor= [UIColor clearColor]; } -(void)viewWillDisappear:(BOOL)animated{ [super viewWillDisappear:YES]; [self stopLive]; } #pragma mark -- Public Method -(void)requestAccessForVideo{ __weak typeof(self) _self = self; AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; switch (status) { case AVAuthorizationStatusNotDetermined: { //許可對話沒有出現 則設定請求 [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) { if(granted){ dispatch_async(dispatch_get_main_queue(), ^{ [_self.session setRunning:YES]; }); } }]; break; } case AVAuthorizationStatusAuthorized: { dispatch_async(dispatch_get_main_queue(), ^{ [_self.session setRunning:YES]; }); break; } case AVAuthorizationStatusDenied: case AVAuthorizationStatusRestricted: //使用者獲取失敗 break; default: break; } } -(void)requestAccessForAudio{ AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio]; switch (status) { case AVAuthorizationStatusNotDetermined:{ [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL granted) { }]; } break; case AVAuthorizationStatusAuthorized: break; case AVAuthorizationStatusRestricted: case AVAuthorizationStatusDenied: break; default: break; } } #pragma mark -- LFStreamingSessionDelegate /** 連結狀態 */ -(void)liveSession:(LFLiveSession *)session liveStateDidChange:(LFLiveState)state{ switch (state) { case LFLiveReady: _linkStatusLb.text = @"未連線"; break; case LFLivePending: _linkStatusLb.text = @"連線中..."; break; case LFLiveStart: _linkStatusLb.text = @"開始連線"; break; case LFLiveStop: _linkStatusLb.text = @"斷開連線"; break; case LFLiveError: _linkStatusLb.text = @"連線錯誤"; default: break; } } /*dug CallBack*/ -(void)liveSession:(LFLiveSession *)session debugInfo:(LFLiveDebug *)debugInfo{ NSLog(@"bugInfo:%@",debugInfo); } /** callback socket errorcode */ - (void)liveSession:(nullable LFLiveSession *)session errorCode:(LFLiveSocketErrorCode)errorCode { NSLog(@"errorCode: %ld", errorCode); } /** **Live */ -(void )startLive{ LFLiveStreamInfo *stream = [LFLiveStreamInfo new]; /*stream.url = @"rtmp://192.168.0.2:1990/liveApp/room"; [self.session startLive:stream];*/後續推流時使用 } -(void)stopLive{ [self.session stopLive]; } - (LFLiveSession*)session { if (!_session) { _session = [[LFLiveSession alloc] initWithAudioConfiguration:[LFLiveAudioConfiguration defaultConfiguration] videoConfiguration:[LFLiveVideoConfiguration defaultConfiguration]]; _session.preView = self.view;//將攝像頭採集資料來源渲染到view上 _session.delegate = self; } return _session; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // Dispose of any resources that can be recreated. } /** **Action 美顏/切換前後攝像頭 @param sender button */ - (IBAction)beautyBtn:(UIButton *)sender { sender.selected = !sender.selected; self.session.beautyFace = !self.session.beautyFace; } - (IBAction)changCamreBtn:(UIButton *)sender { AVCaptureDevicePosition position = self.session.captureDevicePosition; self.session.captureDevicePosition = (position == AVCaptureDevicePositionBack)?AVCaptureDevicePositionBack:AVCaptureDevicePositionFront; } - (IBAction)backBtn:(UIButton *)sender { NSLog(@"返回"); // self.view.window.rootViewController = self.tabBarController; [self.tabBarController setSelectedIndex:0]; self.tabBarController.tabBar.hidden = NO; } */ @end |
方法二、利用系統AVFoundation採集視訊
一、採集硬體(攝像頭)視訊影象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
#import "CaputureViewController.h" #import #import "GPUImageBeautifyFilter.h" @interface CaputureViewController () /**採集視訊*/ //切換螢幕按鈕 @property (weak, nonatomic) IBOutlet UIButton *changScreenBtn; //採集視訊總控制 @property(nonatomic,strong)AVCaptureSession *captureSession; //視訊採集輸入資料來源 @property(nonatomic,strong)AVCaptureDeviceInput *currentVideoDeviceInput; //將攝像頭採集資料來源顯示在螢幕上 @property(nonatomic,weak)AVCaptureVideoPreviewLayer *previedLayer; //採集的擷取資料流 一般用與美顏等處理 @property(nonatomic,weak)AVCaptureConnection *videoConnection; - (IBAction)changScreenBtn:(UIButton *)sender; /*開啟美顏*/ @property (weak, nonatomic) IBOutlet UISwitch *openBeautySwitch; - (IBAction)switch:(UISwitch *)sender; //@property(nonatomic,)BOOL isOpenBeauty; //@property(nonatomic,strong) *; @end @implementation CaputureViewController -(void)viewWillAppear:(BOOL)animated{ [super viewWillAppear:YES]; if (_captureSession) { [_captureSession startRunning]; } } - (void)viewDidLoad { [super viewDidLoad]; [self.view addSubview:self.focusCursorImageView]; self.view.backgroundColor = [UIColor whiteColor]; /*1. 採集視訊 -avfoundation */ [self setupCaputureVideo]; /*2. GPUImage 美顏檢視 */ } - (void)viewWillDisappear:(BOOL)animated{ [super viewWillDisappear:YES]; if (_captureSession) { [_captureSession stopRunning]; } } /** 音視訊捕獲 */ -(void)setupCaputureVideo{ //建立管理物件 _captureSession = [[AVCaptureSession alloc]init]; //獲取攝像頭和音訊 // AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; AVCaptureDevice *videoDevice = [self getVideoDevice:AVCaptureDevicePositionFront]; AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio]; //建立對應音視訊裝置輸入物件 AVCaptureDeviceInput *videoDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:nil]; AVCaptureDeviceInput * audioDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:nil]; _currentVideoDeviceInput = videoDeviceInput; if ([_captureSession canAddInput:_currentVideoDeviceInput]) { [_captureSession addInput:_currentVideoDeviceInput]; } if ([_captureSession canAddInput:audioDeviceInput]) { [_captureSession canAddInput:audioDeviceInput]; } //獲取系統輸出的視訊源 AVCaptureVideoDataOutput *videoOutput = [[AVCaptureVideoDataOutput alloc]init]; AVCaptureAudioDataOutput *audioOutput = [[AVCaptureAudioDataOutput alloc]init]; //序列對列 dispatch_queue_t videoQueue = dispatch_queue_create("VideoQueue",DISPATCH_QUEUE_SERIAL); dispatch_queue_t audioQueue = dispatch_queue_create("audioQueue", DISPATCH_QUEUE_SERIAL); [videoOutput setSampleBufferDelegate:self queue:videoQueue]; [audioOutput setSampleBufferDelegate:self queue:audioQueue]; videoOutput.videoSettings = @{(NSString*)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA)}; // _videoOutput.videoSettings = captureSettings; //新增輸出源 到控制類session中 if ([_captureSession canAddOutput:videoOutput]) { [_captureSession addOutput: videoOutput]; } if ([_captureSession canAddOutput:audioOutput]) { [_captureSession addOutput:audioOutput]; } //獲取視訊輸入和輸出的連結 用於分辨音視訊資料 做處理時用到 _videoConnection = [videoOutput connectionWithMediaType:AVMediaTypeVideo]; //將視屏資料加入檢視層 顯示 AVCaptureVideoPreviewLayer *previedLayer = [AVCaptureVideoPreviewLayer layerWithSession:_captureSession]; previedLayer.frame = [UIScreen mainScreen].bounds; [self.view.layer insertSublayer:previedLayer atIndex:0]; [self.view.layer insertSublayer:_changScreenBtn.layer atIndex:1]; _previedLayer = previedLayer; [_captureSession startRunning]; } //獲取切換後的攝像頭 - (IBAction)changScreenBtn:(UIButton *)sender { //獲取當前的攝像頭 AVCaptureDevicePosition curPosition = _currentVideoDeviceInput.device.position; //獲取改變的方向 AVCaptureDevicePosition togglePosition = curPosition == AVCaptureDevicePositionFront?AVCaptureDevicePositionBack:AVCaptureDevicePositionFront; //獲取當前的攝像頭 AVCaptureDevice *toggleDevice = [self getVideoDevice:togglePosition]; //切換輸入裝置 AVCaptureDeviceInput *toggleDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:toggleDevice error:nil]; [_captureSession removeInput:_currentVideoDeviceInput]; [_captureSession addInput:toggleDeviceInput]; _currentVideoDeviceInput = toggleDeviceInput; } -(AVCaptureDevice *)getVideoDevice:(AVCaptureDevicePosition)position { NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]; for( AVCaptureDevice *device in devices) { if (device .position == position) { return device; } } return nil; } -(UIImageView *)focusCursorImageView{ if (!_focusCursorImageView) { _focusCursorImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"focus"]]; } return _focusCursorImageView; } #pragma mark - AVCaptureVideoDataOutputSampleBufferDelegate //擷取輸出的視訊資料 -(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection{ if (_videoConnection == connection) { NSLog(@"採集的視訊資料"); /*美顏*/ }else{ NSLog(@"採集的音訊資料"); } } |
上述是大致實現獲取最基本資料的情況,一些細節(尺寸、方向)暫時沒有深入,真正做直播的時候,一般是視訊和音訊是分開處理的,只有重點注意那個代理方法。
二、GPUImage 處理
在進行編碼 H.264 之前,一般來說肯定會做一些美顏處理的,否則那播出的感覺太真實,就有點醜啦,在此以磨皮和美白為例簡單瞭解。(具體參考的是:琨君 基於 GPUImage 的實時美顏濾鏡)
直接用 BeautifyFaceDemo 中的類 GPUImageBeautifyFilter
, 可以對的圖片直接進行處理:
1 2 3 4 |
GPUImageBeautifyFilter *filter = [[GPUImageBeautifyFilter alloc] init]; UIImage *image = [UIImage imageNamed:@"testMan"]; UIImage *resultImage = [filterimageByFilteringImage:image]; self.backgroundView.image = resultImage; |
但是視訊中是怎樣進行美容處理呢?怎樣將其轉換的呢?平常我們這樣直接使用:
1 |
GPUImageBeautifyFilter *beautifyFilter = [[GPUImageBeautifyFilter alloc] init];[self.videoCamera addTarget:beautifyFilter];[beautifyFilter addTarget:self.gpuImageView]; |
此處用到了 GPUImageVideoCamera,可以大致瞭解下 GPUImage詳細解析(三)- 實時美顏濾鏡:
GPUImageVideoCamera: GPUImageOutput的子類,提供來自攝像頭的影象資料作為源資料,一般是響應鏈的源頭。
GPUImageView:響應鏈的終點,一般用於顯示GPUImage的影象。
GPUImageFilter:用來接收源影象,通過自定義的頂點、片元著色器來渲染新的影象,並在繪製完成後通知響應鏈的下一個物件。
GPUImageFilterGroup:多個GPUImageFilter的集合。
GPUImageBeautifyFilter:
1 2 3 4 5 6 |
@interface GPUImageBeautifyFilter : GPUImageFilterGroup { GPUImageBilateralFilter *bilateralFilter; GPUImageCannyEdgeDetectionFilter *cannyEdgeFilter; GPUImageCombinationFilter *combinationFilter; GPUImageHSBFilter *hsbFilter; } |
簡單理解這個美顏的流程
不得不說GPUImage 是相當強大的,此處的功能也只是顯現了一小部分,其中 filter 那塊的處理個人目前還有好多不理解,需要去深入瞭解啃原始碼,暫時不過多引入。通過這個過程將 sampleBuffer 美容處理後,自然是進行編碼啦。
三、視訊、音訊壓縮編碼
而編碼是用 硬編碼呢 還是軟編碼呢? 相同位元速率,軟編影象質量更清晰,但是耗電更高,而且會導致CPU過熱燙到攝像頭。不過硬編碼會涉及到其他平臺的解碼,有很多坑。綜合來說,iOS 端硬體相容性較好,iOS 8.0佔有率也已經很高了,可以直接採用硬編。
硬編碼:下面幾個DEMO 可以對比下,當然看 LFLiveKit 更直接。
VideoToolboxPlus
iOSHardwareDecoder
-VideoToolboxDemo
iOS-h264Hw-Toolbox
四、推流
封裝資料成 FLV,通過 RTMP 協議打包上傳,從主播端到服務端即基本完成推流。
4-1、封裝資料通常是封裝成 FLV
FLV流媒體格式是一種新的視訊格式,全稱為FlashVideo。由於它形成的檔案極小、載入速度極快,使得網路觀看視訊檔案成為可能,它的出現有效地解決了視訊檔案匯入Flash後,使匯出的SWF檔案體積龐大,不能在網路上很好的使用等缺點。
(封包 FLV):一般FLV 檔案結構裡是這樣存放的:
[[Flv Header]
[Metainfo Tag]
[Video Tag]
[Audio Tag]
[Video Tag]
[Audio Tag]
[Other Tag]…]
其中 AudioTag 和 VideoTag 出現的順序隨機的,沒有嚴格的定義。Flv Header 是檔案的頭部,用FLV字串標明瞭檔案的型別,以及是否有音訊、視訊等資訊。之後會有幾個位元組告訴接下來的包位元組數。Metainfo 中用來描述Flv中的各種引數資訊,例如視訊的編碼格式、解析度、取樣率等等。如果是本地檔案(非實時直播流),還會有偏移時間戳之類的資訊用於支援快進等操作。VideoTag 存放視訊資料。對於H.264來說,第一幀傳送的NALU應為 SPS和PPS,這個相當於H.264的檔案頭部,播放器解碼流必須先要找到這個才能進行播放。之後的資料為I幀或P幀。AudioTag 存放音訊資料。對於AAC來說,我們只需要在每次硬編碼完成後給資料加上adts頭部資訊即可。
iOS 中的使用:詳細看看 LFLiveKit 中的 LFStreamRTMPSocket 類。
總的說來,這又是一個粗略的過程,站在好多個巨人的肩膀上,但是還是基本瞭解了一個推流的流程,沒有正式專案的經驗,肯定有太很多細節點忽略了和好多坑需要填,還是那個目的,暫時先作為自己的預備知識點吧,不過此處可以擴充套件和深入的知識點真的太多啦,如 LFLiveKit 和 GPUImage 僅僅展露的是冰山一角。
程式碼地址:
gitHub : https://github.com/one-tea/ZKKLiveDemo
備註參考:
LiveVideoCoreSDK
LFLiveKit
GPUImage
LMLiveStreaming
PLCameraStreamingKit
iOS手機直播Demo技術簡介
iOS視訊開發經驗
iOS 上的相機捕捉
CMSampleBufferRef 與 UIImage 的轉換
GPUImage詳細解析(三)- 實時美顏濾鏡
iOS8系統H264視訊硬體編解碼說明
利用FFmpeg+x264將iOS攝像頭實時視訊流編碼為h264檔案
使用VideoToolbox硬編碼H.264
使用iOS自帶AAC編碼器
如何搭建一個完整的視訊直播系統?
直播中累積延時的優化
使用VLC做流媒體伺服器(直播形式)
gitHub程式碼地址:
Object-C版 : https://github.com/one-tea/ZKKLiveDemo
Swift版 : https://github.com/one-tea/ZKKLiveAPP_Swift3.0