EasyPusher安卓直播推流到EasyDarwin开源流媒体服务器工程简析

EasyPusher主要有三部分组件组成:采集,编码,叠加,上传.在这个基础上同时支持本地存储\后台预览的功能.主要业务模块与相关类之间的关系如图所示:

Created with Raphaël 2.1.0 StreamActivity StreamActivity 摄像头线程 摄像头线程 BackgroundCameraService BackgroundCameraService 编码线程 编码线程 Pusher Pusher Muxer Muxer 音频线程 音频线程 音频编码线程 音频编码线程 TxtOverlay TxtOverlay 显示在主界面 后台预览 提供摄像头数据 发送编码后的数据 本地存储 创建\销毁音频(依附于视频线程) 发送编码后的数据 本地存储 提供水印叠加

创建

首先,创建摄像头.这部分代码在MediaStream.java文件,接口为:createCamera:

public void createCamera() {
    mCamera = Camera.open(mCameraId);
    ...
    // 这里设置摄像头参数,设置分辨率,显示方向等
}

创建成功后,再开启摄像头预览.预览的同时做一些初始化工作,初始化编码库和字幕叠加库

/**
 * 开启预览
 */
public synchronized void startPreview() {
    if (mCamera != null) {

        mSWCodec = PreferenceManager.getDefaultSharedPreferences(mApplicationContext).getBoolean("key-sw-codec", false);
        if (mSWCodec) {     // 初始化软编码库
            mMuxer = null;
            mVC = new SWConsumer(mApplicationContext, mEasyPusher);
        } else {            // 初始化硬编码库
            ...             // 创建Muxer.
            mVC = new HWConsumer(mApplicationContext, mEasyPusher);
        }
        ... // 设置视频帧回调,设置显示的holder,开启预览

        ... // 创建叠加库并初始化
    }
    // 同时启动音频线程
    audioStream = new AudioStream(mEasyPusher);
    audioStream.startRecord();
}

视频数据通过onPreviewFrame回调上来,如果需要的话,我们在这里对它做水印叠加:

@Override
public void onPreviewFrame(byte[] data, Camera camera) {
    // 
    if (PreferenceManager.getDefaultSharedPreferences(mApplicationContext).getBoolean("key_enable_video_overlay", false)) {
        // 叠加字幕
        String txt = String.format("drawtext=fontfile=" + mApplicationContext.getFileStreamPath("SIMYOU.ttf") + ": text='%s%s':x=(w-text_w)/2:y=H-60 :fontcolor=white :box=1:boxcolor=0x00000000@0.3", "EasyPusher", new SimpleDateFormat("yyyy-MM-ddHHmmss").format(new Date()));
        txt = "EasyPusher " + new SimpleDateFormat("yy-MM-dd HH:mm:ss SSS").format(new Date());
        overlay.overlay(data, txt);
    }
    // 将数据塞给给编码器
    mVC.onVideo(data, previewFormat);
    mCamera.addCallbackBuffer(data);
}

编码与推送

EasyPusher支持硬编码和软编码.硬编码用MediaCodec来实现的,软编码用X264编码库实现的.分别对应类HWConsumer和SWConsumer.

硬编码的初始化

在startPreview时,调用硬编码的onVideoStart回调,这里进行硬编码的初始化.

    @Override
    public void onVideoStart(int width, int height) throws IOException {
        ...
        startMediaCodec();
        ...
        start();
        mVideoStarted = true;
    }

在这里调用startMediaCodec来创建MediaCodec:

/**
     * 初始化编码器
     */
    private void startMediaCodec() throws IOException {
        ...
        mMediaCodec = MediaCodec.createByCodecName(debugger.getEncoderName());
        MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", mWidth, mHeight);
        ...
        mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        mMediaCodec.start();
    }

在视频数据回调的时候,将视频帧塞入硬编码器进行编码.

    @Override
    public int onVideo(byte[] data, int format) {
        if (!mVideoStarted)return 0;
        // 视频数据预处理.包括format转换\旋转等
        if (format == ImageFormat.YV12 ) {
            JNIUtil.yV12ToYUV420P(data, mWidth, mHeight);
        }else{
            JNIUtil.nV21To420SP(data, mWidth, mHeight);
        }

        int bufferIndex = mMediaCodec.dequeueInputBuffer(0);
        ... // 视频数据塞入编码器.
        // 下面代码用来控制帧率
        if (time > 0) Thread.sleep(time / 2);
        return 0;
    }

同时,编码器线程会持续取走编码后的264格式的数据.并根据情况进行存储和推送.

