一直在忙, 也沒寫過幾次播客! 但一直熱衷於直播開發技術, 公司又不是直播方向的, 所以就年前忙里偷襲研究了一下直播開發, 然后翻閱了很多大神的技術博客等, 寫了一個簡單的Demo, 又根據網上大神們的技術博客搭建了簡易的本地RTMP服務器! 由於時間問題, 沒來記得來記錄下來, 目前demo 只完成了直播音視頻采集, 轉碼, RTMP協議推流, 和本地RTMP簡易服務器 推流這一環節, 拉流還沒來得及寫, RTMP流的播放用的是VLC, 來實現視頻流的播放的!
網上有各種大牛寫的播客, 都很好的, 但我寫這篇播客的目的就是, 想記錄一下當時的思路, 還有分享出來, 讓各位大神指點一下不足之處, 來完善這個小項目! 表達一下我對直播開發的熱愛哈哈...如果有幸能給大家幫些忙, 我倍感榮幸!
好, 廢話不多說, 接下來我們直接開始!
代碼鏈接: Github: https://github.com/jessonliu/JFLivePlaye
技術部分------ ⬇️
腦塗: ![ 直播思維導圖.png ]
視頻直播的大概流程就上腦塗上所畫的, 還有一些沒列出來, 比如, 聊天, 送禮, 踢出, 禁言, 等等一系列功能, 但本文只是針對視頻直播的簡單實現!
下邊來說一下以下的幾個點和使用到的類(后邊會附上demo, 里邊還有詳細的備注)
1. 音視頻采集
音視頻采集, 網上也有很多大神些的技術博客, demo 等, 我這里邊只針對iOS 原聲的來介紹以下
利用AVFoundation框架, 進行音視頻采集
AVCaptureSession // 音視頻錄制期間管理者
AVCaptureDevice // 設備管理者, (用來操作所閃光燈, 聚焦, 攝像頭切換等)
AVCaptureDeviceInput // 音視頻輸入數據的管理對象
AVCaptureVideoDataOutput // 視頻輸出數據的管理者
AVCaptureAudioDataOutput // 音頻輸出數據的管理者
AVCaptureVideoPreviewLayer // 用來展示視頻的圖像
注意, 必須要設置音視頻輸出對象的代理方法, 然后在代理方法中獲取sampleBuffer, 然后判斷captureOutput是音頻還是視頻, 來進行音視頻數據相應的編碼
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
}
也可以利用GPUImageVideoCamera 來進行視頻數據的采集獲取, 可以利用GPUImage 進行美顏, 添加水印, 人臉識別等
2.流媒體
流媒體是指采用流式傳輸的方式在網上播放的媒體格式, 是邊傳邊播的媒體,是多媒體的一種!
然后就是大家需要了解的幾個關鍵詞
幀:視頻是由很多連續圖像組成, 每一幀就代表一幅靜止的圖像
GOP:(Group of Pictures)畫面組,一個GOP就是一組連續的畫面,每個畫面都是一幀,GOP就是很多幀的集合!
幀的分類:I幀、P幀、B幀
為了提高壓縮比例,降低視頻文件的大小,在針對連續動態圖像編碼時,一般會將連續若干幅圖像編碼為P、B、I三種幀類型
I幀:一組連續畫面(GOP)的第一個幀, I幀采用幀內壓縮法(也成關鍵幀壓縮法), I幀的壓縮不依靠與其他幀, 靠盡可能去除圖像空間冗余信息來壓縮的, 可以單獨作為圖像!
P幀:預測幀(也叫前向參考幀), P幀的壓縮依賴於前一幀, 通過充分降低與圖像序列中前面已編碼幀的時間冗余信息來壓縮傳輸數據量的編碼圖像!
B幀:也叫雙向預測幀, 當把一幀壓縮成B幀時,它根據鄰近的前幾幀、本幀以及后幾幀數據的不同點來壓縮本幀,也即僅記錄本幀與前后幀的差值。
幀率:就是在1秒鍾時間里傳輸的圖片的幀數,也可以理解為圖形處理器每秒鍾能夠刷新幾次,通常用FPS表示, 每秒鍾幀數 (fps) 愈多,所顯示的動作就會愈流暢!
碼率: 也成為比特率, 是指每秒傳送的比特(bit)數, 比特率越高,傳送數據速度越快, 單位為 bps(Bit Per Second)。
3. 音視頻的編解碼
音視頻編解碼, 說白了就是對音視頻數據進行壓縮, 減少數據對空間的占用, 便於網絡傳輸, 存儲和使用!
目前直播常用的音視頻編解碼方式是h.264/AVC, AAC/MP3
硬軟編解碼的區別:
硬解碼:由顯卡核心GPU來對高清視頻進行解碼工作,CPU占用率很低,畫質效果比軟解碼略差一點,需要對播放器進行設置。
優點:播放流暢、低功耗
缺點:受視頻格式限制、功耗大、畫質沒有軟解碼好
軟解碼:由CPU負責解碼進行播放
優點:不受視頻格式限制、畫質略好於硬解
缺點:會占用過高的資源、對於高清視頻可能沒有硬解碼流暢(主要看CPU的能力)
蘋果API有提供音視頻硬編解碼接口, 但只針對iOS8.0以上版本!
利用VideoToolbox 和AudioToolbox 這連個框架進行音視頻的硬編碼!
這里附上前輩們的關於VideoToolbox使用的簡書, http://www.jianshu.com/p/6dfe49b5dab8
和AudioToolbox的技術簡書http://www.jianshu.com/p/a671f5b17fc1
感興趣的話可以研究一下!
4.流媒體數據封裝
TS: 是流媒體封裝格式的一種,流媒體封裝的好處就是不需要加載索引再播放,大大降低了首次載入的延遲,兩個TS片段可以無縫拼接,播放器能連續播放!
FLV: 也是一種流媒體的封裝格式,但他形成的文件極小、加載速度極快,使得網絡觀看視頻文件成為可能,因此FLV格式成為了當今主流視頻格式
5.RTMP推流
大家先看一張圖, 常用的直播協議比較

