iOS 直播專題4-音視頻編碼

現(xiàn)在的高清手機拍個照都有2M以上,按照人眼幀率24幀/秒的速度傳輸,網(wǎng)絡(luò)數(shù)度需要達到2 * 24M/秒,一般日常中的網(wǎng)絡(luò)顯然不可能有這樣的網(wǎng)速。這時就需要對音視頻進行編碼壓縮了。

常用的編碼類型有:

  • 視頻編碼:H.264、H.265、VP8、VP9
  • 音頻編碼:aac、Opus、mp3

生活中常說的mp4、avi、flv等指的是封裝格式,就是個容器,把音視頻、字幕、媒體信息等裝進容器里,編碼在這里充當?shù)氖菈嚎s音視頻的角色,這樣才能減少體積。

名詞介紹

YUV

視頻裸數(shù)據(jù)的一種格式,大部分設(shè)備的視頻幀數(shù)據(jù)都是YUV,“Y”表示明亮度(Luminance或Luma),也就是灰度值;而“U”和“V” 表示的則是色度(Chrominance或Chroma),作用是描述影像色彩及飽和度,用于指定像素的顏色。跟我們熟悉的RGB類似,YUV也是一種顏色編碼方法。

  • 主要用于電視系統(tǒng)以及模擬視頻領(lǐng)域,它將亮度信息(Y)與色彩信息(UV)分離,沒有UV信息一樣可以顯示完整的圖像,只不過是黑白的,這樣的設(shè)計很好地解決了彩色電視機與黑白電視的兼容問題。并且,YUV不像RGB那樣要求三個獨立的視頻信號同時傳輸,所以用YUV方式傳送占用極少的頻寬。

  • NV12和NV21屬于YUV420格式,是一種two-plane模式,蘋果iOS的視頻是NV12格式,Android的視頻是NV21格式

H.264

拍攝一段視頻,如果全身基本不動,只有嘴唇在動,如果記錄每一幀的畫面那么這段視頻會非常大,如果只記錄每個幀之間的變化,觀看視頻每幀的原始數(shù)據(jù)都可以在上一幀的基礎(chǔ)上加上這一幀的變化數(shù)據(jù)還原出來,不僅大幅度縮小體積,還能保持比較好的畫質(zhì),整個過程就是H.264的編碼、解碼
在H264協(xié)議里定義了三種幀:
I幀:完整編碼的幀,也叫關(guān)鍵幀
P幀:參考之前的I幀生成的只包含差異部分編碼的幀
B幀:參考前后的幀編碼的幀叫B幀

H264采用的核心算法是幀內(nèi)壓縮和幀間壓縮,幀內(nèi)壓縮是生成I幀的算法,幀間壓縮是生成B幀和P幀的算法。
H264碼流可以分為兩層:VCL層和NAL層
NAL:Network abstraction layer,叫網(wǎng)絡(luò)抽象層,它保存了H264相關(guān)的參數(shù)信息和圖像信息,NAL層由多個單元NALU組成,NALU由NALU頭(00 00 00 01或者00 00 01)、sps(序列參數(shù)集)、pps(圖像參數(shù)集合)、slice、sei、IDR幀、I幀(在圖像運動變化較少時,I幀后面是7個P幀,如果圖像運動變化大時,一個序列就短了,I幀后面可能是3個或者4個P幀)、P幀、B幀等數(shù)據(jù)。

H264原始碼流是由一個接一個的NALU(Nal Unit)組成的,NALU = 開始碼 + NAL類型 + 視頻數(shù)據(jù)
開始碼用于標示這是一個NALU 單元的開始,必須是"00 00 00 01" 或"00 00 01"
NALU類型如下:

