android 硬编解码
2016-10-13 20:11:53 sz66cm 阅读数 2226

Android 采集过程注意

  1. Camera.addCallbackBuffer(byte[] data)其中data的大小要紧密与采集数据的格式相关.
  2. 如采集到的数据格式yuv422i,那么data的大小应该为width * height * 2.
  3. Camera.setPreviewCallback(Camera.PreviewCallback cb)
  4. 每次调用onPreviewFrame(...)的末尾在添加一次Camera.addCallbackBuffer(byte[] data)

Android H264硬编码过程

  1. 生成编码器并且设置相关参数
    public synchronized void open() {
        //YUV420P的大小关系
        byte[] yuv420 = new byte[mWidth * mHeight * 3 / 2];
        //生成编码器并且设置相关参数
        mediaCodec = MediaCodec.createEncoderByType("video/avc");
        //编码格式参数设置,如果对yuv数据进行旋转以后,注意mWidth,
        //mHeight的在90度或270度会颠倒,不设置正确的话对端会花屏
        MediaFormat mediaFormat = MediaFormat.createVideoFormat(
            "video/avc", mWidth, mHeight);
        //设置码率,码率越低,失真越厉害
        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
        /**设置编码输入缓存大小,默认输入数据为yuv420大小,即1.5倍的宽高积,
        如果使用的是其他格式,如yuv422那么就要手动设置大小,不然塞数据时
        会报BufferOverflowException异常*/
        mediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE,
                 mWidth * mHeight * 3 / 2);
        //设置帧率
        mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, framerate);
        /**设置颜色格式(I420,YV12,NV21等)
            如:MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar*/
        mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);
        //设置发送I帧的时间间隔
        mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);//单位:s(秒)
        //完成配置,启动
        mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAT_ENCODE);
        mediaCodec.start();
    }
  1. 进行编码
    public synchronized int encode(byte[] in, int offset, byte[] out, int length) {
        int pos = 0;
        byte[] inBuf = in;
        int l = length;
        /**由于Android 摄像头默认采集的数据是NV21格式,所以要
           转成MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar
           让编码器支持.*/
        NV21toYUV420SemiPlannr(in, offset, yuv420, mWidth, mHeight);
        try {
            /**
                    注意此处获取inputBuffer和outputBuffer的方法,在android LOLLIPOP之后
                    的版本要修改inputBuffer = MediaCodec.getInputBuffer(index);
            */
            ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();
            ByteBuffer[] outputBuffers = mediaCodec.getOutputBuffers();
            /**
                解码的时候,如果此处TIME_OUT非0会有个大坑,很多机子在这句卡死
            */
            int inputBufferIndex = mediaCodec.dequeueInputBuffer(TIME_OUT);
            if(inputBufferIndex >= 0){
                ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
                inputBuffer.clear();
                inputBuffer.put(inBuf, offset, l);
                /**此处getMyTime()函数维护一个递增的时间戳
                    据说此处的第四个参数不传,第一个I帧以后,
                    mediaCodec.dequeueOutputBuffer()一直返回-1,
                    -1对应原生代码的再试一遍的意思*/
                mediaCodec.queueInputBuffer(inputBufferIndex, 0, l, getMyTime(), 0);
            }
            MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
            /**
                此处的TIME_OUT是否要传,待研究.
             */
            int outBufferIndex = mediaCodec.dequeueInputBuffer(bufferInfo, TIME_OUT);
            //待理解继续
        }
    }

(待补充)

Android H264硬解码过程

  1. 生成MediaCodec对象并且设置好参数
    //H264解码器
    codec = MediaCodec.createDecoderByType("video/avc");
    MeidaFormat mediaFormat = MediaFormat.createVideoFormat(
        "video/avc", width, height);
    mediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, width * height);
    codec.configure(mediaFormat, surface, null, 0);
    codec.start();
  1. 进行解码并且播放
    public void decodeAndPlayBack(byte[] in, int offset, int length) {
        //获取喂数据的ByteBuffer数组
        ByteBuffer[] inputBuffers = codec.getInputBuffers();
        /**以下特别注意,TIME_OUT建议设置成0,设置非0很多机子
            出现卡死在此句代码,设置成0的代价只是丢帧*/
        int inputBuffersIndex = codec.dequeueInputBuffer(TIME_OUT);
        if(inputBuffersIndex >= 0) {
            ByteBuffer inputBuffer = inputBuffers[inputBuffersIndex];
            inputBuffer.clear();
            inputBuffer.put(in, offset, length);
            //填充好数据以后,提交通知解码器解码,这几个参数待研究
            codec.queueInputBuffer(inputBuffersIndex, 0, length, 0, 0);
        }
        //释放缓存空间
        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
        int outputBufferIndex = codec.dequeueOutputBuffer(bufferInfo, 0);
        while (outputBufferIndex >=0) {
            codec.releaseOutputBuffer(outputBufferIndex, true);
            outputBufferIndex = codec.dequeueOutputBuffer(bufferInfo, 0);
        }
    }

android 硬编解码 相关内容

2019-05-22 18:36:00 weixin_34150503 阅读数 8

VideoToolbox中的对象:

1)CVPixelBuffer

编码前和解码后的图像数据结构(未压缩光栅图像缓存区-Uncompressed Raster Image Buffer)

2)CVPixelBufferPool

存放CVPixelBuffer

3)pixelBufferAttributes

CFDictionary对象,可能包含了视频的宽高,像素格式类型(32RGBA, YCbCr420),是否可以用于OpenGL ES等相关信息

4)CMTime

