iOS-直播開發(fā)(開發(fā)從底層做起)

一直在忙, 也沒寫過幾次博客! 但一直熱衷于直播開發(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!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,316評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,481評(píng)論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,241評(píng)論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,939評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,697評(píng)論 6 409
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,182評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,406評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,933評(píng)論 1 334
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,772評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,973評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,209評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,638評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,866評(píng)論 1 285
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,644評(píng)論 3 391
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,953評(píng)論 2 373

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