類型 說明
0 未規(guī)定
1 非IDR圖像中不采用數(shù)據(jù)劃分的片段
2 非IDR圖像中A類數(shù)據(jù)劃分片段
3 非IDR圖像中B類數(shù)據(jù)劃分片段
4 非IDR圖像中C類數(shù)據(jù)劃分片段
5 IDR圖像的片段
6 補充增強信息(SEI)
7 序列參數(shù)集(SPS)
8 圖像參數(shù)集(PPS)
9 分割符
10 序列結(jié)束符
11 流結(jié)束符
12 填充數(shù)據(jù)
13 序列參數(shù)集擴展
14 帶前綴的NAL單元
15 子序列參數(shù)集
16-18 保留
19 不采用數(shù)據(jù)劃分的輔助編碼圖像片段
20 編碼片段擴展
21-23 保留
24-31 未規(guī)定

一般我們只用到了1、5、7、8這4個類型就夠了。類型為5表示這是一個I幀,I幀前面必須有SPS和PPS數(shù)據(jù),也就是類型為7和8,類型為1表示這是一個P幀或B幀。

GOP

Group of picture圖像組,也就是兩個I幀之間的距離,GOP值越大,那么I幀率之間P幀和B幀數(shù)量越多,圖像畫質(zhì)越精細,如果GOP是120,如果分辨率是720P,幀率是60,那么兩I幀的時間就是120/60=2s.

SPS

sequence parameter set,序列參數(shù)集(解碼相關(guān)信息,檔次級別、分別率、某檔次中編碼工具開關(guān)標識和涉及的參數(shù)、時域可分級信息等)

PPS

picture parameter set, 圖像參數(shù)集(一幅圖像所用的公共參數(shù),一幅圖像中所有SS應(yīng)用同一個PPS,初始圖像控制信息,初始化參數(shù)、分塊信息)

IDR

Instantaneous Decoding Refresh, 即時解碼刷新,跟I幀是同一個東西,在編碼和解碼中為了方便,要首個I幀和其他I幀區(qū)別開,所以才把第一個首個I幀叫IDR,這樣就方便控制編碼和解碼流程。

H.265

比H.264更加高效的編碼方式是H.265,是H.264的下一個版本,它屬于下一代MPEG-H標準而非MPEG-4,比H264有著更強的壓縮效率,被稱為HEVC(High Efficiency Video Coding/高效視頻編碼),同碼率下理論占用空間節(jié)省了50%足足一半,動態(tài)畫面表現(xiàn)會更加清晰。

  • 市面上,雖然已經(jīng)有一些新手機、電視、CPU都支持硬解H265了,但更多的現(xiàn)存老設(shè)備都不能向上支持硬解,軟解更吃不消。
  • H265的商業(yè)授權(quán)費太貴,市場都沒成熟呢,網(wǎng)絡(luò)視頻站點更不樂意花大錢推。但國內(nèi)IDC帶寬成本比較高,能降低一半的流量,無論對我們用戶,還是對運營商,都是很香的,相信再過幾年,H265就會很快普及了。

PCM

原始的未經(jīng)壓縮的音頻采樣數(shù)據(jù)裸流,它是由模擬信號經(jīng)過采樣、量化、編碼轉(zhuǎn)換成的標準數(shù)字音頻數(shù)據(jù)。
描述PCM數(shù)據(jù)的6個參數(shù):

  • Sample Rate : 采樣頻率。8kHz(電話)、44.1kHz(CD)、48kHz(DVD)。
  • Sample Size : 量化位數(shù)。通常該值為16-bit。
  • Number of Channels : 通道個數(shù)。常見的音頻有立體聲(stereo)和單聲道(mono)兩種類型,立體聲包含左聲道和右聲道。另外還有環(huán)繞立體聲等其它不太常用的類型。
  • Sign : 表示樣本數(shù)據(jù)是否是有符號位,比如用一字節(jié)表示的樣本數(shù)據(jù),有符號的話表示范圍為-128 ~ 127,無符號是0 ~ 255。
  • Byte Ordering : 字節(jié)序。字節(jié)序是little-endian還是big-endian。通常均為little-endian。字節(jié)序說明見第4節(jié)。
  • Integer Or Floating Point : 整形或浮點型。大多數(shù)格式的PCM樣本數(shù)據(jù)使用整形表示,而在一些對精度要求高的應(yīng)用方面,使用浮點類型表示PCM樣本數(shù)據(jù)。