时间戳相关。时间以 64-big/32-bit形式出现。 分子是64-bit的时间值,分母是32-bit的时标(time scale)

5)CMClock

时间戳相关。时间以 64-big/32-bit形式出现。 分子是64-bit的时间值,分母是32-bit的时标(time scale)。它封装了时间源,其中CMClockGetHostTimeClock()封装了mach_absolute_time()

6)CMTimebase

时间戳相关。时间以 64-big/32-bit形式出现。CMClock上的控制视图。提供了时间的映射:CMTimebaseSetTime(timebase, kCMTimeZero); 速率控制:

CMTimebaseSetRate(timebase, 1.0);

7)CMBlockBuffer

编码后,结果图像的数据结构

8)CMVideoFormatDescription

编解码前后的视频图像均封装在CMSampleBuffer中,如果是编码后的图像,以CMBlockBuffe方式存储;解码后的图像,以CVPixelBuffer存储。

9)CMSampleBuffer

存放编解码前后的视频图像的容器数据结构。如图所示,编解码前后的视频图像均封装在CMSampleBuffer中,如果是编码后的图像,以CMBlockBuffer方式存储;解码后的图像,以CVPixelBuffer存储。CMSampleBuffer里面还有另外的时间信息CMTime和视频描述信息CMVideoFormatDesc。


3571697-36975c629e77b0e2.png
1

1.硬解码

1.1 将 H.264码流转换为 CMSampleBuffer

CMSampleBuffer = CMTime + FormatDesc + CMBlockBuffer

需要从H.264的码流里面提取出以上的三个信息。最后组合成CMSampleBuffer,提供给硬解码接口来进行解码工作。

经过处理之后,在Format Description中则是:

3571697-df4a9781efd34056.png
2

需要注意的是:

要从基础的流数据将SPS和PPS转化为Format Desc中的话,需要调用CMVideoFormatDescriptionCreateFromH264ParameterSets()方法。

最后用VTDecompression来解码

使用VTCompressionSession进行硬编码

android 硬编解码 相关内容

2018-12-04 09:09:00 weixin_33724059 阅读数 3

介绍

iOS 8.0 之后,苹果开放了硬解码和硬解码的API。VideoToolbox 是一套纯C语言API。其中包含了很多C语言函数; VideoToolbox是一个低级框架,可直接访问硬件编码器和解码器。它提供视频压缩和解压缩服务,本文主要针对H.264硬编码来进行编解码说明.关于H.264相关知识请参考H.264介绍&编码原理本文不做过多解释!

编码

  • 硬解码:用GPU来解码,减少CPU运算
    • 优点:播放流畅、低功耗,解码速度快
    • 缺点:兼容不好
  • 软解码:用CPU来解码,比如(ffmpeg)
    • 优点:兼容好
    • 缺点:加大CPU负担,耗电增加、没有硬解码流畅,解码速度相对慢

VideoToolbox编码:

1. 首先需要导入#import <VideoToolbox/VideoToolbox.h>

2. 初始化编码会话

@property (nonatomic, assign) VTCompressionSessionRef compressionSession;

// 初始化编码器
- (void)setupVideoSession {
    
    // 1.用于记录当前是第几帧数据
    self.frameID = 0;
    
    // 2.录制视频的宽度&高度,根据实际需求修改
    int width = 720;
    int height = 1280;
    
    // 3.创建CompressionSession对象,该对象用于对画面进行编码
    OSStatus status = VTCompressionSessionCreate(NULL,     // 会话的分配器。传递NULL以使用默认分配器。
                                                 width,    // 帧的宽度,以像素为单位。
                                                 height,   // 帧的高度,以像素为单位。
                                                 kCMVideoCodecType_H264,   // 编解码器的类型,表示使用h.264进行编码
                                                 NULL,   // 指定必须使用的特定视频编码器。传递NULL让视频工具箱选择编码器。
                                                 NULL,   // 源像素缓冲区所需的属性,用于创建像素缓冲池。如果不希望视频工具箱为您创建一个,请传递NULL
                                                 NULL,   // 压缩数据的分配器。传递NULL以使用默认分配器。
                                                 didCompressH264,          // 当一次编码结束会在该函数进行回调,可以在该函数中将数据,写入文件中
                                                 (__bridge void *)(self),  // outputCallbackRefCon
                                                 &_compressionSession);    // 指向一个变量以接收的压缩会话。
    if (status != 0){
        NSLog(@"H264: session 创建失败");
        return ;
    }
    
    // 4.设置实时编码输出(直播必然是实时输出,否则会有延迟)
    VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
    VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel);
    
    // 5.设置关键帧(GOPsize)间隔
    int frameInterval = 60;
    CFNumberRef  frameIntervalRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &frameInterval);
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, frameIntervalRef);
    
    // 6.设置期望帧率(每秒多少帧,如果帧率过低,会造成画面卡顿)
    int fps = 24;
    CFNumberRef fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &fps);
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);
    
    // 7.设置码率(码率: 编码效率, 码率越高,则画面越清晰, 如果码率较低会引起马赛克 --> 码率高有利于还原原始画面,但是也不利于传输)
    int bitRate = width * height * 3 * 4 * 8;
    CFNumberRef bitRateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRate);
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_AverageBitRate, bitRateRef);
    
    // 8.设置码率,均值,单位是byte 这是一个算法
    NSArray *limit = @[@(bitRate * 1.5/8), @(1)];
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef)limit);
    
    // 9.基本设置结束, 准备进行编码
    VTCompressionSessionPrepareToEncodeFrames(_compressionSession);
}

3. 编码完成回调函数

