一直在忙, 也沒寫過幾次博客! 但一直熱衷于直播開發(fā)技術(shù), 公司又不是直播方向的, 所以就年前忙里偷閑研究了一下直播開發(fā), 然后翻閱了很多大神的技術(shù)博客等, 寫了一個(gè)簡(jiǎn)單的Demo, 又根據(jù)網(wǎng)上大神們的技術(shù)博客搭建了簡(jiǎn)易的本地RTMP服務(wù)器! 由于時(shí)間問題, 沒來記得來記錄下來, 目前demo 只完成了直播音視頻采集, 轉(zhuǎn)碼, RTMP協(xié)議推流, 和本地RTMP簡(jiǎn)易服務(wù)器 推流這一環(huán)節(jié), 拉流還沒來得及寫, RTMP流的播放用的是VLC, 來實(shí)現(xiàn)視頻流的播放的!
網(wǎng)上有各種大牛寫的播客, 都很好的, 但我寫這篇播客的目的就是, 想記錄一下當(dāng)時(shí)的思路, 還有分享出來, 讓各位大神指點(diǎn)一下不足之處, 來完善這個(gè)小項(xiàng)目! 表達(dá)一下我對(duì)直播開發(fā)的熱愛哈哈...如果有幸能給大家?guī)托┟? 我倍感榮幸!
?該篇文章只是對(duì)直播需要了解的一些概念等的一下介紹, 然后還有如何進(jìn)行視頻采集! 近期有時(shí)間會(huì)逐步一點(diǎn)一點(diǎn)詳細(xì)的寫文章來介紹!
計(jì)劃步驟下:
好, 廢話不多說, 接下來我們直接開始!
iOS-直播開發(fā)(開發(fā)從底層做起)
1. ?iOS直播開發(fā)(開發(fā)從底層做起)之音視頻采集
2. ?iOS-直播開發(fā)(開發(fā)從底層做起) --- 音視頻硬編碼
3. ?iOS-直播開發(fā)(開發(fā)從底層做起) --- RTMP 協(xié)議推流
4. ?iOS-直播開發(fā)(開發(fā)從底層做起) --- nginx 直播本地服務(wù)器搭建
等等
來一張實(shí)戰(zhàn)圖
代碼鏈接: Github: https://github.com/jessonliu/JFLivePlaye
技術(shù)部分------ ??
腦涂: ![ 直播思維導(dǎo)圖.png ]
視頻直播的大概流程就上腦涂上所畫的, 還有一些沒列出來, 比如, 聊天, 送禮, 踢出, 禁言, 等等一系列功能, 但本文只是針對(duì)視頻直播的簡(jiǎn)單實(shí)現(xiàn)!
下邊來說一下以下的幾個(gè)點(diǎn)和使用到的類(后邊會(huì)附上demo, 里邊還有詳細(xì)的備注)
> 1. 音視頻采集
音視頻采集, 網(wǎng)上也有很多大神些的技術(shù)博客, demo 等, 我這里邊只針對(duì)iOS 原聲的來介紹以下
利用AVFoundation框架, 進(jìn)行音視頻采集
AVCaptureSession ? ? ? ? ? ? ? ? ? ? ? // 音視頻錄制期間管理者
AVCaptureDevice ? ? ? ? ? ? ? ? ? ? ? ? // 設(shè)備管理者, (用來操作所閃光燈, 聚焦, 攝像頭切換等)
AVCaptureDeviceInput ? ? ? ? ? ? ? ?// 音視頻輸入數(shù)據(jù)的管理對(duì)象
AVCaptureVideoDataOutput ? ? ? ?// 視頻輸出數(shù)據(jù)的管理者
AVCaptureAudioDataOutput ? ? ? ?// 音頻輸出數(shù)據(jù)的管理者
AVCaptureVideoPreviewLayer ? ? // 用來展示視頻的圖像
注意, 必須要設(shè)置音視頻輸出對(duì)象的代理方法, 然后在代理方法中獲取sampleBuffer, 然后判斷captureOutput是音頻還是視頻, 來進(jìn)行音視頻數(shù)據(jù)相應(yīng)的編碼
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
}
也可以利用GPUImageVideoCamera 來進(jìn)行視頻數(shù)據(jù)的采集獲取, 可以利用GPUImage 進(jìn)行美顏, 添加水印, 人臉識(shí)別等
2.流媒體
流媒體是指采用流式傳輸?shù)姆绞皆诰W(wǎng)上播放的媒體格式, 是邊傳邊播的媒體,是多媒體的一種!
然后就是大家需要了解的幾個(gè)關(guān)鍵詞
幀:視頻是由很多連續(xù)圖像組成, 每一幀就代表一幅靜止的圖像
GOP:(Group of Pictures)畫面組,一個(gè)GOP就是一組連續(xù)的畫面,每個(gè)畫面都是一幀,GOP就是很多幀的集合!
幀的分類:I幀、P幀、B幀
為了提高壓縮比例,降低視頻文件的大小,在針對(duì)連續(xù)動(dòng)態(tài)圖像編碼時(shí),一般會(huì)將連續(xù)若干幅圖像編碼為P、B、I三種幀類型
I幀:一組連續(xù)畫面(GOP)的第一個(gè)幀, I幀采用幀內(nèi)壓縮法(也成關(guān)鍵幀壓縮法), I幀的壓縮不依靠與其他幀, 靠盡可能去除圖像空間冗余信息來壓縮的, 可以單獨(dú)作為圖像!
P幀:預(yù)測(cè)幀(也叫前向參考幀), P幀的壓縮依賴于前一幀, 通過充分降低與圖像序列中前面已編碼幀的時(shí)間冗余信息來壓縮傳輸數(shù)據(jù)量的編碼圖像!
B幀:也叫雙向預(yù)測(cè)幀, 當(dāng)把一幀壓縮成B幀時(shí),它根據(jù)鄰近的前幾幀、本幀以及后幾幀數(shù)據(jù)的不同點(diǎn)來壓縮本幀,也即僅記錄本幀與前后幀的差值。
幀率:就是在1秒鐘時(shí)間里傳輸?shù)膱D片的幀數(shù),也可以理解為圖形處理器每秒鐘能夠刷新幾次,通常用FPS表示, 每秒鐘幀數(shù) (fps) 愈多,所顯示的動(dòng)作就會(huì)愈流暢!
碼率: ? 也成為比特率, 是指每秒傳送的比特(bit)數(shù), 比特率越高,傳送數(shù)據(jù)速度越快, 單位為 bps(Bit Per Second)。
3. 音視頻的編解碼
音視頻編解碼, 說白了就是對(duì)音視頻數(shù)據(jù)進(jìn)行壓縮, 減少數(shù)據(jù)對(duì)空間的占用, 便于網(wǎng)絡(luò)傳輸, 存儲(chǔ)和使用!
目前直播常用的音視頻編解碼方式是h.264/AVC, AAC/MP3
硬軟編解碼的區(qū)別:
硬解碼:由顯卡核心GPU來對(duì)高清視頻進(jìn)行解碼工作,CPU占用率很低,畫質(zhì)效果比軟解碼略差一點(diǎn),需要對(duì)播放器進(jìn)行設(shè)置。
優(yōu)點(diǎn):播放流暢、低功耗
缺點(diǎn):受視頻格式限制、功耗大、畫質(zhì)沒有軟解碼好
軟解碼:由CPU負(fù)責(zé)解碼進(jìn)行播放
優(yōu)點(diǎn):不受視頻格式限制、畫質(zhì)略好于硬解
缺點(diǎn):會(huì)占用過高的資源、對(duì)于高清視頻可能沒有硬解碼流暢(主要看CPU的能力)
蘋果API有提供音視頻硬編解碼接口, 但只針對(duì)iOS8.0以上版本!
利用VideoToolbox 和AudioToolbox 這連個(gè)框架進(jìn)行音視頻的硬編碼!
這里附上前輩們的關(guān)于VideoToolbox使用的簡(jiǎn)書, http://www.lxweimin.com/p/6dfe49b5dab8
和AudioToolbox的技術(shù)簡(jiǎn)書http://www.lxweimin.com/p/a671f5b17fc1
感興趣的話可以研究一下!
4.流媒體數(shù)據(jù)封裝
TS: 是流媒體封裝格式的一種,流媒體封裝的好處就是不需要加載索引再播放,大大降低了首次載入的延遲,兩個(gè)TS片段可以無縫拼接,播放器能連續(xù)播放!
FLV: 也是一種流媒體的封裝格式,但他形成的文件極小、加載速度極快,使得網(wǎng)絡(luò)觀看視頻文件成為可能,因此FLV格式成為了當(dāng)今主流視頻格式
5.RTMP推流
大家先看一張圖, 常用的直播協(xié)議比較
這里只介紹一下RTMP協(xié)議, 如果還想了解更多的可在網(wǎng)上查找一下, 有很多關(guān)于流媒體協(xié)議的技術(shù)博客!
RTMP協(xié)議是基于TCP/IP 的協(xié)議簇;RTMP(Real Time Messaging Protocol)實(shí)時(shí)消息傳送協(xié)議是Adobe Systems公司為Flash播放器和服務(wù)器之間音頻、視頻和數(shù)據(jù)傳輸 開發(fā)的開放協(xié)議
它有多種變種:
a, RTMP工作在TCP之上,默認(rèn)使用端口1935;
b, RTMPE在RTMP的基礎(chǔ)上增加了加密功能;
c, RTMPT封裝在HTTP請(qǐng)求之上,可穿透防火墻;
d, RTMPS類似RTMPT,增加了TLS/SSL的安全功能;
它是一個(gè)互聯(lián)網(wǎng)TCP/IP體系結(jié)構(gòu)中應(yīng)用層的協(xié)議。RTMP協(xié)議中基本的數(shù)據(jù)單元稱為消息(Message)。當(dāng)RTMP協(xié)議在互聯(lián)網(wǎng)中傳輸數(shù)據(jù)的時(shí)候,消息會(huì)被拆分成更小的單元,稱為消息塊(Chunk)。RTMP傳輸媒體數(shù)據(jù)的過程中,發(fā)送端首先把媒體數(shù)據(jù)封裝成消息,然后把消息分割成消息塊,最后將分割后的消息塊通過TCP協(xié)議發(fā)送出去。接收端在通過TCP協(xié)議收到數(shù)據(jù)后,首先把消息塊重新組合成消息,然后通過對(duì)消息進(jìn)行解封裝處理就可以恢復(fù)出媒體數(shù)據(jù)。
播放一個(gè)RTMP協(xié)議的流媒體需要經(jīng)過以下幾個(gè)步驟:握手,建立連接,建立流,播放。
demo中RTMP協(xié)議推流, 用的是librtmp-iOS框架! 參考https://my.oschina.net/jerikc/blog/501948
6. 播放器
IJKPlayer 是一個(gè)基于 ffplay 的輕量級(jí) Android/iOS 視頻播放器。API 易于集成;編譯配置可裁剪,方便控制安裝包大小;支持 硬件加速解碼,更加省電。而DanmakuFlameMaster(開源彈幕框架) 架構(gòu)清晰,簡(jiǎn)單易用,支持多種高效率繪制方式選擇,支持多種自定義功能設(shè)置!
代碼:
#import "JFLiveShowVC.h" 該類負(fù)責(zé)音視頻采集及展示, 用于時(shí)間沒問題, 沒有吧音視頻采集單獨(dú)拿出來封裝!
`
- (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);
// 檢查權(quán)限和設(shè)備
[self checkDeviceAuth];
// 數(shù)據(jù)保存路徑
self.documentDictionary = [(NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES)) objectAtIndex:0];
// 音頻編碼對(duì)象初始化
self.audioEncoder = [[AACEncoder alloc] init];
self.audioEncoder.delegate = self; ? ?// 設(shè)置代理
self.videoEncoder = [[JFVideoEncoder alloc] init]; // 視頻編碼對(duì)象初始化
self.videoEncoder.delegate = self; ? ? ? ? ? ? ? ? ? ? ? ? // 設(shè)置代理
_lock = dispatch_semaphore_create(1); // 當(dāng)并行執(zhí)行的處理更新數(shù)據(jù)時(shí),會(huì)產(chǎn)生數(shù)據(jù)不一致的情況,使用Serial Dipatch queue 進(jìn)行同步, 控制并發(fā)
}
// 檢查是否授權(quán)攝像頭的使用權(quán)限
- (void)checkDeviceAuth {
switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) {
case AVAuthorizationStatusAuthorized: ? // 已授權(quán)
NSLog(@"已授權(quán)");
[self initAVCaptureSession];
break;
case AVAuthorizationStatusNotDetermined: ? ?// 用戶尚未進(jìn)行允許或者拒絕,
{
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
if (granted) {
NSLog(@"已授權(quán)");
[self initAVCaptureSession];
} else {
NSLog(@"用戶拒絕授權(quán)攝像頭的使用, 返回上一頁, 請(qǐng)打開--> 設(shè)置 -- > 隱私 --> 通用等權(quán)限設(shè)置");
}
}];
}
break;
default:
{
NSLog(@"用戶尚未授權(quán)攝像頭的使用權(quán)");
}
break;
}
}
// 初始化 管理者
- (void)initAVCaptureSession {
self.session = [[AVCaptureSession alloc] init];
// 設(shè)置錄像的分辨率
// 先判斷是被是否支持要設(shè)置的分辨率
if ([self.session canSetSessionPreset:AVCaptureSessionPreset1280x720]) {
// 如果支持則設(shè)置
[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;
// 創(chuàng)建攝像頭類型數(shù)組
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
// 便利管理抓捕道德所有支持制定類型的 設(shè)備集合
for (AVCaptureDevice *device in devices) {
if (device.position == AVCaptureDevicePositionFront) {
self.videoDevice = device;
}
}
// 視頻
[self videoInputAndOutput];
// 音頻
[self audioInputAndOutput];
// 錄制的同時(shí)播放
[self initPreviewLayer];
// 提交配置
[self.session commitConfiguration];
}
// 視頻輸入輸出
- (void)videoInputAndOutput {
NSError *error;
// 視頻輸入
// 初始化 根據(jù)輸入設(shè)備來初始化輸出對(duì)象
self.videoInput = [[AVCaptureDeviceInput alloc] initWithDevice:self.videoDevice error:&error];
if (error) {
NSLog(@"-- 攝像頭出錯(cuò) -- %@", error);
return;
}
// 將輸入對(duì)象添加到管理者 -- AVCaptureSession 中
// 先判斷是否能搞添加輸入對(duì)象
if ([self.session canAddInput:self.videoInput]) {
// 管理者能夠添加 才可以添加
[self.session addInput:self.videoInput];
}
// 視頻輸出
// 初始化 輸出對(duì)象
self.videoOutput = [[AVCaptureVideoDataOutput alloc] init];
// 是否允許卡頓時(shí)丟幀
self.videoOutput.alwaysDiscardsLateVideoFrames = NO;
if ([self supportsFastTextureUpload])
{
// 是否支持全頻色彩編碼 YUV 一種色彩編碼方式, 即YCbCr, 現(xiàn)在視頻一般采用該顏色空間, 可以分離亮度跟色彩, 在不影響清晰度的情況下來壓縮視頻
BOOL supportsFullYUVRange = NO;
// 獲取輸出對(duì)象 支持的像素格式
NSArray *supportedPixelFormats = self.videoOutput.availableVideoCVPixelFormatTypes;
for (NSNumber *currentPixelFormat in supportedPixelFormats)
{
if ([currentPixelFormat intValue] == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)
{
supportsFullYUVRange = YES;
}
}
// 根據(jù)是否支持 來設(shè)置輸出對(duì)象的視頻像素壓縮格式,
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]];
}
// 設(shè)置代理
[self.videoOutput setSampleBufferDelegate:self queue:videoProcessingQueue];
// 判斷管理是否可以添加 輸出對(duì)象
if ([self.session canAddOutput:self.videoOutput]) {
[self.session addOutput:self.videoOutput];
AVCaptureConnection *connection = [self.videoOutput connectionWithMediaType:AVMediaTypeVideo];
// 設(shè)置視頻的方向
connection.videoOrientation = AVCaptureVideoOrientationPortrait;
// 視頻穩(wěn)定設(shè)置
if ([connection isVideoStabilizationSupported]) {
connection.preferredVideoStabilizationMode = AVCaptureVideoStabilizationModeAuto;
}
connection.videoScaleAndCropFactor = connection.videoMaxScaleAndCropFactor;
}
}
// 音頻輸入輸出
- (void)audioInputAndOutput {
NSError *jfError;
// 音頻輸入設(shè)備
self.audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
// 音頻輸入對(duì)象
self.audioInput = [[AVCaptureDeviceInput alloc] initWithDevice:self.audioDevice error:&jfError];
if (jfError) {
NSLog(@"-- 錄音設(shè)備出錯(cuò) -- %@", jfError);
}
// 將輸入對(duì)象添加到 管理者中
if ([self.session canAddInput:self.audioInput]) {
[self.session addInput:self.audioInput];
}
// 音頻輸出對(duì)象
self.audioOutput = [[AVCaptureAudioDataOutput alloc] init];
// 將輸出對(duì)象添加到管理者中
if ([self.session canAddOutput:self.audioOutput]) {
[self.session addOutput:self.audioOutput];
}
// 設(shè)置代理
[self.audioOutput setSampleBufferDelegate:self queue:audioProcessingQueue];
}
// 播放同時(shí)進(jìn)行播放
- (void)initPreviewLayer {
[self.view layoutIfNeeded];
// 初始化對(duì)象
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 返回上一級(jí)
- (IBAction)backAction:(id)sender {
// 結(jié)束直播
[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數(shù)據(jù)到文件
- (void) writeH264Data:(void*)data length:(size_t)length addStartCode:(BOOL)b
{
// 添加4字節(jié)的 h264 協(xié)議 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(@"準(zhǔn)備");
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(@"鏈接出錯(cuò)");
self.uploading = NO;
self.isFirstFrame = NO;
self.uploading = NO;
break;
default:
break;
}
}
// 獲取當(dāng)前時(shí)間戳
- (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;
}
`
// 注: 必須控制好線程, 不然很容易出現(xiàn)卡死或閃退的情況!
關(guān)于音視頻編解碼的代碼, 就不在這里展示了, 放在demo 中, 有需要的話話可以下載!
Github: https://github.com/jessonliu/JFLivePlaye
本地流媒體服務(wù)器的搭建這個(gè)給大家一個(gè)連接: http://www.lxweimin.com/p/8ea016b2720e
以上就是直播開發(fā)中所要設(shè)計(jì)道德知識(shí)點(diǎn)和一些第三方框架, 如果全是用第三方的話, 就會(huì)省事很多, 用起來也很方便, 但我個(gè)人比較喜歡刨根問題, 想了解原理!
如果寫的不妥當(dāng)或不足的地方, 希望大神指正和補(bǔ)充! 由于前段時(shí)間比較忙, 拉流, 解碼和播放還沒來得及寫, 我在直播的路上還有很長(zhǎng)的路要走, 還需要不斷地學(xué)習(xí)提高, 了解更底層的東西, 才能更好的掌握直播的整個(gè)流程技術(shù), 后期寫完會(huì)更新一個(gè)完整的Demo!