Android音视频-视频编解码(H.264视频硬编硬解)

在前面接触了音频的编解码,学习了通过MediaCodec来进行硬编码。把AudioRecord 采集的到的PCM音频数据编码压缩为AAC格式的音频数据,然后解码为PCM通过AudioTrack来播放。参考Demo链接

前面我们可以很形象的了解音频数据,然后如何来编解码音频数据,并且操作这些数据。对于视频的数据的编解码也希望可以有那样形象的理解。

在前面文章中Camera预览中我们直接使用高级API MediaRecord来进行视频的录制包括视频和音频数据的录制。它屏蔽了我们对于底层到底是如何编码完成的的实现细节。但是我们作为开发者要了解实现底层的实现细节。

本文实现功能:

  • 通过Camera采集NV21数据编码为H.264视频文件并保存
  • 通过Camera2采集YV12数据编码为H.264视频文件并保存
  • 通过SurfaceView解码显示Camera编码保存的H.264视频文件
  • 通过TextureView解码显示Camera编码保存的H.264视频文件

视频编解码基础

这个我们在前面已经了解过,这里再稍微回顾一下,视频的编码我们仔细学习视频的编码格式的编解码。封装视频的格式的编解码主流的有H.26X系列和MPEG系列,我们前面使用Camera来录制视频的时候就设置了MediaRecord的音视频的编解码。代码回顾:

mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.MPEG_4_SP);
        mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);

我们通过MediaRecorder设置了视频编码为MPEG 系列来编码,录制完成的视频拿出来看一下详情是不是MPEG的编码。


这里写图片描述

可以看到是我们设置的音视频的编码方式。

这个是Android的高级API来做的,底层的实现细节都屏蔽了,我们不止要知道这些,还要和音频一样知道简单的PCM数据如何编解码成我们要的数据格式。

视频编码

我们同样也和音频的原始数据PCM数据编码很为wav类似的过程。对于视频数据我们前面通过摄像头采集到的视频数据数据是YUV或者RGB格式的。这种数据格式因为是原始的数据,它就会很大了,我们于是通过各种编码方式可以压缩视频数据的大小。并且不使用MediaRecorder来做,而是通过偏向于底层数据一点的MediaCodec来自己编码视频数据。

  • 我们要实现的功能位通过Camera采集到每帧YUV原始数据
  • 录制视频后,编码YUV原始数据为H.264视频格式
  • 保存H.264编码格式的视频文件

编码YUV为H.26X编码视频格式

我们采用的都是硬编硬解的方式来完成功能,主要是熟悉API的使用。
刚开始我想要通过Camera2来编码相机预览数据为H.264文件,这其中经历了很多波折,难点在预览数据的格式转换上面。对于几个基本的数据格式的概念我们要好好的了解一下。

YUV数据

YUV通过Y,U,V三个分量表示颜色空间,Y表示亮度,UV表示色度。RGB颜色空间每个像素点都有独立的RGB三个颜色分量值。YUV却不同:
YUV根据UV采样数目的不同,分为YUV444,YUV422,YUV420等。

YUV420

表示每个像素点有一个独立的亮度表示,即Y;色度UV分量由每四个像素点共享一个。例如一个4X4的图片,在YUV420格式下,有16个Y,UV各四个。
YUV420根据UV色度的存储顺序不同,又分为不同的格式。它分为两个YUV420P和YUV420SP两个大类,YUV420P的UV顺序存储,YUV420SP的UV交错存储。以4X4的图片格式为例部分格式如下:

名称 数据存储顺序 所属大类
I420 YYYYYYYYYYYYYYYYUUUUVVVV YUV420P
YV12 YYYYYYYYYYYYYYYYVVVVUUUU YUV420P
NV12 YYYYYYYYYYYYYYYYVUVUVUVU YUV420SP
NV21 YYYYYYYYYYYYYYYYUVUVUVUV YUV420SP

编码主要代码

初始化MediaCodec

