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

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

创建

首先,创建摄像头.这部分代码在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经过长时间的企业用户检验,稳定性非常高;

下载地址

获取更多信息

邮件:support@easydarwin.org

WEB:www.EasyDarwin.org

QQ交流群:587254841

Copyright © EasyDarwin.org 2012-2017

EasyPusher安卓直播推流到EasyDarwin开源流媒体服务器工程简析的更多相关文章

  1. ffmpeg强制使用TCP方式推流到EasyDarwin开源流媒体服务器进行直播

    我们的EasyDarwin目前部署在阿里云的服务器上面,运行的效果是非常好的,而且无论是以TCP方式.还是UDP的方式推送,都可以非常好地进行直播转发: 但并不是所有的用户服务器都是阿里云的形式,有很 ...

  2. EasyDarwin开源流媒体服务器实现RTSP直播同步输出MP4、RTMP、HLS的方案思路

    背景 近期跟开源团队商量,想在EasyDarwin上继续做一些功能扩展,目前EasyDarwin开源流媒体服务器只能够实现高效的RTSP推流直播转发/分发功能,输入与输出都是RTSP/RTP流,不能够 ...

  3. 解决用EasyDarwin开源流媒体服务器做HLS直播时Flash Player卡住的问题

    最近在开发EasyDarwin开源流媒体服务器HLS直播的时候发现一个现象:在PC上用flash player播放HLS和在ios上面播放HLS时,效果明显不同,在ios上播放非常稳定,而在flash ...

  4. EasyDarwin开源流媒体服务器Golang版本:服务端录像功能发布

    EasyDarwin开源流媒体服务器(www.easydarwin.org)现在使用Go版本实现了.最新的代码提交,已经支持了推流(或者拉流)的同时进行本地存储. 本地存储的原理,是在推流的同时启动f ...

  5. NodeJS版本EasyDarwin开源流媒体服务器开发心得

    title: Node版本EasyDarwin开发心得 date: 2018-03-27 22:46:15 tags: 年后着手Node版本EasyDarwin的开发工作,截止到今天2018年03月2 ...

  6. EasyDarwin开源流媒体服务器Golang版本:拉转推功能之拉流实现方法

    EasyDarwin开源流媒体服务器(www.easydarwin.org),拉转推是一个很有意义的功能,它可将一个独立的RTSP数据源"拉"到服务器,再通过转发协议转发给多个客户 ...

  7. EasyDarwin开源流媒体服务器性能优化之Work-stealing优化方案

    本文转自EasyDarwin开源团队成员Alex的博客:http://blog.csdn.net/cai6811376/article/details/52400226 EasyDarwin团队的Ba ...

  8. EasyDarwin开源流媒体服务器将select改为epoll的方法

    本文来自EasyDarwin团队Fantasy(fantasy(at)easydarwin.org) 一. EasyDarwin网络模型介绍 EventContext负责监听所有网络读写事件,Even ...

  9. EasyDarwin开源流媒体服务器提供的TS切片/HLS直播打包库

    EasyHLS  Github:https://github.com/EasyDarwin/EasyHLS EasyHLS是什么? EasyHLS是EasyDarwin开源流媒体社区开发的一款HLS打 ...

随机推荐

  1. 20145307第三次JAVA学习实验报告

    20145307 <Java程序设计>第三次实验报告 北京电子科技学院(BESTI)实验报告 课程:Java程序设计 班级:1453 指导教师:娄嘉鹏 实验日期:2016.04.22 实验 ...

  2. journalctl 工具使用

    在Systemd出现之前,Linux系统及各应用的日志都是分别管理的,Systemd开始统一管理了所有Unit的启动日志,这样带来的好处就是可以只用一个 journalctl命令,查看所有内核和应用的 ...

  3. Java ArrayList在foreach中remove的问题分析

    目录 iterator itr.hasNext 和 itr.next 实现 倒数第二个元素的特殊 如何避坑 都说ArrayList在用foreach循环的时候,不能add元素,也不能remove元素, ...

  4. 无法读取服务器服务中的服务器队列性能数据。数据段的第一个四字节 (DWORD) 中包

    无法打开服务器服务性能对象.数据段的第一个四字节 (DWORD) 包含状态代码. 解决方法:修改注册表禁用PerfNet性能计数器. 具体方法:打开注册表,在HKEY_LOCAL_MACHINE\SY ...

  5. jQuery获取属性值的方法

    1.利用绑定事件:     $(".callback").on("click","#knbh",function(){      ***** ...

  6. 08_MySQL DQL_SQL99标准中的多表查询(内连接)

    # sql99语法/*语法: select 查询列表 from 表1 别名 [连接类型] join 表2 别名 on 连接条件 [where 筛选条件] [group by 分组] [having 分 ...

  7. linux下增加useradd提示existing lock file /etc/subgid.lock without a PID

    # useradd git -g git useradd: existing lock file /etc/subgid.lock without a PID useradd: cannot lock ...

  8. git开发错分支

    1,代码未提交时: 使用以下命令即可解决. git add .      (把所有改动暂存) git stash     (把暂存的文件提交到git的暂存栈) git checkout 本该提交代码的 ...

  9. 开机启动服务(ftp、apache、mysql)

    当linux服务器开机时,会将 /etc/rc.d/rc.local 中的指令全部执行一遍, 因此将相应服务的启动指令放到该shell脚本中即可实现开机启动效果; 在 /etc/rc.d/rc.loc ...

  10. 百度地图API学习总结

    常用技术   1.创建地图: var map = new BMap.Map("divid"); 2.创建坐标点:var point = new BMap.Point("经 ...