Android音视频开发(四)——MediaCodec:解码视频,得到YUV值,一帧一帧加载到SD卡中保存。

        我们上一节了解了MediaExtractor、MediaMuxer、MediaFormat、MediaCodec.BufferInfo。重复的内容我就不再赘述了,假如有上面的四个的一些补充还是会写一下。接下来我们学习MediaCodec,本节篇幅会比较长,知识点较多,请耐心品味。

一、MediaCodec

        ​ MediaCodec类可用于访问低级媒体编解码器,即编码器/解码器组件。它是Android低级多媒体支持基础设施的一部分(通常与MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, Surface,以及AudioTrack.)。

1.1数据类型

        编解码器处理三种数据:压缩数据原始音频数据原始视频数据。​ 所有这三种数据都可以使用ByteBuffers,但您应该使用Surface用于原始视频数据以提高编解码器性能。Surface使用原生视频缓冲区,而不将其映射或复制到ByteBuffers因此,它的效率更高。

        使用Surface时,通常无法访问原始视频数据,但可以使用ImageReader类来访问不安全的解码(原始)视频帧。这可能仍然比使用字节缓冲区更有效,因为一些本机缓冲区可以映射到直接的字节缓冲区。使用ByteBuffer模式时,可以使用Image类别和getInput/OutputImage(int)访问原始视频帧。

(1)压缩缓冲区

        压缩数据可以作为解码器的输入、编码器的输出,需要指定数据的格式,这样codec才知道如何处理这些压缩数据

  • MediaFormat.KEY_MIME格式类型。
  • 对于视频类型,通常是一个单独的压缩视频帧。
  • 对于音频数据,通常是一个单独的访问单元(一个编码的音频段通常包含由格式类型决定的几毫秒的音频),但是这个要求稍微宽松一些,因为一个buffer可能包含多个编码的音频访问单元。
  • 在这两种情况下,buffer都不会在任意字节边界上开始或结束,而是在帧/访问单元边界上开始或结束,除非它们被BUFFER_FLAG_PARTIAL_FRAME标记。

(2)原始音频缓冲区

        原始音频数据 — PCM音频数据帧,这是每个通道按通道顺序的一个样本。

(3)原始视频缓冲区

        在ByteBuffer模式下,视频buffer根据它们的MediaFormat.KEY_COLOR_FORMAT进行布局。可以从getCodecInfo(). MediaCodecInfo.getCapabilitiesForType.CodecCapability.colorFormats获取支持的颜色格式。视频编解码器可以支持三种颜色格式:

  • native raw video format: CodecCapabilities.COLOR_FormatSurface,可以与输入/输出的Surface一起使用。
  • flexible YUV buffers 例如CodecCapabilities.COLOR_FormatYUV420Flexible, 可以使用getInput/OutputImage(int)与输入/输出Surface一起使用,也可以在ByteBuffer模式下使用。
  • other, specific formats: 通常只支持ByteBuffer模式。有些颜色格式是厂商特有的,其他定义在CodecCapabilities。对于等价于flexible格式的颜色格式,可以使用getInput/OutputImage(int)。

        从Build.VERSION_CODES.LOLLIPOP_MR1.开始,所有视频编解码器都支持flexible的YUV 4:2:0 buffer。

1.2 MediaCodec API 

(1)MediaCodec创建:

  • createDecoderByType/createEncoderByType:根据特定MIME类型(如"video/avc")创建codec。然而,这不能用于注入特征,并且可能创建不能处理特定期望媒体格式的编解码器。
  • createByCodecName:知道组件的确切名称(如OMX.google.mp3.decoder)的时候,根据组件名创建codec。使用MediaCodecList可以获取组件的名称。

(2)MediaCodec配置与启动:

  • configure:配置解码器或者编码器。