@Override
    public void run(){
        do {
            outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 10000);
            ... // 一些返回值判断语句
            ByteBuffer outputBuffer;
            .. // outputBuffer为从编码器取出的编码后的数据
            outputBuffer.position(bufferInfo.offset);
            outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
            .. // muxer录像的处理逻辑
            // 下面是获取sps pps数据.
            boolean sync = false;
            if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {// sps
                sync = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_SYNC_FRAME) != 0;
                if (!sync) {
                    byte[] temp = new byte[bufferInfo.size];
                    outputBuffer.get(temp);
                    mPpsSps = temp;
                    mMediaCodec.releaseOutputBuffer(outputBufferIndex, false);
                    continue;
                } else {
                    mPpsSps = new byte[0];
                }
            }
            // 如果是关键帧,那么把sps pps拷贝到关键帧前面.
            if (sync) {
                System.arraycopy(mPpsSps, 0, h264, 0, mPpsSps.length);
                outputBuffer.get(h264, mPpsSps.length, bufferInfo.size);
                mPusher.push(h264, 0, mPpsSps.length + bufferInfo.size, bufferInfo.presentationTimeUs / 1000, 1);
            }else{
                outputBuffer.get(h264, 0, bufferInfo.size);
                mPusher.push(h264, 0, bufferInfo.size, bufferInfo.presentationTimeUs / 1000, 1);
                if (BuildConfig.DEBUG)
                    Log.i(TAG, String.format("push video stamp:%d", bufferInfo.presentationTimeUs / 1000));
            }
            // 释放buffer.
            mMediaCodec.releaseOutputBuffer(outputBufferIndex, false);

        }
        while (mVideoStarted);
    }

停止

停止预览时,关闭摄像头,同时进行反初始化音频线程\编码器\muxer\TxtOverlay:

public synchronized void stopPreview() {
        if (mCamera != null) {
            mCamera.stopPreview();
            mCamera.setPreviewCallbackWithBuffer(null);
        }
        if (audioStream != null) {
            audioStream.stop();
            audioStream = null;
        }
        if (mVC != null)
            mVC.onVideoStop();
        if (overlay != null)
            overlay.release();

        if (mMuxer != null) {
            mMuxer.release();
            mMuxer = null;
        }
    }

在stopPreview时,编码器进行反初始化:

/**
 * 停止编码并释放编码资源占用
 */
private void stopMediaCodec() {
    mMediaCodec.stop();
    mMediaCodec.release();
}

软编码的处理

软编码是通过x264进行编码的.
首先进行初始化,创建编码器,同时开启编码线程:

@Override
public void onVideoStart(int width, int height) {
    this.mWidth = width;
    this.mHeight = height;

    x264 = new X264Encoder();
    int bitrate = (int) (mWidth*mHeight*20*2*0.07f);
    x264.create(width, height, 20, bitrate/500);
    mVideoStarted = true;
    start();
}

同硬编码一致,视频数据通过onVideo回调给编码器,这里将视频数据附加上时间戳,并缓存到队列里.
这里用到了双缓冲逻辑.yuv_caches是一个空闲的yuv buffer池,如果yuv buffer池里面没有buffer,则创建新的buffer,拷贝数据,然后放到队列里.
否则在yuv buffer池里取出buffer,拷贝数据,再放到队列里.

 @Override
public int onVideo(byte[] data, int format) {
    try {
        .. // 帧率控制相关
        // 这里用到了双缓冲逻辑.yuv_caches是一个空闲的yuv buffer池,如果yuv buffer池里面没有buffer,则创建新的buffer,拷贝数据,在放到队列里.
        // 否则在yuv buffer池里取出buffer,拷贝数据,再放到队列里.
        byte[] buffer = yuv_caches.poll();
        if (buffer == null || buffer.length != data.length) {
            buffer = new byte[data.length];
        }
        // 拷贝数据,放到yuv队列中
        System.arraycopy(data, 0, buffer, 0, data.length);
        yuvs.offer(new TimedBuffer(buffer));
    }catch (InterruptedException ex){
        ex.printStackTrace();
    }
    return 0;
}

接下来,编码线程从队列取出yuv数据,进行编码.编码结束后将yuv数据回收到yuv buffer池

@Override
    public void run(){
        do {
            // 从队列中取出yuv
            TimedBuffer tb = yuvs.take();
            // 编码
            r = x264.encode(data, 0, h264, 0, outLen, keyFrm);
            // 将yuv数据放到缓冲池
            yuv_caches.offer(data);
            // 推送
            mPusher.push(h264, 0, outLen[0], tb.time, 1);
        }while (mVideoStarted);
    }

在stopPreview时,对软编码器进行反初始化:

@Override
public void onVideoStop() {
    ...// 停止编码线程
    if (x264 != null) {
        // 关闭编码器
        x264.close();
    }
    x264 = null;
}

通过持续的编码\推送数据,就可实现视频传输的功能.
EasyPusher现在已在GitHub上开源.地址:https://github.com/EasyDarwin/EasyPusher_Android

通过EasyPusher我们就可以避免接触到稍显复杂的RTSP/RTP/RTCP推送流程,只需要调用EasyPusher的几个API接口,就能轻松、稳定地把流媒体音视频数据推送给RTSP流媒体服务器进行转发和分发,EasyPusher经过长时间的企业用户检验,稳定性非常高;

下载地址

EasyPusher_Android

EasyPusher_iOS

获取更多信息

邮件:support@easydarwin.org

WEB:www.EasyDarwin.org

QQ交流群:587254841

Copyright © EasyDarwin.org 2012-2017

EasyDarwin

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值