EasyPusher安卓Android手机直播推送之MediaCodec 硬编码H264格式

来源:互联网 发布:网络电视如何看直播 编辑:程序博客网 时间:2024/05/16 13:01

最近在研究EasyDarwin的Push库EasyPusher,EasyPusher可以推送H264视频到EasyDarwin服务器,终端可以通过rtsp协议访问该实时流,达到手机直播的功能,延迟基本在2秒以内。 

EasyDarwinQQ群:496258327 
本文主要记录一下最近研究的关于Android手机如何获取实时画面,并将数据编码为H264的格式的视频流,编码使用的是Android自带的MediaCodec,也就是硬解。 
本demo的下载地址:MediaCodecDemo

MediaCodec是Android在4.1中加入的新的API,目前也有很多文章介绍MediaCodec的用法,但是很多时候很多手机都失败,主要问题出现在调用dequeueOutputBuffer的时候总是返回-1,让你以为No buffer available !这里介绍一个开源项目libstreaming,我们借助此项目中封装的一个工具类EncoderDebugger,来初始化MediaCodec会很好的解决此问题,目前为止测试了几个手机都可以成功,包括小米华为Moto。 
看一下怎么使用的

EncoderDebugger debugger = EncoderDebugger.debug(getApplicationContext(), width, height);MediaCodec mMediaCodec = MediaCodec.createByCodecName(debugger.getEncoderName());
  • 1
  • 2

嗯,就这样。当然了,后面还是要根据需要对mMediaCodec设置其他参数的,看一下本demo中设置参数的过程吧