四个参数:

  • MediaFormat format:输入数据的格式(解码器)或输出数据的所需格式(编码器)。传null等同于传递MediaFormat.MediaFormat作为空的MediaFormat。
  • Surface surface:指定Surface,用于解码器输出的渲染。如果编解码器不生成原始视频输出(例如,不是视频解码器)和/或想配置解码器输出ByteBuffer,则传null。
  • MediaCrypto crypto:指定一个crypto对象,用于对媒体数据进行安全解密。对于非安全的编解码器,传null。
  • int flags:当组件是编码器时,flags指定为常量CONFIGURE_FLAG_ENCODE。
  • start:成功配置组件后调用start。

(3)buffer处理的接口:

MediaCodec可以处理具体的视频流,主要有这几个方法:

  • getInputBuffers:获取需要编码数据的输入流队列,返回的是一个ByteBuffer数组 
  • getInputBuffer(index) : 获取InputBuffers数组index下标的ByteBuffer
  • queueInputBuffer:输入流入队列 
  • dequeueInputBuffer:从输入流队列中取数据进行编码操作 
  • getOutputBuffers:获取编解码之后的数据输出流队列,返回的是一个ByteBuffer数组 
  • getOutputBuffer(index) : 获取OutputBuffers数组index下标的ByteBuffer
  • dequeueOutputBuffer:从输出队列中取出编码操作之后的数据 
  • releaseOutputBuffer:处理完成,释放ByteBuffer数据 

(4)处理完之后的操作:

  • flush:清空的输入和输出端口。
  • stop:终止decode/encode会话
  • release:释放编解码器实例使用的资源。

二、MediaFormat 一些属性理解

        MediaFormat的格式被指定为键/值对。keys是字符串。values可以是整数、长整型、浮点型、字符串或ByteBuffer。(要素元数据被指定为  字符串/布尔  对。)

2.1 所有音频/视频格式通用的键,所有未标记为可选的键都是强制性的:

名字值类型描述
KEY_MIMEString格式的类型。
KEY_CODECS_STRINGString可选,媒体格式的RFC 6381编解码器字符串
KEY_MAX_INPUT_SIZEInteger可选,输入数据缓冲区的最大大小
KEY_PIXEL_ASPECT_RATIO_WIDTHInteger可选,像素长宽比宽度
KEY_PIXEL_ASPECT_RATIO_HEIGHTInteger可选,像素纵横比高度
KEY_BIT_RATEInteger仅编码器,以比特/秒为单位的所需比特率
KEY_DURATIONLong内容的持续时间(以微秒计)

2.2 视频格式有以下键:

名字值类型描述
KEY_WIDTHInteger
KEY_HEIGHTInteger
KEY_COLOR_FORMATInteger由用户为编码器设置,以解码器的输出格式可读
KEY_FRAME_RATEInteger or Float要求用于编码器,可选用于解码器
KEY_CAPTURE_RATEInteger
KEY_I_FRAME_INTERVALInteger or Float仅编码器关键帧之间的时间间隔。添加了浮动支持android.os.Build.VERSION_CODES#N_MR1
KEY_INTRA_REFRESH_PERIODInteger仅编码器,可选
KEY_LATENCYInteger仅编码器,可选
KEY_MAX_WIDTHInteger仅解码器,可选,最大分辨率宽度
KEY_MAX_HEIGHTInteger仅解码器,可选,最大分辨率高度
KEY_REPEAT_PREVIOUS_FRAME_AFTERLong仅表面模式下的编码器,可选
KEY_PUSH_BLANK_BUFFERS_ON_STOPInteger解码器仅渲染到表面,可选
KEY_TEMPORAL_LAYERINGString仅编码器可选的时间分层模式

2.3 音频格式有以下键:

名字值类型描述
KEY_CHANNEL_COUNTInteger
KEY_SAMPLE_RATEInteger
KEY_PCM_ENCODINGInteger可选择的
KEY_IS_ADTSInteger可选,如果解码AAC音频内容,将此键设置为1表示每个音频帧都以ADTS标头为前缀。
KEY_AAC_PROFILEInteger仅编码器可选,如果内容是AAC音频,则指定所需的配置文件。
KEY_AAC_SBR_MODEInteger仅编码器,可选,如果内容是AAC音频,则指定所需的SBR模式。
KEY_AAC_DRC_TARGET_REFERENCE_LEVELInteger仅解码器可选,如果内容是AAC音频,则指定目标参考电平。
KEY_AAC_ENCODED_TARGET_LEVELInteger仅解码器可选,如果内容是AAC音频,则指定编码器使用的目标参考电平。
KEY_AAC_DRC_BOOST_FACTORInteger仅解码器,可选,如果内容是AAC音频,则指定DRC增强因子。
KEY_AAC_DRC_ATTENUATION_FACTORInteger仅解码器可选,如果内容是AAC音频,则指定DRC衰减系数。
KEY_AAC_DRC_HEAVY_COMPRESSIONInteger仅解码器可选,如果内容是AAC音频,则指定是否使用重度压缩。
KEY_AAC_MAX_OUTPUT_CHANNEL_COUNTInteger仅解码器可选,如果内容是AAC音频,则指定解码器输出的最大通道数。
KEY_AAC_DRC_EFFECT_TYPEInteger仅解码器可选,如果内容是AAC音频,则指定要使用的MPEG-D DRC效果类型。
KEY_AAC_DRC_OUTPUT_LOUDNESSInteger仅解码器,可选,如果内容是AAC音频,则返回DRC输出响度。
KEY_AAC_DRC_ALBUM_MODEInteger仅解码器可选,如果内容是AAC音频,则指定MPEG-D DRC专辑模式是否处于活动状态。
KEY_CHANNEL_MASKInteger可选,音频通道分配的遮罩
KEY_ENCODER_DELAYInteger可选,从解码的音频流的开始处要修剪的帧数。
KEY_ENCODER_PADDINGInteger可选,从解码的音频流的结尾开始修剪的帧数。
KEY_FLAC_COMPRESSION_LEVELInteger仅编码器,可选,如果内容是FLAC音频,则指定所需的压缩级别。
KEY_MPEGH_PROFILE_LEVEL_INDICATIONInteger仅解码器可选,如果内容是MPEG-H音频,则指定流的配置文件和级别。
KEY_MPEGH_COMPATIBLE_SETSByteBuffer仅解码器可选,如果内容是MPEG-H音频,则指定流的兼容集(配置文件和级别)。
KEY_MPEGH_REFERENCE_CHANNEL_LAYOUTInteger仅解码器可选,如果内容是MPEG-H音频,则指定流的首选参考通道布局。

2.4 字幕格式有以下关键字:

线格式的类型。KEY_LANGUAGE线内容的语言。KEY_CAPTION_SERVICE_NUMBER(同Internationalorganizations)国际组织可选,隐藏字幕服务或频道号。

名字值类型描述
KEY_MIMEString格式的类型
KEY_LANGUAGEString内容的语言
KEY_CAPTION_SERVICE_NUMBERint可选,隐藏字幕服务或频道号

2.5 图像格式有以下键:

KEY_MIMEString格式的类型。
KEY_WIDTHInteger
KEY_HEIGHTInteger
KEY_COLOR_FORMATInteger由用户为编码器设置,以解码器的输出格式可读
KEY_TILE_WIDTHInteger如果图像有网格,则需要
KEY_TILE_HEIGHTInteger如果图像有网格,则需要
KEY_GRID_ROWSInteger如果图像有网格,则需要
KEY_GRID_COLUMNSInteger如果图像有网格,则需要

至于值,常用的也就那些,我就不提供了,自己去开发者文档查找。

三、Image 

与媒体源一起使用的单个完整图像缓冲区,例如 MediaCodec或 CameraDevice 。

该类允许通过一个或多个ByteBuffers高效地直接应用程序访问图像的像素数据。 每个缓冲区都封装在描述该平面中像素数据布局的Image.Plane中。 由于这种直接访问方式,与Bitmap类不同,图像不能直接用作UI资源。

由于图像通常由硬件组件直接生成或使用,因此它们是整个系统共享的有限资源,应在不再需要时立即关闭。

例如,当使用ImageReader类从各种媒体源读出图像时,一旦达到the maximum outstanding image count ,不关闭旧的图像对象将阻止新图像的可用性。 发生这种情况时,获取新图像的函数通常会抛出IllegalStateException 。