public AvcEncoder(int width, int height, int framerate, File outFile,boolean isCamera) {
        this.mIsCamera = isCamera;
        mWidth = width;
        mHeight = height;
        mFrameRate = framerate;
        mOutFile = outFile;

        MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", width, height);
        mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,
                MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar);
        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 5);
        mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
        mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
        try {
            mMediaCodec = MediaCodec.createEncoderByType("video/avc");
        } catch (IOException e) {
            e.printStackTrace();
        }
        mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        mMediaCodec.start();
        createfile();
    }

开始编码:

public void startEncoderThread() {
        Thread encoderThread = new Thread(new Runnable() {

            @SuppressLint("NewApi")
            @Override
            public void run() {
                isRunning = true;
                byte[] input = null;
                long pts = 0;
                long generateIndex = 0;

                while (isRunning) {
                    if (mYuvQueue.size() > 0) {
                        input = mYuvQueue.poll();
                        if(mIsCamera){
                            //NV21数据所需空间为如下,所以建立如下缓冲区
                            byte[] yuv420sp = new byte[mWidth * mHeight * 3 / 2];
                            NV21ToNV12(input, yuv420sp, mWidth, mHeight);
                            input = yuv420sp;
                        }else{
                            byte[] yuv420sp = new byte[mWidth * mHeight * 3 / 2];
                            YV12toNV12(input, yuv420sp, mWidth, mHeight);
                            input = yuv420sp;
                        }
                    }
                    if (input != null) {
                        try {
                            ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();

                            int inputBufferIndex = mMediaCodec.dequeueInputBuffer(-1);
                            if (inputBufferIndex >= 0) {
                                pts = computePresentationTime(generateIndex);
                                ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
                                inputBuffer.clear();
                                inputBuffer.put(input);
                                mMediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length, pts, 0);
                                generateIndex += 1;
                            }

                            ByteBuffer[] outputBuffers = mMediaCodec.getOutputBuffers();
                            MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
                            int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);

                            while (outputBufferIndex >= 0) {
                                //Log.i("AvcEncoder", "Get H264 Buffer Success! flag = "+bufferInfo.flags+",pts = "+bufferInfo.presentationTimeUs+"");
                                ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
                                byte[] outData = new byte[bufferInfo.size];
                                outputBuffer.get(outData);
                                if (bufferInfo.flags == 2) {
                                    mConfigByte = new byte[bufferInfo.size];
                                    mConfigByte = outData;
                                } else if (bufferInfo.flags == 1) {
                                    byte[] keyframe = new byte[bufferInfo.size + mConfigByte.length];
                                    System.arraycopy(mConfigByte, 0, keyframe, 0, mConfigByte.length);
                                    System.arraycopy(outData, 0, keyframe, mConfigByte.length, outData.length);
                                    outputStream.write(keyframe, 0, keyframe.length);
                                } else {
                                    outputStream.write(outData, 0, outData.length);
                                }

                                mMediaCodec.releaseOutputBuffer(outputBufferIndex, false);
                                outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);
                            }

                        } catch (Throwable t) {
                            t.printStackTrace();
                        }
                    } else {
                        try {
                            Thread.sleep(500);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        });
        encoderThread.start();
    }

停止编码

public void stopThread() {
        if (!isRunning) return;
        isRunning = false;
        try {
            stopEncoder();
            if (outputStream != null) {
                outputStream.flush();
                outputStream.close();
                outputStream = null;
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

这个使用MediaCodec的代码编码参考自这里

我这里做了一个小的区分:

  • 在使用Camera的时候,设置预览的数据格式为NV21
  • 在使用Camera2的时候,无法支持预览数据为NV21,我设置的预览数据格式YV12

设置以后为什么做如下代码处理?

if (mYuvQueue.size() > 0) {
                        input = mYuvQueue.poll();
                        if(mIsCamera){
                            //NV12数据所需空间为如下,所以建立如下缓冲区
                            //y=W*h;u=W*H/4;v=W*H/4,so total add is W*H*3/2
                            byte[] yuv420sp = new byte[mWidth * mHeight * 3 / 2];
                            NV21ToNV12(input, yuv420sp, mWidth, mHeight);
                            input = yuv420sp;
                        }else{
                            byte[] yuv420sp = new byte[mWidth * mHeight * 3 / 2];
                            YV12toNV12(input, yuv420sp, mWidth, mHeight);
                            input = yuv420sp;
                        }
                    }

可以看到我把Camera的NV21的预览数据和Camera2的YV12预览数据都转换成了NV12格式的数据。并且我在编码为H.264视频的时候图片没有出现有一点绿色在上面的情况。所以我得出这么一个结论:

H.264编码必须要用NV12,所以我们拿到预览数据要做格式转换
这个结论和我参考的一些文章的结论不同,但是我的实践代码是我的这个结论的代码成功的输出了格式良好的H.264文件,暂且认为我的结论是正确的。

ok终于搞完了这个东西,至于获取预览数据的代码就参考之前的实现了Camera有回掉很简单,Camera2通过ImageRender来获取。生成的视频通过VLC播放器清晰的显示出来了,很高兴。完整Demo在最下面贴出。

视频解码

视频的解码我感觉遇到的问题就是对于视频的格式的处理。而对于视频的MediaCodec解码我感觉我遇到的问题就是对于输出显示的视频的宽高的概念问题开始模糊了。

解码主要代码

初始化MediaCodec

public void initCodec() {
        File f = new File(mFilePath);
        if (null == f || !f.exists() || f.length() == 0) {
            Toast.makeText(mContext, "指定文件不存在", Toast.LENGTH_LONG).show();
            return;
        }
        try {
            //获取文件输入流
            mInputStream = new DataInputStream(new FileInputStream(new File(mFilePath)));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        try {
            //通过多媒体格式名创建一个可用的解码器
            mCodec = MediaCodec.createDecoderByType("video/avc");
        } catch (IOException e) {
            e.printStackTrace();
        }
        //初始化编码器
        final MediaFormat mediaformat = MediaFormat.createVideoFormat("video/avc", mVideoWidth, mVideoHeight);
        //获取h264中的pps及sps数据
        if (isUsePpsAndSps) {
            byte[] header_sps = {0, 0, 0, 1, 103, 66, 0, 42, (byte) 149, (byte) 168, 30, 0, (byte) 137, (byte) 249, 102, (byte) 224, 32, 32, 32, 64};
            byte[] header_pps = {0, 0, 0, 1, 104, (byte) 206, 60, (byte) 128, 0, 0, 0, 1, 6, (byte) 229, 1, (byte) 151, (byte) 128};
            mediaformat.setByteBuffer("csd-0", ByteBuffer.wrap(header_sps));
            mediaformat.setByteBuffer("csd-1", ByteBuffer.wrap(header_pps));
        }
        //设置帧率
        mediaformat.setInteger(MediaFormat.KEY_FRAME_RATE, mFrameRate);
        mCodec.configure(mediaformat, mSurface, null, 0);
    }

开启解码线程

public class decodeH264Thread implements Runnable {
        @Override
        public void run() {
            try {
                decodeLoop();
            } catch (Exception e) {
            }
        }

        private void decodeLoop() {
            //存放目标文件的数据
            ByteBuffer[] inputBuffers = mCodec.getInputBuffers();
            //解码后的数据,包含每一个buffer的元数据信息,例如偏差,在相关解码器中有效的数据大小
            MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
            long startMs = System.currentTimeMillis();
            long timeoutUs = 10000;
            byte[] marker0 = new byte[]{0, 0, 0, 1};
            byte[] dummyFrame = new byte[]{0x00, 0x00, 0x01, 0x20};
            byte[] streamBuffer = null;
            try {
                streamBuffer = getBytes(mInputStream);
            } catch (IOException e) {
                e.printStackTrace();
            }
            int bytes_cnt = 0;
            while (mStartFlag == true) {
                bytes_cnt = streamBuffer.length;
                if (bytes_cnt == 0) {
                    streamBuffer = dummyFrame;
                }

                int startIndex = 0;
                int remaining = bytes_cnt;
                while (true) {
                    if (remaining == 0 || startIndex >= remaining) {
                        break;
                    }
                    int nextFrameStart = KMPMatch(marker0, streamBuffer, startIndex + 2, remaining);
                    if (nextFrameStart == -1) {
                        nextFrameStart = remaining;
                    } else {
                    }

                    int inIndex = mCodec.dequeueInputBuffer(timeoutUs);
                    if (inIndex >= 0) {
                        ByteBuffer byteBuffer = inputBuffers[inIndex];
                        byteBuffer.clear();
                        byteBuffer.put(streamBuffer, startIndex, nextFrameStart - startIndex);
                        //在给指定Index的inputbuffer[]填充数据后,调用这个函数把数据传给解码器
                        mCodec.queueInputBuffer(inIndex, 0, nextFrameStart - startIndex, 0, 0);
                        startIndex = nextFrameStart;
                    } else {
                        continue;
                    }

                    int outIndex = mCodec.dequeueOutputBuffer(info, timeoutUs);
                    if (outIndex >= 0) {
                        //帧控制是不在这种情况下工作,因为没有PTS H264是可用的
                        while (info.presentationTimeUs / 1000 > System.currentTimeMillis() - startMs) {
                            try {
                                Thread.sleep(100);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        boolean doRender = (info.size != 0);
                        //对outputbuffer的处理完后,调用这个函数把buffer重新返回给codec类。
                        mCodec.releaseOutputBuffer(outIndex, doRender);
                    }
                }
                mStartFlag = false;
                mHandler.sendEmptyMessage(0);
            }
        }
    }

停止解码线程

public void stopDecodingThread() {
        mStartFlag = false;
        if (mCodec != null) {
            mCodec.stop();
            mCodec = null;
            try {
                mDecodeThread.join();
                mDecodeThread = null;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

以上H.264解码的关键代码参考自这篇博文

SurfaceView调用解码代码

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_decode_h264_to_surface_view);
        mSurfaceView = findViewById(R.id.surfaceview);
        mSurfaceView.setKeepScreenOn(true);
        SurfaceHolder holder = mSurfaceView.getHolder();
        holder.addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(SurfaceHolder holder) {
                if (mAvcDecoder == null) {
                    File file = getExternalFilesDir(Environment.DIRECTORY_MOVIES);
                    File[] files = null;
                    if (file.exists()) {
                        files = file.listFiles(new FileFilter() {
                            @Override
                            public boolean accept(File pathname) {
                                return pathname.getAbsolutePath().endsWith(".h264");
                            }
                        });
                    }

                    if (files != null && files.length > 0) {
                        mFile = files[0];
                    }

                    if (mFile == null) {
                        Toast.makeText(DecodeH264ToSurfaceViewActivity.this, "视频文件不存在,先生成", Toast.LENGTH_SHORT).show();
                        return;
                    }

                    mAvcDecoder = new AVCDecoderToSurface(mHandler,
                            DecodeH264ToSurfaceViewActivity.this, mFile.getAbsolutePath(),
                            holder.getSurface(), 1080, 1920, 30);
                    mAvcDecoder.initCodec();
                }
            }

            @Override
            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
                Log.e(TAG, "surfaceChanged: ");
            }

            @Override
            public void surfaceDestroyed(SurfaceHolder holder) {
                Log.e(TAG, "surfaceDestroyed: ");
            }
        });
        mButton = findViewById(R.id.button);
    }

问题描述

我使用SurfaceView显示解码的数据可以显示出来,但是视频是一个在竖直屏幕下先左边旋转90度的显示方式?为什么这样呢?并且我的视频发送到电脑端通过VLC显示也是一个竖直屏幕方向左边选择90的的方式播放?

出现这个问题我第一个想到的是把这个SurfaceView顺时针转90度应该就可以了吧,于是使用TextureView来显示解码的数据,关键代码如下:

private void initTextureView() {
        mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
            @Override
            public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
//                Matrix matrix = new Matrix();
//                matrix.postRotate(90, mTextureView.getWidth() / 2, mTextureView.getHeight() / 2);
//                mTextureView.setTransform(matrix);
                if (mAvcDecoder == null) {
                    File file = getExternalFilesDir(Environment.DIRECTORY_MOVIES);
                    File[] files = null;
                    if (file.exists()) {
                        files = file.listFiles(new FileFilter() {
                            @Override
                            public boolean accept(File pathname) {
                                return pathname.getAbsolutePath().endsWith(".h264");
                            }
                        });
                    }

                    if (files != null && files.length > 0) {
                        mFile = files[0];
                    }

                    if (mFile == null) {
                        Toast.makeText(DecodeH264ToTextureViewActivity.this, "视频文件不存在,先生成", Toast.LENGTH_SHORT).show();
                        return;
                    }

                    mAvcDecoder = new AVCDecoderToSurface(mHandler,
                            DecodeH264ToTextureViewActivity.this, mFile.getAbsolutePath(),
                            new Surface(surface), 1920, 1080, 30);
                    mAvcDecoder.initCodec();
                }
            }

            @Override
            public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {

            }

            @Override
            public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
                if (mAvcDecoder != null) {
                    mAvcDecoder.stopDecodingThread();
                }
                return false;
            }

            @Override
            public void onSurfaceTextureUpdated(SurfaceTexture surface) {

            }
        });
    }

以为可以见证效果,视频确实是转了90度“正”了。但是显示的宽高不对,并且严重拉扯了。怎么样的呢,图片描述一下:



我感觉这个问题出现的原因得从源来看,我上面是希望从输出端来调正。我的视频解码只从Camera的预览数据Demo测试通过。所以再回顾看看设置的Camera的相关宽高的代码

....
//设置预览尺寸onPreviewFrame的尺寸
            parameters.setPreviewSize(mBestSize.width, mBestSize.height);

            //设置拍照输出图片尺寸
            parameters.setPictureSize(mBestSize.width, mBestSize.height);

            int rotationDegrees = getCameraDisplayOrientation((Activity) mContext, mCameraId);
            Log.e(TAG, "initCamera: rotation degrees=" + rotationDegrees);
            mCamera.setDisplayOrientation(rotationDegrees);

            parameters.setPreviewFormat(ImageFormat.NV21);
....
@Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        switch (mState) {
            case STATE_PREVIEW:
                if (mAvcEncoder != null) {
                    mAvcEncoder.stopThread();
                    mAvcEncoder = null;
                    Toast.makeText(mContext, "停止录制视频成功", Toast.LENGTH_SHORT).show();
                }
                break;
            case STATE_RECORD:
                Log.e(TAG, "onPreviewFrame: record video");
                if (mAvcEncoder == null) {
                    mAvcEncoder = new AvcEncoder(mBestSize.width,
                            mBestSize.height, mFrameRate,
                            getOutputMediaFile(MEDIA_TYPE_VIDEO), true);
                    mAvcEncoder.startEncoderThread();
                    Toast.makeText(mContext, "开始录制视频成功", Toast.LENGTH_SHORT).show();
                }

                mAvcEncoder.putYUVData(data);
                break;
        }
    }