// 编码完成回调
void didCompressH264(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer) {
    
    // 1.判断状态是否等于没有错误
    if (status != noErr) {
        return;
    }
    if (!CMSampleBufferDataIsReady(sampleBuffer)) {
        NSLog(@"didCompressH264 data is not ready ");
        return;
    }
    
    // 2.根据传入的参数获取对象
    VideoH264EnCode* encoder = (__bridge VideoH264EnCode*)outputCallbackRefCon;
    
    // 3.判断是否是关键帧
    bool isKeyframe = !CFDictionaryContainsKey( (CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0)), kCMSampleAttachmentKey_NotSync);
    
    // 判断当前帧是否为关键帧
    // 获取sps & pps数据
    if (isKeyframe)
    {
        // 获取编码后的信息(存储于CMFormatDescriptionRef中)
        CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
        
        // 获取SPS信息
        size_t sparameterSetSize, sparameterSetCount;
        const uint8_t *sparameterSet;
        CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0 );
        
        // 获取PPS信息
        size_t pparameterSetSize, pparameterSetCount;
        const uint8_t *pparameterSet;
        CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0 );
        
        // 装sps/pps转成NSData
        NSData *sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize];
        NSData *pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize];
        
        // 写入文件
        [encoder gotSpsPps:sps pps:pps];
    }
    
    // 获取数据块
    CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
    size_t length, totalLength;
    char *dataPointer;
    OSStatus statusCodeRet = CMBlockBufferGetDataPointer(dataBuffer, 0, &length, &totalLength, &dataPointer);
    if (statusCodeRet == noErr) {
        size_t bufferOffset = 0;
        static const int AVCCHeaderLength = 4; // 返回的nalu数据前四个字节不是0001的startcode,而是大端模式的帧长度length
        
        // 循环获取nalu数据
        while (bufferOffset < totalLength - AVCCHeaderLength) {
            uint32_t NALUnitLength = 0;
            // Read the NAL unit length
            memcpy(&NALUnitLength, dataPointer + bufferOffset, AVCCHeaderLength);
            
            // 从大端转系统端
            NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);
            
            NSData* data = [[NSData alloc] initWithBytes:(dataPointer + bufferOffset + AVCCHeaderLength) length:NALUnitLength];
            [encoder gotEncodedData:data isKeyFrame:isKeyframe];
            
            // 移动到写一个块,转成NALU单元
            // Move to the next NAL unit in the block buffer
            bufferOffset += AVCCHeaderLength + NALUnitLength;
        }
    }
}

4. 获取SPS/PPS,以及I,P,B 帧数据,并将其通过 block 回调

// 获取 sps 以及 pps,并进行StartCode
- (void)gotSpsPps:(NSData*)sps pps:(NSData*)pps{
    
    // 拼接NALU的 StartCode,默认规定使用 00000001
    const char bytes[] = "\x00\x00\x00\x01";
    size_t length = (sizeof bytes) - 1;
    NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];

    NSMutableData *h264Data = [[NSMutableData alloc] init];
    [h264Data appendData:ByteHeader];
    [h264Data appendData:sps];
    if (self.h264DataBlock) {
        self.h264DataBlock(h264Data);
    }
    
    [h264Data resetBytesInRange:NSMakeRange(0, [h264Data length])];
    [h264Data setLength:0];
    [h264Data appendData:ByteHeader];
    [h264Data appendData:pps];
    if (self.h264DataBlock) {
        self.h264DataBlock(h264Data);
    }
}

- (void)gotEncodedData:(NSData*)data isKeyFrame:(BOOL)isKeyFrame{
    
    const char bytes[] = "\x00\x00\x00\x01";
    size_t length = (sizeof bytes) - 1;     //string literals have implicit trailing '\0'
    NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
    
    NSMutableData *h264Data = [[NSMutableData alloc] init];
    [h264Data appendData:ByteHeader];
    [h264Data appendData:data];
    if (self.h264DataBlock) {
        self.h264DataBlock(h264Data);
    }
}

5. 通过传入原始帧数据进行调用并回调

// 将 sampleBuffer(摄像头捕捉数据,原始帧数据) 编码为H.264
- (void)encodeSampleBuffer:(CMSampleBufferRef)sampleBuffer H264DataBlock:(void (^)(NSData * _Nonnull))h264DataBlock{
    
    if (!self.compressionSession) {
        return;
    }
    //  1.保存 block 块
    self.h264DataBlock = h264DataBlock;
    
    //  2.将sampleBuffer转成imageBuffer
    CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
    
    //  3.根据当前的帧数,创建CMTime的时间
    CMTime presentationTimeStamp = CMTimeMake(self.frameID++, 1000);
    VTEncodeInfoFlags flags;
    
    //  4.开始编码该帧数据
    OSStatus statusCode = VTCompressionSessionEncodeFrame(
                                                          self.compressionSession,
                                                          imageBuffer,
                                                          presentationTimeStamp,
                                                          kCMTimeInvalid,
                                                          NULL,
                                                          (__bridge void * _Nullable)(self),
                                                          &flags
                                                          );
    
    if (statusCode != noErr) {
        NSLog(@"H264: VTCompressionSessionEncodeFrame failed with %d", (int)statusCode);
        VTCompressionSessionInvalidate(self.compressionSession);
        CFRelease(self.compressionSession);
        self.compressionSession = NULL;
        return;
    }
}

通过上述几个方法,可以完成原始帧数据的H.264编码工作,并将其回调给调用者, 具体如何拼接以及使用,根据自身项目需求来进行使用

VideoToolbox解码:

解码与编码正好相反,在拿到H.264的每一帧数据后进行解码操作,最后获取 原始帧数据进行展示

1. 初始化解码器

- (BOOL)initH264Decoder {
    if(_deocderSession) {
        return YES;
    }
    const uint8_t* const parameterSetPointers[2] = { _sps, _pps };
    const size_t parameterSetSizes[2] = { _spsSize, _ppsSize };
    OSStatus status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault,
                                                                          2, //param count
                                                                          parameterSetPointers,
                                                                          parameterSetSizes,
                                                                          4, //nal start code size
                                                                          &_decoderFormatDescription);
    
    if(status == noErr) {
        NSDictionary* destinationPixelBufferAttributes = @{
                                                           (id)kCVPixelBufferPixelFormatTypeKey : [NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange], //硬解必须是 kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange 或者是kCVPixelFormatType_420YpCbCr8Planar
                                                           //这里款高和编码反的
                                                           (id)kCVPixelBufferOpenGLCompatibilityKey : [NSNumber numberWithBool:YES]
                                                           };
        
        
        VTDecompressionOutputCallbackRecord callBackRecord;
        callBackRecord.decompressionOutputCallback = didDecompress;
        callBackRecord.decompressionOutputRefCon = (__bridge void *)self;
        status = VTDecompressionSessionCreate(kCFAllocatorDefault,
                                              _decoderFormatDescription,
                                              NULL,
                                              (__bridge CFDictionaryRef)destinationPixelBufferAttributes,
                                              &callBackRecord,
                                              &_deocderSession);
        VTSessionSetProperty(_deocderSession, kVTDecompressionPropertyKey_ThreadCount, (__bridge CFTypeRef)[NSNumber numberWithInt:1]);
        VTSessionSetProperty(_deocderSession, kVTDecompressionPropertyKey_RealTime, kCFBooleanTrue);
    } else {
        NSLog(@"IOS8VT: reset decoder session failed status=%d", (int)status);
    }
    
    return YES;
}

2. 解码操作

// 解码操作,外部调用
- (void)decodeNalu:(uint8_t *)frame size:(uint32_t) frameSize{
    
    int nalu_type = (frame[4] & 0x1F);
    CVPixelBufferRef pixelBuffer = NULL;
    uint32_t nalSize = (uint32_t)(frameSize - 4);
    uint8_t *pNalSize = (uint8_t*)(&nalSize);
    frame[0] = *(pNalSize + 3);
    frame[1] = *(pNalSize + 2);
    frame[2] = *(pNalSize + 1);
    frame[3] = *(pNalSize);
    
    //传输的时候。关键帧不能丢数据 否则绿屏   B/P可以丢  这样会卡顿
    switch (nalu_type)
    {
        case 0x05:
            //  关键帧
            if([self initH264Decoder])
            {
                pixelBuffer = [self decode:frame withSize:frameSize];
            }
            break;
        case 0x07:
            //  sps
            _spsSize = frameSize - 4;
            _sps = malloc(_spsSize);
            memcpy(_sps, &frame[4], _spsSize);
            break;
        case 0x08:
        {
            //  pps
            _ppsSize = frameSize - 4;
            _pps = malloc(_ppsSize);
            memcpy(_pps, &frame[4], _ppsSize);
            break;
        }
        default:
        {
            //  B/P其他帧
            if([self initH264Decoder]){
                pixelBuffer = [self decode:frame withSize:frameSize];
            }
            break;
        }
    }
}


- (CVPixelBufferRef)decode:(uint8_t *)frame withSize:(uint32_t)frameSize{
    CVPixelBufferRef outputPixelBuffer = NULL;
    
    CMBlockBufferRef blockBuffer = NULL;
    OSStatus status  = CMBlockBufferCreateWithMemoryBlock(NULL,
                                                          (void *)frame,
                                                          frameSize,
                                                          kCFAllocatorNull,
                                                          NULL,
                                                          0,
                                                          frameSize,
                                                          FALSE,
                                                          &blockBuffer);
    if(status == kCMBlockBufferNoErr) {
        CMSampleBufferRef sampleBuffer = NULL;
        const size_t sampleSizeArray[] = {frameSize};
        status = CMSampleBufferCreateReady(kCFAllocatorDefault,
                                           blockBuffer,
                                           _decoderFormatDescription ,
                                           1, 0, NULL, 1, sampleSizeArray,
                                           &sampleBuffer);
        if (status == kCMBlockBufferNoErr && sampleBuffer) {
            VTDecodeFrameFlags flags = 0;
            VTDecodeInfoFlags flagOut = 0;
            OSStatus decodeStatus = VTDecompressionSessionDecodeFrame(_deocderSession,
                                                                      sampleBuffer,
                                                                      flags,
                                                                      &outputPixelBuffer,
                                                                      &flagOut);
            
            if(decodeStatus == kVTInvalidSessionErr) {
                NSLog(@"IOS8VT: Invalid session, reset decoder session");
            } else if(decodeStatus == kVTVideoDecoderBadDataErr) {
                NSLog(@"IOS8VT: decode failed status=%d(Bad data)", (int)decodeStatus);
            } else if(decodeStatus != noErr) {
                NSLog(@"IOS8VT: decode failed status=%d", (int)decodeStatus);
            }
            CFRelease(sampleBuffer);
        }
        CFRelease(blockBuffer);
    }
    return outputPixelBuffer;
}

3. 解码完成回调