AAC

全稱Advanced Audio Coding,高級音頻編碼,是一種專為聲音數(shù)據(jù)設(shè)計的文件壓縮格式。與MP3不同,它采用了全新的算法進行編碼,更加高效,具有更高的“性價比”。利用AAC格式,可使人感覺聲音質(zhì)量沒有明顯降低的前提下,更加小巧。

  • 優(yōu)點:相較于mp3,AAC格式的音質(zhì)更佳,文件更小。

  • 不足:AAC屬于有損壓縮的格式,與時下流行的APE、FLAC等無損格式相比音質(zhì)存在“本質(zhì)上”的差距。加之,傳輸速度更快的USB3.0和16G以上大容量MP3正在加速普及,也使得AAC頭上“小巧”的光環(huán)不復(fù)存在。

幀率

單位為fps(frame pre second),視頻畫面每秒有多少幀畫面,數(shù)值越大畫面越流暢

碼率

單位為bps(bit pre second),視頻每秒輸出的數(shù)據(jù)量,數(shù)值越大畫面越清晰

分辨率

視頻畫面像素密度,例如常見的720P、1080P等

關(guān)鍵幀間隔

每隔多久編碼一個關(guān)鍵幀

流程:


image.png

視頻編碼

分為軟編碼和音編碼方式

  • 軟編碼
    使用CPU進行編碼。
    H.264 初始化軟編碼:
- (void)initCompressionSession{
    _sendQueue = dispatch_queue_create("com.youku.laifeng.h264.sendframe", DISPATCH_QUEUE_SERIAL);
    [self initializeNALUnitStartCode];
    _lastPTS = kCMTimeInvalid;
    _timescale = 1000;
    frameCount = 0;
#ifdef DEBUG
    enabledWriteVideoFile = NO;
    [self initForFilePath];
#endif
    
    _encoder = [WSAVEncoder encoderForHeight:(int)_configuration.videoSize.height andWidth:(int)_configuration.videoSize.width bitrate:(int)_configuration.videoBitRate];
    [_encoder encodeWithBlock:^int(NSArray* dataArray, CMTimeValue ptsValue) {
        [self incomingVideoFrames:dataArray ptsValue:ptsValue];
        return 0;
    } onParams:^int(NSData *data) {
        [self generateSPSandPPS];
        return 0;
    }];
}

H.264開始軟編碼:

- (void)encodeVideoData:(CVPixelBufferRef)pixelBuffer timeStamp:(uint64_t)timeStamp {
  
    CVPixelBufferLockBaseAddress(pixelBuffer, 0);
    CMVideoFormatDescriptionRef videoInfo = NULL;
    CMVideoFormatDescriptionCreateForImageBuffer(NULL, pixelBuffer, &videoInfo);
    
    CMTime frameTime = CMTimeMake(timeStamp, 1000);
    CMTime duration = CMTimeMake(1, (int32_t)_configuration.videoFrameRate);
    CMSampleTimingInfo timing = {duration, frameTime, kCMTimeInvalid};
    
    CMSampleBufferRef sampleBuffer = NULL;
    CMSampleBufferCreateForImageBuffer(kCFAllocatorDefault, pixelBuffer, YES, NULL, NULL, videoInfo, &timing, &sampleBuffer);
    CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
    [_encoder encodeFrame:sampleBuffer];
    CFRelease(videoInfo);
    CFRelease(sampleBuffer);

    frameCount++;
}
  • 硬編碼
    不使用CPU進行編碼,使用顯卡(GPU)進行硬件加速,專用的DSP、FPGA、ASIC芯片等硬件進行編碼。
    iOS8.0以上才支持硬編碼
    H.264初始化硬編碼:
    - (void)resetCompressionSession {
    if (compressionSession) {
        VTCompressionSessionCompleteFrames(compressionSession, kCMTimeInvalid);

        VTCompressionSessionInvalidate(compressionSession);
        CFRelease(compressionSession);
        compressionSession = NULL;
    }

    // allocator  分配器,設(shè)置為默認分配
    // width 寬
    // height 高
    // encoderSpecification 編碼規(guī)范,設(shè)置nil由videoToolbox自己選擇
    // imageBufferAttributes 源像素緩沖區(qū)屬性.設(shè)置nil不讓videToolbox創(chuàng)建,而自己創(chuàng)建
    // compressedDataAllocator 壓縮數(shù)據(jù)分配器.設(shè)置nil,默認的分配
    // outputCallback 編碼回調(diào)
    // refcon 回調(diào)客戶定義的參考值,此處把self傳過去,因為我們需要在C函數(shù)中調(diào)用self的方法,而C函數(shù)無法直接調(diào)self
    // compressionSessionOut 編碼會話
    OSStatus status = VTCompressionSessionCreate(NULL, _configuration.videoSize.width, _configuration.videoSize.height, kCMVideoCodecType_H264, NULL, NULL, NULL, VideoCompressonOutputCallback, (__bridge void *)self, &compressionSession);
    if (status != noErr) {
        return;
    }

    _currentVideoBitRate = _configuration.videoBitRate;
    /// 設(shè)置關(guān)鍵幀(GOPsize)間隔,GOP太小的話圖像會模糊
    VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, (__bridge CFTypeRef)@(_configuration.videoMaxKeyframeInterval));
    /// 最大幀率間隔
    VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration, (__bridge CFTypeRef)@(_configuration.videoMaxKeyframeInterval/_configuration.videoFrameRate));
    /// 設(shè)置期望幀率,不是實際幀率
    VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_ExpectedFrameRate, (__bridge CFTypeRef)@(_configuration.videoFrameRate));
    /// 碼率,碼率大了話就會非常清晰,但同時文件也會比較大。碼率小的話,圖像有時會模糊,但也勉強能看
    VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_AverageBitRate, (__bridge CFTypeRef)@(_configuration.videoBitRate));
    /// 數(shù)據(jù)率限制
    NSArray *limit = @[@(_configuration.videoBitRate * 1.5/8), @(1)];
    VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef)limit);
    /// 設(shè)置實時編碼輸出(避免延遲)
    VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
    VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Main_AutoLevel);
    /// 是否產(chǎn)生B幀(因為B幀在解碼時并不是必要的,是可以拋棄B幀的)
    VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_AllowFrameReordering, kCFBooleanTrue);
    VTSessionSetProperty(compressionSession, kVTCompressionPropertyKey_H264EntropyMode, kVTH264EntropyMode_CABAC);
    VTCompressionSessionPrepareToEncodeFrames(compressionSession);

}

H.264開始編碼:

- (void)encodeVideoData:(CVPixelBufferRef)pixelBuffer timeStamp:(uint64_t)timeStamp {
    if(_isBackGround) return;
    frameCount++;
    CMTime presentationTimeStamp = CMTimeMake(frameCount, (int32_t)_configuration.videoFrameRate);
    VTEncodeInfoFlags flags;
    CMTime duration = CMTimeMake(1, (int32_t)_configuration.videoFrameRate);

    NSDictionary *properties = nil;
    if (frameCount % (int32_t)_configuration.videoMaxKeyframeInterval == 0) {
        properties = @{(__bridge NSString *)kVTEncodeFrameOptionKey_ForceKeyFrame: @YES};
    }
    NSNumber *timeNumber = @(timeStamp);

    OSStatus status = VTCompressionSessionEncodeFrame(compressionSession, pixelBuffer, presentationTimeStamp, duration, (__bridge CFDictionaryRef)properties, (__bridge_retained void *)timeNumber, &flags);
    if(status != noErr){
        [self resetCompressionSession];
    }
}