3.1 getPlanes() 方法

        获取该图像的像素平面阵列,平面的数量由图像的格式决定。这个需要了解ImageFormat的格式了,这个以后再了解。

3.2 ImageFormat.getBitsPerPixel()方法

        使用此函数可检索ImageFormat的每个像素的位数。

这个在此就点一下算了,之后在单独开一节来具体讲解。

四、demo实现

//解码操作,返回YUV加载的bitmap图片
public class ImageShowActivity extends Activity {
    private TextView tv_yun;
    //图片的个数
    private int imageNum = 0;
    private final String mVideoPath = Environment.getExternalStorageDirectory() + "/Pictures/music.mp4";
    private MediaExtractor extractor;//用于解封装
    private MediaFormat videoFormat;//保存视频轨道的媒体格式
    private MediaCodec mediaCodec;//解码视频轨道资源
    private int rotation;
    private long duration;

    //用于代表YUV的种类
    public static final int YUV420P = 0;
    public static final int YUV420SP = 1;
    public static final int NV21 = 2;

    //保存YUV数据的byte[]
    private byte[] bytes;

    private static int width;
    private static int height;

    //1.创建MediaExtractor和MediaCodec :  MediaExtractor负责解封装,MediaCodec负责解码视频轨道资源
    //2.解码获取图片,并进行转换:YUV_420_888-->NV21
    //3.YuvImage 加载nv21,转化成Bitmap用于显示。
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_image_show);

        tv_yun = findViewById(R.id.tv_image_yuv);

        GetVideo();
    }

}