// 解码回调函数
static void didDecompress( void *decompressionOutputRefCon, void *sourceFrameRefCon, OSStatus status, VTDecodeInfoFlags infoFlags, CVImageBufferRef pixelBuffer, CMTime presentationTimeStamp, CMTime presentationDuration ){
    CVPixelBufferRef *outputPixelBuffer = (CVPixelBufferRef *)sourceFrameRefCon;
    *outputPixelBuffer = CVPixelBufferRetain(pixelBuffer);
    VideoH264Decoder *decoder = (__bridge VideoH264Decoder *)decompressionOutputRefCon;
    
    if ([decoder.delegate respondsToSelector:@selector(decoder:didDecodingFrame:)]) {
        [decoder.delegate decoder: decoder didDecodingFrame:pixelBuffer];
    }
}

4. 通过 OpenGL 进行 帧数据展示

代码就不贴出来了,可以通过 demo 进行查看.

本文主要通过 VideoToolbox 对 iPhone 手机摄像头拍摄的视频流进行 编码和解码,并进行展示,仅仅提供了基本的编解码功能,具体在项目中如何使用还要根据自身项目来定,关于视频流传输,可以参考 Socket & CocoaAsyncSocket介绍与使用,以及如何处理粘包等问题;

Demo 地址: https://github.com/liuchuan-alex/VideoToolBox

更详细使用请查看: VideoToolbox官方说明文档

android 硬编解码 相关内容

2019-01-15 16:48:00 weixin_34291004 阅读数 4

官方文档 https://developer.android.google.cn/reference/android/media/MediaCodec

MediaCodec 是做硬件(GPU,充分利用GPU 的并行处理能力)编解码的。(通常结合 MediaExtractor、MediaSync、MediaMuxer、MediaCrypto、MediaDrm、Image、Surface、AudioTrack 使用)

codec (即 encoder + decoder ) :编解码器
MediaExtractor:解封装
MediaMuxer: 混合器(封装)(音视频合成)
MediaSync: 音视频同步
MediaCrypto: 加密

原始音频数据 (PCM )和 视频帧压缩编码(aac + h.264 等 )后要封装到一个容器(MP4 等)中进行传播。同理播放器播放前要先解封装,取出里面的音频部分和视频部分,然后解码成硬件可以直接播放和渲染的音频流和视频流。由于音频流和视频流是分别播放和渲染的,所以这里就有音视频同步的问题。

356361-88f9733c632b2786.png
jiagou.png

codec 处理输入数据产生输出数据。它通过输入缓冲集合和输出缓冲集合异步的处理数据。先请求一个空的 input buffer,然后填充上要处理的数据发送给 codec 处理。 codec 处理数据后会把结果写到一个空的 output buffer 中。最后你请求 output buffer 从里面读出处理后的数据就行了。output buffer 用完后释放回 codec 重新使用。

数据类型

codec 处理3种类型的数据, compressed data (待解码的数据 或 编码后的数据)、raw audio data (待编码或解码后的数据)和 raw video data (待编码或解码后的数据)。3种数据类型都可以用 ByteBuffers 处理。还可以用 Surface 来处理 raw video data 来提高性能。因为 Surface 可以直接使用 native video buffers (在 native 层分配的 buffer)而不需要映射或拷贝到 ByteBuffers (ByteBuffers 是分配在 JVM 堆中的缓冲区) 中。

状态

356361-6f45df58f7d1958f.png
state.png

概念上主要包含 Stopped、Executing、Released 3种状态。Stopped 包含 Configured、Uninitialized、Error 3个子状态。 Executing 包含 Flushed、Running、End of Stream 3个子状态。

codec 实例化以后默认是 Uninitialized 状态,之后需要调用 configure 进入 Configured 状态,再调用 start 进入 Executing 状态。运行状态默认是 Flushed, 这时可以调用 dequeueInputBuffer 拿到一个 input buffer 开始处理数据,进入 Running 状态。当没有输入后需要写一个 end-of-stream marker 的标志(可以放在最后一个 Input buffer 中,也可以用一个单独的空 buffer,空 buffer 的 timestamp 会被忽略)。当 End of stream 后 codec 就不再接受输入了, 但仍然继续产生输出直到输出 buffer 遇到 end-of-stream marker 标志(这个标志是 codec 写的,前提是当没有输入时你必须给 input buffer 写一个 end-of-stream 的标志。这个标志可以作为 codec 处理完毕的标志)。在运行状态可以调用 flush() 方法回到 Flushed 状态。

运行时调用 stop 方法会重新回到 Uninitialized ,这时要重新 configue 、start 才能重新运行。 运行出错时会进入 Error 状态,这时可以调用 reset 方法恢复到 Uninitialized。 codec 不再使用时调用 release 方法释放资源进入 Released 状态。

创建

5.0 之后官方推荐用 MediaCodecList.findDecoderForFormat 传入一个 MediaFormat 来查找你要使用的 codec。 然后调用 MediaCodec.createByCodecName(String) 方法创建 codec。

MediaFormat mediaFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, 44100, 1);
mediaFormat.setString(MediaFormat.KEY_BIT_RATE, null);
MediaCodecList mediaCodecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
String name = mediaCodecList.findEncoderForFormat(mediaFormat);
Log.d(TAG, "name is " + name); // OMX.google.aac.encoder
try {
    mediaCodec = MediaCodec.createByCodecName(name);
} catch (IOException e) {
    e.printStackTrace();
}

也可以调用 MediaCodec.createDecoder/EncoderByType(String) 传入要处理数据的 MIME type 来创建。

try {
    // 5.0 之前可以这样写,aac 编解码一般都支持
    mediaCodec = MediaCodec.createEncoderByType("audio/mp4a-latm");
} catch (IOException e) {
    e.printStackTrace();
}