這里只介紹一下RTMP協議, 如果還想了解更多的可在網上查找一下, 有很多關於流媒體協議的技術博客!
RTMP協議是基於TCP/IP 的協議簇;RTMP(Real Time Messaging Protocol)實時消息傳送協議是Adobe Systems公司為Flash播放器和服務器之間音頻、視頻和數據傳輸 開發的開放協議
它有多種變種:
a, RTMP工作在TCP之上,默認使用端口1935;
b, RTMPE在RTMP的基礎上增加了加密功能;
c, RTMPT封裝在HTTP請求之上,可穿透防火牆;
d, RTMPS類似RTMPT,增加了TLS/SSL的安全功能;
它是一個互聯網TCP/IP體系結構中應用層的協議。RTMP協議中基本的數據單元稱為消息(Message)。當RTMP協議在互聯網中傳輸數據的時候,消息會被拆分成更小的單元,稱為消息塊(Chunk)。RTMP傳輸媒體數據的過程中,發送端首先把媒體數據封裝成消息,然后把消息分割成消息塊,最后將分割后的消息塊通過TCP協議發送出去。接收端在通過TCP協議收到數據后,首先把消息塊重新組合成消息,然后通過對消息進行解封裝處理就可以恢復出媒體數據。
播放一個RTMP協議的流媒體需要經過以下幾個步驟:握手,建立連接,建立流,播放。
demo中RTMP協議推流, 用的是librtmp-iOS框架! 參考https://my.oschina.net/jerikc/blog/501948
6. 播放器
IJKPlayer 是一個基於 ffplay 的輕量級 Android/iOS 視頻播放器。API 易於集成;編譯配置可裁剪,方便控制安裝包大小;支持 硬件加速解碼,更加省電。而DanmakuFlameMaster(開源彈幕框架) 架構清晰,簡單易用,支持多種高效率繪制方式選擇,支持多種自定義功能設置!
代碼:
#import "JFLiveShowVC.h" 該類負責音視頻采集及展示, 用於時間沒問題, 沒有吧音視頻采集單獨拿出來封裝!
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. // 需要用到的線程 videoProcessingQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); audioProcessingQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0); _jfEncodeQueue_video = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); _jfEncodeQueue_audio = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); // 檢查權限和設備 [self checkDeviceAuth]; // 數據保存路徑 self.documentDictionary = [(NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES)) objectAtIndex:0]; // 音頻編碼對象初始化 self.audioEncoder = [[AACEncoder alloc] init]; self.audioEncoder.delegate = self; // 設置代理 self.videoEncoder = [[JFVideoEncoder alloc] init]; // 視頻編碼對象初始化 self.videoEncoder.delegate = self; // 設置代理 _lock = dispatch_semaphore_create(1); // 當並行執行的處理更新數據時,會產生數據不一致的情況,使用Serial Dipatch queue 進行同步, 控制並發 } // 檢查是否授權攝像頭的使用權限 - (void)checkDeviceAuth { switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) { case AVAuthorizationStatusAuthorized: // 已授權 NSLog(@"已授權"); [self initAVCaptureSession]; break; case AVAuthorizationStatusNotDetermined: // 用戶尚未進行允許或者拒絕, { [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) { if (granted) { NSLog(@"已授權"); [self initAVCaptureSession]; } else { NSLog(@"用戶拒絕授權攝像頭的使用, 返回上一頁, 請打開--> 設置 -- > 隱私 --> 通用等權限設置"); } }]; } break; default: { NSLog(@"用戶尚未授權攝像頭的使用權"); } break; } } // 初始化 管理者 - (void)initAVCaptureSession { self.session = [[AVCaptureSession alloc] init]; // 設置錄像的分辨率 // 先判斷是被是否支持要設置的分辨率 if ([self.session canSetSessionPreset:AVCaptureSessionPreset1280x720]) { // 如果支持則設置 [self.session canSetSessionPreset:AVCaptureSessionPreset1280x720]; } else if ([self.session canSetSessionPreset:AVCaptureSessionPresetiFrame960x540]) { [self.session canSetSessionPreset:AVCaptureSessionPresetiFrame960x540]; } else if ([self.session canSetSessionPreset:AVCaptureSessionPreset640x480]) { [self.session canSetSessionPreset:AVCaptureSessionPreset640x480]; } // 開始配置 [self.session beginConfiguration]; // 初始化視頻管理 self.videoDevice = nil; // 創建攝像頭類型數組 NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]; // 便利管理抓捕道德所有支持制定類型的 設備集合 for (AVCaptureDevice *device in devices) { if (device.position == AVCaptureDevicePositionFront) { self.videoDevice = device; } } // 視頻 [self videoInputAndOutput]; // 音頻 [self audioInputAndOutput]; // 錄制的同時播放 [self initPreviewLayer]; // 提交配置 [self.session commitConfiguration]; } // 視頻輸入輸出 - (void)videoInputAndOutput { NSError *error; // 視頻輸入 // 初始化 根據輸入設備來初始化輸出對象 self.videoInput = [[AVCaptureDeviceInput alloc] initWithDevice:self.videoDevice error:&error]; if (error) { NSLog(@"-- 攝像頭出錯 -- %@", error); return; } // 將輸入對象添加到管理者 -- AVCaptureSession 中 // 先判斷是否能搞添加輸入對象 if ([self.session canAddInput:self.videoInput]) { // 管理者能夠添加 才可以添加 [self.session addInput:self.videoInput]; } // 視頻輸出 // 初始化 輸出對象 self.videoOutput = [[AVCaptureVideoDataOutput alloc] init]; // 是否允許卡頓時丟幀 self.videoOutput.alwaysDiscardsLateVideoFrames = NO; if ([self supportsFastTextureUpload]) { // 是否支持全頻色彩編碼 YUV 一種色彩編碼方式, 即YCbCr, 現在視頻一般采用該顏色空間, 可以分離亮度跟色彩, 在不影響清晰度的情況下來壓縮視頻 BOOL supportsFullYUVRange = NO; // 獲取輸出對象 支持的像素格式 NSArray *supportedPixelFormats = self.videoOutput.availableVideoCVPixelFormatTypes; for (NSNumber *currentPixelFormat in supportedPixelFormats) { if ([currentPixelFormat intValue] == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) { supportsFullYUVRange = YES; } } // 根據是否支持 來設置輸出對象的視頻像素壓縮格式, if (supportsFullYUVRange) { [self.videoOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange] forKey:(id)kCVPixelBufferPixelFormatTypeKey]]; } else { [self.videoOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange] forKey:(id)kCVPixelBufferPixelFormatTypeKey]]; } } else { [self.videoOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_32BGRA] forKey:(id)kCVPixelBufferPixelFormatTypeKey]]; } // 設置代理 [self.videoOutput setSampleBufferDelegate:self queue:videoProcessingQueue]; // 判斷管理是否可以添加 輸出對象 if ([self.session canAddOutput:self.videoOutput]) { [self.session addOutput:self.videoOutput]; AVCaptureConnection *connection = [self.videoOutput connectionWithMediaType:AVMediaTypeVideo]; // 設置視頻的方向 connection.videoOrientation = AVCaptureVideoOrientationPortrait; // 視頻穩定設置 if ([connection isVideoStabilizationSupported]) { connection.preferredVideoStabilizationMode = AVCaptureVideoStabilizationModeAuto; } connection.videoScaleAndCropFactor = connection.videoMaxScaleAndCropFactor; } } // 音頻輸入輸出 - (void)audioInputAndOutput { NSError *jfError; // 音頻輸入設備 self.audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio]; // 音頻輸入對象 self.audioInput = [[AVCaptureDeviceInput alloc] initWithDevice:self.audioDevice error:&jfError]; if (jfError) { NSLog(@"-- 錄音設備出錯 -- %@", jfError); } // 將輸入對象添加到 管理者中 if ([self.session canAddInput:self.audioInput]) { [self.session addInput:self.audioInput]; } // 音頻輸出對象 self.audioOutput = [[AVCaptureAudioDataOutput alloc] init]; // 將輸出對象添加到管理者中 if ([self.session canAddOutput:self.audioOutput]) { [self.session addOutput:self.audioOutput]; } // 設置代理 [self.audioOutput setSampleBufferDelegate:self queue:audioProcessingQueue]; } // 播放同時進行播放 - (void)initPreviewLayer { [self.view layoutIfNeeded]; // 初始化對象 self.previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.session]; self.previewLayer.frame = self.view.layer.bounds; self.previewLayer.connection.videoOrientation = [self.videoOutput connectionWithMediaType:AVMediaTypeVideo].videoOrientation; self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill; self.previewLayer.position = CGPointMake(self.liveView.frame.size.width*0.5,self.liveView.frame.size.height*0.5); CALayer *layer = self.liveView.layer; layer.masksToBounds = true; [layer addSublayer:self.previewLayer]; } #pragma mark 返回上一級 - (IBAction)backAction:(id)sender { // 結束直播 [self.socket stop]; [self.session stopRunning]; [self.videoEncoder stopEncodeSession]; fclose(_h264File); fclose(_aacFile); [self.navigationController popViewControllerAnimated:YES]; } #pragma mark 開始直播 - (IBAction)startLiveAction:(UIButton *)sender { _h264File = fopen([[NSString stringWithFormat:@"%@/jf_encodeVideo.h264", self.documentDictionary] UTF8String], "wb"); _aacFile = fopen([[NSString stringWithFormat:@"%@/jf_encodeAudio.aac", self.documentDictionary] UTF8String], "wb"); // 初始化 直播流信息 JFLiveStreamInfo *streamInfo = [[JFLiveStreamInfo alloc] init]; streamInfo.url = @"rtmp://192.168.1.110:1935/rtmplive/room"; self.socket = [[JFRtmpSocket alloc] initWithStream:streamInfo]; self.socket.delegate = self; [self.socket start]; // 開始直播 [self.session startRunning]; sender.hidden = YES; } #pragma mark -- AVCaptureAudioDataOutputSampleBufferDelegate - (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection { if (captureOutput == self.audioOutput) { [self.audioEncoder encodeSampleBuffer:sampleBuffer timeStamp:self.currentTimestamp completionBlock:^(NSData *encodedData, NSError *error) { fwrite(encodedData.bytes, 1, encodedData.length, _aacFile); }]; } else { [self.videoEncoder encodeWithSampleBuffer:sampleBuffer timeStamp:self.currentTimestamp completionBlock:^(NSData *data, NSInteger length) { fwrite(data.bytes, 1, length, _h264File); }]; } } - (void)dealloc { if ([self.session isRunning]) { [self.session stopRunning]; } [self.videoOutput setSampleBufferDelegate:nil queue:dispatch_get_main_queue()]; [self.audioOutput setSampleBufferDelegate:nil queue:dispatch_get_main_queue()]; } // 是否支持快速紋理更新 - (BOOL)supportsFastTextureUpload; { #if TARGET_IPHONE_SIMULATOR return NO; #else #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wtautological-pointer-compare" return (CVOpenGLESTextureCacheCreate != NULL); #pragma clang diagnostic pop #endif } // 保存h264數據到文件 - (void) writeH264Data:(void*)data length:(size_t)length addStartCode:(BOOL)b { // 添加4字節的 h264 協議 start code const Byte bytes[] = "\x00\x00\x00\x01"; if (_h264File) { if(b) fwrite(bytes, 1, 4, _h264File); fwrite(data, 1, length, _h264File); } else { NSLog(@"_h264File null error, check if it open successed"); } } #pragma mark - JFRtmpSocketDelegate - (void)jf_videoEncoder_call_back_videoFrame:(JFVideoFrame *)frame { if (self.uploading) { [self.socket sendFrame:frame]; } } #pragma mark - AACEncoderDelegate - (void)jf_AACEncoder_call_back_audioFrame:(JFAudioFrame *)audionFrame { if (self.uploading) { [self.socket sendFrame:audionFrame]; } } #pragma mark -- JFRtmpSocketDelegate - (void)socketStatus:(nullable JFRtmpSocket *)socket status:(JFLiveState)status { switch (status) { case JFLiveReady: NSLog(@"准備"); break; case JFLivePending: NSLog(@"鏈接中"); break; case JFLiveStart: NSLog(@"已連接"); if (!self.uploading) { self.timestamp = 0; self.isFirstFrame = YES; self.uploading = YES; } break; case JFLiveStop: NSLog(@"已斷開"); break; case JFLiveError: NSLog(@"鏈接出錯"); self.uploading = NO; self.isFirstFrame = NO; self.uploading = NO; break; default: break; } } // 獲取當前時間戳 - (uint64_t)currentTimestamp{ dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); uint64_t currentts = 0; if(_isFirstFrame == true) { _timestamp = NOW; _isFirstFrame = false; currentts = 0; } else { currentts = NOW - _timestamp; } dispatch_semaphore_signal(_lock); return currentts; }
// 注: 必須控制好線程, 不然很容易出現卡死或閃退的情況!
關於音視頻編解碼的代碼, 就不在這里展示了, 放在demo 中, 有需要的話話可以下載!
Github: https://github.com/jessonliu/JFLivePlaye
本地流媒體服務器的搭建這個給大家一個連接: http://www.jianshu.com/p/8ea016b2720e
以上就是直播開發中所要設計道德知識點和一些第三方框架, 如果全是用第三方的話, 就會省事很多, 用起來也很方便, 但我個人比較喜歡刨根問題, 想了解原理!
如果寫的不妥當或不足的地方, 希望大神指正和補充! 由於前段時間比較忙, 拉流, 解碼和播放還沒來得及寫, 我在直播的路上還有很長的路要走, 還需要不斷地學習提高, 了解更底層的東西, 才能更好的掌握直播的整個流程技術, 后期寫完會更新一個完整的Demo!