//获取到视频轨道资源
    private void GetVideo(){
        extractor = new MediaExtractor();
        try {
            extractor.setDataSource(mVideoPath);
            //获取轨道个数
            int trackCount = extractor.getTrackCount();
            for (int i = 0; i <trackCount ; i++) {
                //获取某一轨道的媒体格式
                MediaFormat trackFormat = extractor.getTrackFormat(i);
                if(trackFormat.getString(MediaFormat.KEY_MIME).contains("video")){
                    videoFormat =trackFormat;
                    extractor.selectTrack(i);
                    break;
                }
            }
            if(videoFormat == null){
                throw new IllegalArgumentException("没有获取到视频的Format");
            }
            // 其中MediaFormat.KEY_COLOR_FORMAT 需要设置成COLOR_FormatYUV420Flexible
            // MediaCodec的所有硬件解码都支持这种格式
            videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,COLOR_FormatYUV420Flexible);
            videoFormat.setInteger(MediaFormat.KEY_WIDTH,videoFormat.getInteger(MediaFormat.KEY_WIDTH));
            videoFormat.setInteger(MediaFormat.KEY_HEIGHT,videoFormat.getInteger(MediaFormat.KEY_HEIGHT));
            if(videoFormat.containsKey(MediaFormat.KEY_ROTATION)){
                //获取旋转多少的值
                rotation = videoFormat.getInteger(MediaFormat.KEY_ROTATION);
            }
            //内容持续时间
            duration = videoFormat.getLong(MediaFormat.KEY_DURATION);
            //MediaCodec创建:createDecoderByType/createEncoderByType:根据特定MIME类型(如"video/avc")创建codec。
            //createByCodecName:知道组件的确切名称(如OMX.google.mp3.decoder)的时候,根据组件名创建codec。使用MediaCodecList可以获取组件的名称。
            mediaCodec = MediaCodec.createDecoderByType(videoFormat.getString(MediaFormat.KEY_MIME));
            //configure:配置解码器或者编码器。
            // 参数1:MediaFormat ,参数2:Surface ,参数3:MediaCrypto ,参数4:flags
            mediaCodec.configure(videoFormat,null,null,0);
            mediaCodec.start();
            //开始解码
            processByExtractor();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
  • 描述视频格式内容的颜色格式的关键字,需要在android.media.MediaCodecInfo.CodecCapabilities中声明。
  • 通过MediaFormat,获取了视频信息,duration 和rotation 是比较重要的。duration /1000/1000可以判断我们需要取几帧,1s一帧,rotation判断视频旋转角度,后面生成bitmap需要用到。
    //开始解码,获取帧序列
    private void processByExtractor() {
        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
        long timeOut = 5* 1000;//5ms
        boolean inputDone = false;
        boolean outputDone = false;
        ByteBuffer[] inputBuffers = null;
//        if(Build.VERSION.SDK_INT< Build.VERSION_CODES.LOLLIPOP){
//            //开始喂数据
//            inputBuffers = mediaCodec.getInputBuffers();
//        }
        inputBuffers = mediaCodec.getInputBuffers();
        //开始解码
        int count = 0;
        while (!outputDone){
            if(!inputDone){
                //喂数据
                //如果是要获取所有帧序列,则不需要使用seekTo方法。
                //extractor.seekTo(count * intervalMs * 1000,MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
                int inputBufferIndex = mediaCodec.dequeueInputBuffer(timeOut);
                if(inputBufferIndex >= 0){
                    ByteBuffer inputBuffer;
                    if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.LOLLIPOP){
                        inputBuffer = mediaCodec.getInputBuffer(inputBufferIndex);
                    }else {
                        inputBuffer = inputBuffers[inputBufferIndex];
                    }
                    int sampleDate = extractor.readSampleData(inputBuffer,0);

                    if(sampleDate > 0 && count * 1000 <= duration){
                        long sampleTime = extractor.getSampleTime();
                        int sampleFlags = extractor.getSampleFlags();

                        mediaCodec.queueInputBuffer(inputBufferIndex,0,sampleDate,sampleTime,0);
                        extractor.advance();
                        count++;
                    }else {
                        //小于0,就说明读完了
                        mediaCodec.queueInputBuffer(inputBufferIndex,0,0,0,MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                        inputDone = true;
                    }
                }
            }
            if(!outputDone){
                //获取数据
                int status = mediaCodec.dequeueOutputBuffer(bufferInfo,timeOut);
                if(status == MediaCodec.INFO_TRY_AGAIN_LATER){
                }else if(status == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED){
                }else if(status == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED){
                }else {
                    if((bufferInfo.flags&MediaCodec.BUFFER_FLAG_END_OF_STREAM)!=0){
                        outputDone = true;
                    }
                    boolean doRender = (bufferInfo.size !=0);
                    //获取图片并保存,getOutputImage格式是YUV_420_888
                    Image image = mediaCodec.getOutputImage(status);
                    mediaCodec.getOutputBuffer(status);
                    System.out.println("成功获取到图片"+"SSSSSSSSSSSSSSSSSSSSSSS");
                    imageNum++;
                    //dateFromImage(image);
                    //使用新方法来获取yuv数据
                    bytes = getBytesFromImageAsType(image,2);

                    //根据yuv数据获取Bitmap
                    Bitmap bitmap = getBitmapFromYUV(bytes,width,height,rotation);
                    //保存图片
                    if(bitmap != null){
                        //显示图片
                        String businesslogofile=Environment.getExternalStorageDirectory()+"/Pictures/logo"+imageNum+".png";
                        File file = new File(businesslogofile);
                        try {
                            bitmap.compress(Bitmap.CompressFormat.JPEG,100,new FileOutputStream(file));
                        } catch (FileNotFoundException e) {
                            e.printStackTrace();
                        }
                        System.out.println("图片导入成功");
                    }

                    mediaCodec.releaseOutputBuffer(status,doRender);
                    //这里先尝试获取第一张图片
                    //break;

                }
            }
        }

    }

细将一下MediaCodec的四个方法:

1. dequeueInputBuffer:

public final int dequeueInputBuffer(long timeoutUs)
  • 返回用于填充有效数据的输入buffer的索引,如果当前没有可用的buffer,则返回-1
  • long timeoutUs等待可用的输入buffer的时间。
    • 如果timeoutUs == 0,则立即返回。
    • 如果timeoutUs < 0,则无限期等待可用的输入buffer。
    • 如果timeoutUs > 0,则等待“timeoutUs”微秒。

2. queueInputBuffer:在指定索引处填充输入buffer后,使用queueInputBuffer将buffer提交给组件。

许多codec要求实际压缩的数据流之前必须有“特定于codec的数据”,即用于初始化codec的设置数据,如

  • AVC视频中的PPS/SPS。
  • vorbis音频中的code tables。
public native final void queueInputBuffer(
    int index,
    int offset, int size, long presentationTimeUs, int flags)
  • int index:以前调用dequeueInputBuffer(long)返回的输入buffer的索引。
  • int offset:数据开始时输入buffer中的字节偏移量。
  • int size:有效输入数据的字节数。
  • long presentationTimeUs:此buffer的PTS(以微秒为单位)。
  • int flags:一个由BUFFER_FLAG_CODEC_CONFIG和BUFFER_FLAG_END_OF_STREAM标志组成的位掩码。虽然没有被禁止,但是大多数codec并不对输入buffer使用BUFFER_FLAG_KEY_FRAME标志。
    • BUFFER_FLAG_END_OF_STREAM:用于指示这是输入数据的最后一部分。
    • BUFFER_FLAG_CODEC_CONFIG:通过指定这个标志,可以在start()或flush()之后直接提交特定于codec的数据buffer。但是,如果您使用包含这些密钥的媒体格式配置编解码器,它们将在启动后由MediaCodec直接自动提交。因此,不建议使用BUFFER_FLAG_CODEC_CONFIG标志,只建议高级用户使用。

3. dequeueOutputBuffer:从MediaCodec获取输出buffer。

    public final int dequeueOutputBuffer(
            @NonNull BufferInfo info, long timeoutUs) 
  • 返回值:已成功解码的输出buffer的索引或INFO_*常量之一(INFO_TRY_AGAIN_LATER, INFO_OUTPUT_FORMAT_CHANGED 或 INFO_OUTPUT_BUFFERS_CHANGED)。
    • 返回INFO_TRY_AGAIN_LATER而timeoutUs指定为了非负值,表示超时了。
    • 返回INFO_OUTPUT_FORMAT_CHANGED表示输出格式已更改,后续数据将遵循新格式。
  • BufferInfo info:输出buffer的metadata。
  • long timeoutUs:含义同dequeueInputBuffer中的timeoutUs参数。

4. releaseOutputBuffer:使用此方法将输出buffer返回给codec或将其渲染在输出surface。

public void releaseOutputBuffer (int index, boolean render)
  • boolean render:如果在配置codec时指定了一个有效的surface,则传递true会将此输出buffer在surface上渲染。一旦不再使用buffer,该surface将把buffer释放回codec。

    //根据image获取yuv值-------------------NEW
    public static byte[] getBytesFromImageAsType(Image image, int type){
        try {
            //获取源数据,如果是YUV格式的数据planes.length = 3
            //plane[i]里面的实际数据可能存在byte[].length <= capacity (缓冲区总大小)
            final Image.Plane[] planes = image.getPlanes();

            //数据有效宽度,一般的,图片width <= rowStride,这也是导致byte[].length <= capacity的原因
            // 所以我们只取width部分
            width = image.getWidth();
            height = image.getHeight();

            //此处用来装填最终的YUV数据,需要1.5倍的图片大小,因为Y U V 比例为 4:1:1 (这里是YUV_420_888)
            byte[] yuvBytes = new byte[width * height * ImageFormat.getBitsPerPixel(ImageFormat.YUV_420_888) / 8];
            //目标数组的装填到的位置
            int dstIndex = 0;

            //临时存储uv数据的
            byte uBytes[] = new byte[width * height / 4];
            byte vBytes[] = new byte[width * height / 4];
            int uIndex = 0;
            int vIndex = 0;

            int pixelsStride, rowStride;
            for (int i = 0; i < planes.length; i++) {
                pixelsStride = planes[i].getPixelStride();
                rowStride = planes[i].getRowStride();

                ByteBuffer buffer = planes[i].getBuffer();

                //如果pixelsStride==2,一般的Y的buffer长度=640*480,UV的长度=640*480/2-1
                //源数据的索引,y的数据是byte中连续的,u的数据是v向左移以为生成的,两者都是偶数位为有效数据
                byte[] bytes = new byte[buffer.capacity()];
                buffer.get(bytes);

                int srcIndex = 0;
                if (i == 0) {
                    //直接取出来所有Y的有效区域,也可以存储成一个临时的bytes,到下一步再copy
                    for (int j = 0; j < height; j++) {
                        System.arraycopy(bytes, srcIndex, yuvBytes, dstIndex, width);
                        srcIndex += rowStride;
                        dstIndex += width;
                    }
                } else if (i == 1) {
                    //根据pixelsStride取相应的数据
                    for (int j = 0; j < height / 2; j++) {
                        for (int k = 0; k < width / 2; k++) {
                            uBytes[uIndex++] = bytes[srcIndex];
                            srcIndex += pixelsStride;
                        }
                        if (pixelsStride == 2) {
                            srcIndex += rowStride - width;
                        } else if (pixelsStride == 1) {
                            srcIndex += rowStride - width / 2;
                        }
                    }
                } else if (i == 2) {
                    //根据pixelsStride取相应的数据
                    for (int j = 0; j < height / 2; j++) {
                        for (int k = 0; k < width / 2; k++) {
                            vBytes[vIndex++] = bytes[srcIndex];
                            srcIndex += pixelsStride;
                        }
                        if (pixelsStride == 2) {
                            srcIndex += rowStride - width;
                        } else if (pixelsStride == 1) {
                            srcIndex += rowStride - width / 2;
                        }
                    }
                }
            }
            //   image.close();
            //根据要求的结果类型进行填充
            switch (type) {
                case YUV420P:
                    System.arraycopy(uBytes, 0, yuvBytes, dstIndex, uBytes.length);
                    System.arraycopy(vBytes, 0, yuvBytes, dstIndex + uBytes.length, vBytes.length);
                    break;
                case YUV420SP:
                    for (int i = 0; i < vBytes.length; i++) {
                        yuvBytes[dstIndex++] = uBytes[i];
                        yuvBytes[dstIndex++] = vBytes[i];
                    }
                    break;
                case NV21:
                    for (int i = 0; i < vBytes.length; i++) {
                        yuvBytes[dstIndex++] = vBytes[i];
                        yuvBytes[dstIndex++] = uBytes[i];
                    }
                    break;
            }
            return yuvBytes;
        } catch (final Exception e) {
            if (image != null) {
                image.close();
            }
        }
        return null;
    }
    private Bitmap getBitmapFromYUV(byte[] date, int width, int height, int rotation) {
        //使用YuvImage---》NV21
        YuvImage yuvImage = new YuvImage(date, ImageFormat.NV21,width,height,null);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        yuvImage.compressToJpeg(new Rect(0,0,width,height),20,baos);
        byte[] jdate =baos.toByteArray();
        BitmapFactory.Options bitmapFatoryOptions = new BitmapFactory.Options();
        bitmapFatoryOptions.inPreferredConfig = Bitmap.Config.RGB_565;
        bitmapFatoryOptions.inSampleSize = 4;
        if(rotation == 0){
            Bitmap bmp = BitmapFactory.decodeByteArray(jdate,0,jdate.length,bitmapFatoryOptions);
            return bmp;
        }else {
            Matrix m = new Matrix();
            m.postRotate(rotation);
            Bitmap bmp = BitmapFactory.decodeByteArray(jdate,0,jdate.length,bitmapFatoryOptions);
            Bitmap bml = Bitmap.createBitmap(bmp,0,0,bmp.getWidth(),bmp.getHeight(),m,true);
            return bml;
        }
    }

这一节就是对本系列的(一)(二)基本知识的利用了。这个有机会再开一个小节来讲。

  • 9
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论
以下是一个简单的Android JNI代码示例,演示如何使用C++类MediaCodec和MediaFormat来解码视频文件并获取输出缓冲区: ```c++ #include <jni.h> #include <android/log.h> #include <android/native_window_jni.h> #include <media/NdkMediaCodec.h> #define LOG_TAG "MediaCodecExample" #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) extern "C" { JNIEXPORT void JNICALL Java_com_example_MediaCodecUtils_decode(JNIEnv *env, jobject instance, jstring filePath_, jobject surface) { const char *filePath = env->GetStringUTFChars(filePath_, NULL); FILE *fp = fopen(filePath, "rb"); if (!fp) { LOGD("Failed to open file: %s", filePath); return; } // 创建解码器 AMediaCodec *codec = AMediaCodec_createDecoderByType("video/avc"); AMediaFormat *format = AMediaFormat_new(); AMediaFormat_setString(format, AMEDIAFORMAT_KEY_MIME, "video/avc"); AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_WIDTH, 1920); AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_HEIGHT, 1080); AMediaFormat_setBuffer(format, "csd-0", (void *) csd_data, csd_size); AMediaCodec_configure(codec, format, ANativeWindow_fromSurface(env, surface), NULL /* crypto */, 0 /* flags */); AMediaCodec_start(codec); // 循环解码 bool inputDone = false; bool outputDone = false; while (!outputDone) { if (!inputDone) { // 获取可用输入缓冲区的索引 ssize_t inputIndex = AMediaCodec_dequeueInputBuffer(codec, 2000); if (inputIndex >= 0) { AMediaCodecBufferInfo inputBufferInfo; auto inputBuffer = AMediaCodec_getInputBuffer(codec, inputIndex); size_t bytesRead = fread(inputBuffer->data, 1, inputBuffer->capacity, fp); if (bytesRead > 0) { // 提交输入缓冲区 AMediaCodec_queueInputBuffer(codec, inputIndex, 0, bytesRead, 0, inputDone ? AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM : 0); } else { inputDone = true; AMediaCodec_queueInputBuffer(codec, inputIndex, 0, 0, 0, AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM); } } } // 获取可用输出缓冲区的索引 AMediaCodecBufferInfo outputBufferInfo; ssize_t outputIndex = AMediaCodec_dequeueOutputBuffer(codec, &outputBufferInfo, 0); if (outputIndex == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED) { // 更新输出格式 format = AMediaCodec_getOutputFormat(codec); LOGD("Output format changed: %s", AMediaFormat_toString(format)); } else if (outputIndex == AMEDIACODEC_INFO_OUTPUT_BUFFERS_CHANGED) { // 忽略此消息 LOGD("Output buffers changed"); } else if (outputIndex >= 0) { auto outputBuffer = AMediaCodec_getOutputBuffer(codec, outputIndex); // 处理输出缓冲区 LOGD("Output buffer %zd, size %d, flags %d, pts %lld, dts %lld", outputIndex, outputBufferInfo.size, outputBufferInfo.flags, outputBufferInfo.presentationTimeUs, outputBufferInfo.decodingTimeUs); // 获取输出缓冲区的数据 uint8_t *data = outputBuffer->data; int dataSize = outputBufferInfo.size; // 处理数据... // 处理完输出缓冲区后,释放缓冲区 AMediaCodec_releaseOutputBuffer(codec, outputIndex, true /* render */); outputDone = (outputBufferInfo.flags & AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM) != 0; } } // 释放资源 AMediaCodec_stop(codec); AMediaCodec_delete(codec); fclose(fp); env->ReleaseStringUTFChars(filePath_, filePath); } } ``` 这个例子,我们使用AMediaCodec_createDecoderByType函数创建一个H.264视频解码器,然后使用AMediaFormat来指定视频的宽度、高度、MIME类型和CSD数据。在configure方法,我们将解码器与一个Surface关联起来,以便输出图像可以在屏幕上渲染。在循环,我们首先获取可用的输入缓冲区索引,并从输入文件读取数据填充缓冲区。然后,我们提交输入缓冲区,并等待解码器输出缓冲区。当我们获取可用的输出缓冲区索引时,我们可以从输出缓冲区获取数据并进行处理。最后,我们释放输出缓冲区并检查是否到达了输入文件的结尾。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

撩得Android一次心动

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值