初始化

调用 configue 方法,如果需要异步处理 buffer 可以先调用 setCallback 方法设置回调。

数据处理

输入输出 buffer 是用 buffer-ID 来标识的。调用 start 后通过 dequeueInput/OutputBuffer(…) 方法拿到一个 buffer。异步模式需要在 MediaCodec.Callback.onInput/OutputBufferAvailable(…) 回调中拿到 buffer。
拿到输出 buffer 的数据处理完毕后要调用 releaseOutputBuffer 将 buffer 释放回 codec 中。

输入输出 buffer 用完后都要及时提交到/释放回 codec 。毕竟 codec 的 buffer 数量是有限的,如果占满了,肯定就没法处理了。输入 buffer 被占满后 dequeueInputBuffer 会一直返回 -1, 输出 buffer 占满后 dequeueOutputBuffer 会一直返回 -1

5.0 之后官方推荐以异步方式处理 buffer

 MediaCodec codec = MediaCodec.createByCodecName(name);
 MediaFormat mOutputFormat; // member variable
 codec.setCallback(new MediaCodec.Callback() {
   @Override
   void onInputBufferAvailable(MediaCodec mc, int inputBufferId) {
     // 拿到一个输入 buffer -> 填充数据 ->入队交给 codec 处理
     ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);
     // fill inputBuffer with valid data
     …
     codec.queueInputBuffer(inputBufferId, …);
   }

   @Override
   void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, …) {
     // 拿出一个 codec 处理完的输出 buffer -> 处理 -> 释放
     ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
     MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
     // bufferFormat is equivalent to mOutputFormat
     // outputBuffer is ready to be processed or rendered.
     …
     codec.releaseOutputBuffer(outputBufferId, …);
   }

   @Override
   void onOutputFormatChanged(MediaCodec mc, MediaFormat format) {
     // Subsequent data will conform to new format.
     // Can ignore if using getOutputFormat(outputBufferId)
     mOutputFormat = format; // option B
   }

   @Override
   void onError(…) {
     …
   }
 });
 codec.configure(format, …);
 mOutputFormat = codec.getOutputFormat(); // option B
 codec.start();
 // wait for processing to complete
 codec.stop();
 codec.release();

同步处理方式(不推荐)

MediaCodec codec = MediaCodec.createByCodecName(name);
 codec.configure(format, …);
 MediaFormat outputFormat = codec.getOutputFormat(); // option B
 codec.start();
 for (;;) {
   int inputBufferId = codec.dequeueInputBuffer(timeoutUs);
   if (inputBufferId >= 0) {
     ByteBuffer inputBuffer = codec.getInputBuffer(…);
     // fill inputBuffer with valid data
     …
     codec.queueInputBuffer(inputBufferId, …);
   }
   int outputBufferId = codec.dequeueOutputBuffer(…);
   if (outputBufferId >= 0) {
     ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
     MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
     // bufferFormat is identical to outputFormat
     // outputBuffer is ready to be processed or rendered.
     …
     codec.releaseOutputBuffer(outputBufferId, …);
   } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
     // Subsequent data will conform to new format.
     // Can ignore if using getOutputFormat(outputBufferId)
     outputFormat = codec.getOutputFormat(); // option B
   }
 }
 codec.stop();
 codec.release();

同步方式使用 ByteBuffer 数组获取 buffer (已废弃)

MediaCodec codec = MediaCodec.createByCodecName(name);
codec.configure(format, …);
codec.start();
ByteBuffer[] inputBuffers = codec.getInputBuffers();
ByteBuffer[] outputBuffers = codec.getOutputBuffers();
for (;;) {
   int inputBufferId = codec.dequeueInputBuffer(…);
   if (inputBufferId >= 0) {
     // fill inputBuffers[inputBufferId] with valid data
     …
     codec.queueInputBuffer(inputBufferId, …);
   }
   int outputBufferId = codec.dequeueOutputBuffer(…);
   if (outputBufferId >= 0) {
     // outputBuffers[outputBufferId] is ready to be processed or rendered.
     …
     codec.releaseOutputBuffer(outputBufferId, …);
   } else if (outputBufferId == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
     outputBuffers = codec.getOutputBuffers();
   } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
     // Subsequent data will conform to new format.
     MediaFormat format = codec.getOutputFormat();
   }
 }
codec.stop();
codec.release();

如果需要兼容 4.X 版本,还得用上面的方法。

End-of-stream Handling

当到达输入末尾时,在调用 queueInputBuffer 时要在参数里面写一个 BUFFER_FLAG_END_OF_STREAM 的标志,表示输入完毕了。标志可以写在最后一个 buffer 中,也可以最后再专门提交一个空的 buffer (没有可用数据)。如果用空 buffer ,buffer 的 timestamp 会被忽略。

输入结束后 codec 就不再接受输入了,但会继续产生输出。输出处理完毕后也会在最后一个有用 buffer 或 空buffer 中含有 end-of-stream 的标志。可以用这个标志来标识 codec 处理完毕。通过 MediaCodec.BufferInfo 可以拿到 buffer 的 flag。

发出 end-of-stream 的 buffer 后就不要再提交 Input buffer 了。除非 codec 被 flushed, or stopped and restarted。

Using an Output Surface

codec 的输出也可以直接关联到一个 Surface 上。如视频解码后可以直接渲染到 SurfaceView 上。但这时 output buffers 就不可用了。getOutputBuffer/Image(int) 会返回 null。getOutputBuffers() 也会返回一个全是 null 的数组。