H.265敬請期待。。。。。。。。!

音頻編碼

AAC編碼:

- (void)encodeAudioData:(nullable NSData*)audioData timeStamp:(uint64_t)timeStamp {
    if (![self createAudioConvert]) {
        return;
    }
    
    if(leftLength + audioData.length >= self.configuration.bufferLength){
        /// 發(fā)送
        NSInteger totalSize = leftLength + audioData.length;
        NSInteger encodeCount = totalSize/self.configuration.bufferLength;
        char *totalBuf = malloc(totalSize);
        char *p = totalBuf;
        
        memset(totalBuf, (int)totalSize, 0);
        memcpy(totalBuf, leftBuf, leftLength);
        memcpy(totalBuf + leftLength, audioData.bytes, audioData.length);
        
        for(NSInteger index = 0;index < encodeCount;index++){
            [self encodeBuffer:p  timeStamp:timeStamp];
            p += self.configuration.bufferLength;
        }
        
        leftLength = totalSize%self.configuration.bufferLength;
        memset(leftBuf, 0, self.configuration.bufferLength);
        memcpy(leftBuf, totalBuf + (totalSize -leftLength), leftLength);
        
        free(totalBuf);
        
    }else{
        /// 積累
        memcpy(leftBuf+leftLength, audioData.bytes, audioData.length);
        leftLength = leftLength + audioData.length;
    }
}

- (void)encodeBuffer:(char*)buf timeStamp:(uint64_t)timeStamp{
    
    AudioBuffer inBuffer;
    inBuffer.mNumberChannels = 1;
    inBuffer.mData = buf;
    inBuffer.mDataByteSize = (UInt32)self.configuration.bufferLength;
    
    AudioBufferList buffers;
    buffers.mNumberBuffers = 1;
    buffers.mBuffers[0] = inBuffer;
    
    
    // 初始化編碼后輸出緩沖列表
    AudioBufferList outBufferList;
    outBufferList.mNumberBuffers = 1;
    outBufferList.mBuffers[0].mNumberChannels = inBuffer.mNumberChannels;
    outBufferList.mBuffers[0].mDataByteSize = inBuffer.mDataByteSize;   // 設(shè)置緩沖區(qū)大小
    outBufferList.mBuffers[0].mData = aacBuf;           // 設(shè)置AAC緩沖區(qū)
    UInt32 outputDataPacketSize = 1;
    if (AudioConverterFillComplexBuffer(m_converter, inputDataProc, &buffers, &outputDataPacketSize, &outBufferList, NULL) != noErr) {
        return;
    }
    
    WSAudioFrame *audioFrame = [WSAudioFrame new];
    audioFrame.timestamp = timeStamp;
    audioFrame.data = [NSData dataWithBytes:aacBuf length:outBufferList.mBuffers[0].mDataByteSize];
    
    char exeData[2];
    exeData[0] = _configuration.asc[0];
    exeData[1] = _configuration.asc[1];
    audioFrame.audioInfo = [NSData dataWithBytes:exeData length:2];
    if (self.aacDeleage && [self.aacDeleage respondsToSelector:@selector(audioEncoder:audioFrame:)]) {
        [self.aacDeleage audioEncoder:self audioFrame:audioFrame];
    }
    
    if (self->enabledWriteVideoFile) {
        NSData *adts = [self adtsData:_configuration.numberOfChannels rawDataLength:audioFrame.data.length];
        fwrite(adts.bytes, 1, adts.length, self->fp);
        fwrite(audioFrame.data.bytes, 1, audioFrame.data.length, self->fp);
    }
    
}

項目源碼下載

還有 4% 的精彩內(nèi)容
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
支付 ¥1.00 繼續(xù)閱讀

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容