private void initMediaCodec() {    int dgree = getDgree();    framerate = 15;    bitrate = 2 * width * height * framerate / 20;    EncoderDebugger debugger = EncoderDebugger.debug(getApplicationContext(), width, height);    mConvertor = debugger.getNV21Convertor();    try {        mMediaCodec = MediaCodec.createByCodecName(debugger.getEncoderName());        MediaFormat mediaFormat;        if (dgree == 0) {            //dree==0的时候,需要将画面旋转90度,所以这里编码的时候需要将宽和高颠倒,            //否则编码后的会面会出现四重画面并且花屏            mediaFormat = MediaFormat.createVideoFormat("video/avc", height, width);        } else {            mediaFormat = MediaFormat.createVideoFormat("video/avc", width, height);        }        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);        mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, framerate);        mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,                debugger.getEncoderColorFormat());        mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);        mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);        mMediaCodec.start();    } catch (IOException e) {        e.printStackTrace();    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

编码之前先看一下要编码的数据怎么获取吧,这个当然是来自Camera。 
首先是创建SurfaceView用于预览视频画面,并设置回调,来监控生命周期。

surfaceView = (SurfaceView) findViewById(R.id.sv_surfaceview);surfaceView.getHolder().addCallback(this);surfaceView.getHolder().setFixedSize(getResources().getDisplayMetrics().widthPixels,getResources().getDisplayMetrics().heightPixels);
  • 1
  • 2
  • 3
  • 4

然后是创建Camera的方法:

private boolean ctreateCamera(SurfaceHolder surfaceHolder) {    try {        //mCameraId=Camera.CameraInfo.CAMERA_FACING_BACK        mCamera = Camera.open(mCameraId);        Camera.Parameters parameters = mCamera.getParameters();        Camera.CameraInfo camInfo = new Camera.CameraInfo();        Camera.getCameraInfo(mCameraId, camInfo);        int cameraRotationOffset = camInfo.orientation;        //设置预览格式NV21,他属于YUV420SP        parameters.setPreviewFormat(ImageFormat.NV21);        parameters.setPreviewSize(width, height);        mCamera.setParameters(parameters);        mCamera.autoFocus(null);        //计算preview画面需要旋转的角度。目前木有做横竖屏切换的时候无缝旋转画面,后面再搞。        int  displayRotation = (cameraRotationOffset - getDgree() + 360) % 360;        mCamera.setDisplayOrientation(displayRotation);        mCamera.setPreviewDisplay(surfaceHolder);        return true;    } catch (Exception e) {        destroyCamera();        e.printStackTrace();        return false;    }}private int getDgree() {    int rotation = getWindowManager().getDefaultDisplay().getRotation();    int degrees = 0;    switch (rotation) {        case Surface.ROTATION_0:            degrees = 0;            break; // Natural orientation        case Surface.ROTATION_90:            degrees = 90;            break; // Landscape left        case Surface.ROTATION_180:            degrees = 180;            break;// Upside down        case Surface.ROTATION_270:            degrees = 270;            break;// Landscape right    }    return degrees;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

摄像头创建完毕,就是开启预览

/** * 开启预览 */public synchronized void startPreview() {    if (mCamera != null && !started) {        mCamera.startPreview();        int previewFormat = mCamera.getParameters().getPreviewFormat();        Camera.Size previewSize = mCamera.getParameters().getPreviewSize();        int size = previewSize.width * previewSize.height                * ImageFormat.getBitsPerPixel(previewFormat)                / 8;        mCamera.addCallbackBuffer(new byte[size]);        mCamera.setPreviewCallbackWithBuffer(previewCallback);        started = true;        btnSwitch.setText("停止");    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

上面就是设置了预览回调的方式,回调中将预览画面一帧一帧的返回给我们,给我们的数据就是NV21格式的,根据需要决定是否需要对数据进行旋转,旋转之后,就是转换,将NV21数据转为YUV420P格式的数据,然后就可以编码为H264数据了。

Camera.PreviewCallback previewCallback = new Camera.PreviewCallback() {    //mSpsPps用来存储sps pps数据,后面遇到关键帧(I帧),必须将spspps数据加到I帧前面    byte[] mSpsPps = new byte[0];    @Override    public void onPreviewFrame(byte[] data, Camera camera) {        if (data == null) {            return;        }        ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();        ByteBuffer[] outputBuffers = mMediaCodec.getOutputBuffers();        byte[] dst = new byte[data.length];        Camera.Size previewSize = mCamera.getParameters().getPreviewSize();        if (getDgree() == 0) {            //手机竖屏的时候要将获取的数据顺时针旋转90度,否则画面不是正着的,而是逆时针90度            dst = Util.rotateNV21Degree90(data, previewSize.width, previewSize.height);        } else {            dst = data;        }        try {            int bufferIndex = mMediaCodec.dequeueInputBuffer(5000000);            if (bufferIndex >= 0) {                inputBuffers[bufferIndex].clear();                //将YUV420SP数据转换成YUV420P的格式,并将结果存入inputBuffers[bufferIndex]                mConvertor.convert(dst, inputBuffers[bufferIndex]);                mMediaCodec.queueInputBuffer(bufferIndex, 0,                        inputBuffers[bufferIndex].position(),                        System.nanoTime() / 1000, 0);                MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();                int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0);                while (outputBufferIndex >= 0) {                    ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];                    byte[] outData = new byte[bufferInfo.size];                    //从buff中读取数据到outData中                    outputBuffer.get(outData);                    //记录pps和sps,pps和sps数据开头是0x00 0x00 0x00 0x01 0x67,                    // 0x67对应十进制103                    if (outData[0] == 0 && outData[1] == 0 && outData[2] == 0                             && outData[3] == 1 && outData[4] == 103) {                        mSpsPps = outData;                    } else if (outData[0] == 0 && outData[1] == 0 && outData[2] == 0                             && outData[3] == 1 && outData[4] == 101) {                        //关键帧开始规则是0x00 0x00 0x00 0x01 0x65,0x65对应十进制101                        //在关键帧前面加上pps和sps数据                        byte[] iframeData = new byte[mSpsPps.length + outData.length];                        System.arraycopy(mSpsPps, 0, iframeData, 0, mSpsPps.length);                        System.arraycopy(outData, 0, iframeData, mSpsPps.length, outData.length);                        outData = iframeData;                    }                    //至此,这一帧的数据已经经过MediaCodec编码完毕,这个outData就是我们需要的数据了,                    //因为EasyDarwin可以自动将H264打包为RTP,                    //所以EasyPusher只需要负责将outData推给EasyDarwin就OK了                    //保存H264数据到本地文件easy.h264                    Util.save(outData, 0, outData.length, path, true);                    mMediaCodec.releaseOutputBuffer(outputBufferIndex, false);                    outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0);                }            } else {                Log.e("easypusher", "No buffer available !");            }        } catch (Exception e) {            StringWriter sw = new StringWriter();            PrintWriter pw = new PrintWriter(sw);            e.printStackTrace(pw);            String stack = sw.toString();            Log.e("save_log", stack);            e.printStackTrace();        } finally {            mCamera.addCallbackBuffer(dst);        }    }};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71

保存之后的文件easy.h264我用VLC播放器打开,截屏如下: 
easy264截屏 
OK,基本上完毕了,该注意的地方都写在代码中了 
需要Demo的请到这里https://github.com/kidloserme/MediaCodecDemo

获取更多信息

邮件:support@easydarwin.org

WEB:www.EasyDarwin.org

Copyright © EasyDarwin.org 2012-2016

本文转自Holo的博客:

http://blog.csdn.net/u013758734/article/details/50834770

http://blog.csdn.net/xiejiashu/article/details/51154259


阅读全文
0 0
原创粉丝点击