你可以选择是否直接把输出渲染到 Surface 上。

  • Do not render the buffer: Call releaseOutputBuffer(bufferId, false).
  • Render the buffer with the default timestamp: Call releaseOutputBuffer(bufferId, true).
  • Render the buffer with a specific timestamp: Call releaseOutputBuffer(bufferId, timestamp).

Using an Input Surface

也可以用 Surface 作为 codec 的输入,同理这时 input buffer 就不可用了,调用 dequeueInputBuffer 会抛异常。

调用 signalEndOfInputStream() 后 surface 会停止向 codec 发送数据。

转载于:https://www.jianshu.com/p/06dfc5cf95a2

android 硬编解码 相关内容

2019-01-15 16:48:00 weixin_34357267 阅读数 40

官方文档 https://developer.android.google.cn/reference/android/media/MediaCodec

MediaCodec 是做硬件(GPU,充分利用GPU 的并行处理能力)编解码的。(通常结合 MediaExtractor、MediaSync、MediaMuxer、MediaCrypto、MediaDrm、Image、Surface、AudioTrack 使用)

codec (即 encoder + decoder ) :编解码器
MediaExtractor:解封装
MediaMuxer: 混合器(封装)(音视频合成)
MediaSync: 音视频同步
MediaCrypto: 加密

原始音频数据 (PCM )和 视频帧压缩编码(aac + h.264 等 )后要封装到一个容器(MP4 等)中进行传播。同理播放器播放前要先解封装,取出里面的音频部分和视频部分,然后解码成硬件可以直接播放和渲染的音频流和视频流。由于音频流和视频流是分别播放和渲染的,所以这里就有音视频同步的问题。

356361-88f9733c632b2786.png
jiagou.png

codec 处理输入数据产生输出数据。它通过输入缓冲集合和输出缓冲集合异步的处理数据。先请求一个空的 input buffer,然后填充上要处理的数据发送给 codec 处理。 codec 处理数据后会把结果写到一个空的 output buffer 中。最后你请求 output buffer 从里面读出处理后的数据就行了。output buffer 用完后释放回 codec 重新使用。

数据类型

codec 处理3种类型的数据, compressed data (待解码的数据 或 编码后的数据)、raw audio data (待编码或解码后的数据)和 raw video data (待编码或解码后的数据)。3种数据类型都可以用 ByteBuffers 处理。还可以用 Surface 来处理 raw video data 来提高性能。因为 Surface 可以直接使用 native video buffers (在 native 层分配的 buffer)而不需要映射或拷贝到 ByteBuffers (ByteBuffers 是分配在 JVM 堆中的缓冲区) 中。

状态

356361-6f45df58f7d1958f.png
state.png

概念上主要包含 Stopped、Executing、Released 3种状态。Stopped 包含 Configured、Uninitialized、Error 3个子状态。 Executing 包含 Flushed、Running、End of Stream 3个子状态。

codec 实例化以后默认是 Uninitialized 状态,之后需要调用 configure 进入 Configured 状态,再调用 start 进入 Executing 状态。运行状态默认是 Flushed, 这时可以调用 dequeueInputBuffer 拿到一个 input buffer 开始处理数据,进入 Running 状态。当没有输入后需要写一个 end-of-stream marker 的标志(可以放在最后一个 Input buffer 中,也可以用一个单独的空 buffer,空 buffer 的 timestamp 会被忽略)。当 End of stream 后 codec 就不再接受输入了, 但仍然继续产生输出直到输出 buffer 遇到 end-of-stream marker 标志(这个标志是 codec 写的,前提是当没有输入时你必须给 input buffer 写一个 end-of-stream 的标志。这个标志可以作为 codec 处理完毕的标志)。在运行状态可以调用 flush() 方法回到 Flushed 状态。

运行时调用 stop 方法会重新回到 Uninitialized ,这时要重新 configue 、start 才能重新运行。 运行出错时会进入 Error 状态,这时可以调用 reset 方法恢复到 Uninitialized。 codec 不再使用时调用 release 方法释放资源进入 Released 状态。

创建

5.0 之后官方推荐用 MediaCodecList.findDecoderForFormat 传入一个 MediaFormat 来查找你要使用的 codec。 然后调用 MediaCodec.createByCodecName(String) 方法创建 codec。

MediaFormat mediaFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, 44100, 1);
mediaFormat.setString(MediaFormat.KEY_BIT_RATE, null);
MediaCodecList mediaCodecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
String name = mediaCodecList.findEncoderForFormat(mediaFormat);
Log.d(TAG, "name is " + name); // OMX.google.aac.encoder
try {
    mediaCodec = MediaCodec.createByCodecName(name);
} catch (IOException e) {
    e.printStackTrace();
}

也可以调用 MediaCodec.createDecoder/EncoderByType(String) 传入要处理数据的 MIME type 来创建。

try {
    // 5.0 之前可以这样写,aac 编解码一般都支持
    mediaCodec = MediaCodec.createEncoderByType("audio/mp4a-latm");
} catch (IOException e) {
    e.printStackTrace();
}

初始化

调用 configue 方法,如果需要异步处理 buffer 可以先调用 setCallback 方法设置回调。

数据处理

输入输出 buffer 是用 buffer-ID 来标识的。调用 start 后通过 dequeueInput/OutputBuffer(…) 方法拿到一个 buffer。异步模式需要在 MediaCodec.Callback.onInput/OutputBufferAvailable(…) 回调中拿到 buffer。
拿到输出 buffer 的数据处理完毕后要调用 releaseOutputBuffer 将 buffer 释放回 codec 中。