反思以上的代码,我设置了Camera的mCamera.setDisplayOrientation方法旋转90度,但是要注意的是在onPreviewFrame中每一帧的数据并没有选择90度,并且我们设置MediaCodec的宽高,它决定了我们编码的视频的文件的最终的宽高的大小。这就解释了我们的视频放到PC端的VLC上面播放是一个左边选择90度的形式来播放的。

原来如此,就是我的编码的源视频文件的每一帧的数据是在竖直方向下同Camera Sensor的数据,它要经过顺时针转90度才是我们物理世界实际看到的效果,通过代码选择一下,并且设置编码的时候的宽度和高度对掉一下,OK。完美解决我们竖直屏幕下解码播放视频的问题。

我们获取的一个最佳的宽高是宽度高于高度的,这个是通过Camera的API来获取的,而它的计量是通过Camera Sensor的左边来看的。所以我们获取并且设置的最佳的显示宽高例如这样的1920X1080的。

注意我解码的视频的源文件是通过Camera预览生成视频的Demo中来的,没有去看Camera2的预览生成的源文件的解码过程了,并且我使用的是小米5 Android7.0系统。通过Camera相关API获取到一个最佳的显示宽高大小是1920X1080,而通过Camera2相关APi获取最佳的宽高为1440X1080,在解码视频文件的时候我固定的写了解码MediaCodec的宽高为1920X1080

完整Demo:
查看
参考链接:
雷大神对于音视频编解码的总结和区别
对于YUV格式的分析
H.264视频解码参考

猜你喜欢

转载自blog.csdn.net/lyman_ye/article/details/79063069