输入输出 buffer 用完后都要及时提交到/释放回 codec 。毕竟 codec 的 buffer 数量是有限的,如果占满了,肯定就没法处理了。输入 buffer 被占满后 dequeueInputBuffer 会一直返回 -1, 输出 buffer 占满后 dequeueOutputBuffer 会一直返回 -1

5.0 之后官方推荐以异步方式处理 buffer

 MediaCodec codec = MediaCodec.createByCodecName(name);
 MediaFormat mOutputFormat; // member variable
 codec.setCallback(new MediaCodec.Callback() {
   @Override
   void onInputBufferAvailable(MediaCodec mc, int inputBufferId) {
     // 拿到一个输入 buffer -> 填充数据 ->入队交给 codec 处理
     ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);
     // fill inputBuffer with valid data
     …
     codec.queueInputBuffer(inputBufferId, …);
   }

   @Override
   void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, …) {
     // 拿出一个 codec 处理完的输出 buffer -> 处理 -> 释放
     ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
     MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
     // bufferFormat is equivalent to mOutputFormat
     // outputBuffer is ready to be processed or rendered.
     …
     codec.releaseOutputBuffer(outputBufferId, …);
   }

   @Override
   void onOutputFormatChanged(MediaCodec mc, MediaFormat format) {
     // Subsequent data will conform to new format.
     // Can ignore if using getOutputFormat(outputBufferId)
     mOutputFormat = format; // option B
   }

   @Override
   void onError(…) {
     …
   }
 });
 codec.configure(format, …);
 mOutputFormat = codec.getOutputFormat(); // option B
 codec.start();
 // wait for processing to complete
 codec.stop();
 codec.release();

同步处理方式(不推荐)

MediaCodec codec = MediaCodec.createByCodecName(name);
 codec.configure(format, …);
 MediaFormat outputFormat = codec.getOutputFormat(); // option B
 codec.start();
 for (;;) {
   int inputBufferId = codec.dequeueInputBuffer(timeoutUs);
   if (inputBufferId >= 0) {
     ByteBuffer inputBuffer = codec.getInputBuffer(…);
     // fill inputBuffer with valid data
     …
     codec.queueInputBuffer(inputBufferId, …);
   }
   int outputBufferId = codec.dequeueOutputBuffer(…);
   if (outputBufferId >= 0) {
     ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
     MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
     // bufferFormat is identical to outputFormat
     // outputBuffer is ready to be processed or rendered.
     …
     codec.releaseOutputBuffer(outputBufferId, …);
   } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
     // Subsequent data will conform to new format.
     // Can ignore if using getOutputFormat(outputBufferId)
     outputFormat = codec.getOutputFormat(); // option B
   }
 }
 codec.stop();
 codec.release();

同步方式使用 ByteBuffer 数组获取 buffer (已废弃)

MediaCodec codec = MediaCodec.createByCodecName(name);
codec.configure(format, …);
codec.start();
ByteBuffer[] inputBuffers = codec.getInputBuffers();
ByteBuffer[] outputBuffers = codec.getOutputBuffers();
for (;;) {
   int inputBufferId = codec.dequeueInputBuffer(…);
   if (inputBufferId >= 0) {
     // fill inputBuffers[inputBufferId] with valid data
     …
     codec.queueInputBuffer(inputBufferId, …);
   }
   int outputBufferId = codec.dequeueOutputBuffer(…);
   if (outputBufferId >= 0) {
     // outputBuffers[outputBufferId] is ready to be processed or rendered.
     …
     codec.releaseOutputBuffer(outputBufferId, …);
   } else if (outputBufferId == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
     outputBuffers = codec.getOutputBuffers();
   } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
     // Subsequent data will conform to new format.
     MediaFormat format = codec.getOutputFormat();
   }
 }
codec.stop();
codec.release();

如果需要兼容 4.X 版本,还得用上面的方法。

End-of-stream Handling

当到达输入末尾时,在调用 queueInputBuffer 时要在参数里面写一个 BUFFER_FLAG_END_OF_STREAM 的标志,表示输入完毕了。标志可以写在最后一个 buffer 中,也可以最后再专门提交一个空的 buffer (没有可用数据)。如果用空 buffer ,buffer 的 timestamp 会被忽略。

输入结束后 codec 就不再接受输入了,但会继续产生输出。输出处理完毕后也会在最后一个有用 buffer 或 空buffer 中含有 end-of-stream 的标志。可以用这个标志来标识 codec 处理完毕。通过 MediaCodec.BufferInfo 可以拿到 buffer 的 flag。

发出 end-of-stream 的 buffer 后就不要再提交 Input buffer 了。除非 codec 被 flushed, or stopped and restarted。

Using an Output Surface

codec 的输出也可以直接关联到一个 Surface 上。如视频解码后可以直接渲染到 SurfaceView 上。但这时 output buffers 就不可用了。getOutputBuffer/Image(int) 会返回 null。getOutputBuffers() 也会返回一个全是 null 的数组。

你可以选择是否直接把输出渲染到 Surface 上。

  • Do not render the buffer: Call releaseOutputBuffer(bufferId, false).
  • Render the buffer with the default timestamp: Call releaseOutputBuffer(bufferId, true).
  • Render the buffer with a specific timestamp: Call releaseOutputBuffer(bufferId, timestamp).

Using an Input Surface

也可以用 Surface 作为 codec 的输入,同理这时 input buffer 就不可用了,调用 dequeueInputBuffer 会抛异常。

调用 signalEndOfInputStream() 后 surface 会停止向 codec 发送数据。

android 硬编解码 相关内容

基于CUDA的硬编解码

阅读数 4590

没有更多推荐了,返回首页