Android RTMP录频直播二(录屏H264视频编码)

一、录屏H264编码简单流程
录屏视频H264编码.png
1. H264编码配置类
public class VideoUtils {
    // 360*640(1000_000) 540*960(高清 1500_000) 720*1280(超清 1800_000) 1080*1920(蓝光 2500_000)
    // MIMETYPE_VIDEO_AVC   MIMETYPE_VIDEO_MPEG4
    public static final String MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_AVC;
    public static final int DECODE_HEIGHT = 1080;
    public static final int DECODE_WIDTH = 1920;
    public static final int BIT_RATE = 2500_000; // 码率
    // 码率控制方式 CQP(恒定质量),CRF(恒定码率),VBR(平均码率)。 VBR好一些,不会出现花点
    public static final int BITRATE_MODE = MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR;
    public static final int FRAME_RATE = 30; // I帧刷新频率
    public static final int I_FRAME_INTERVAL = 1; // 每秒刷新
}
2. 录屏H264编码代码
/**
 * H264视频编码
 * MediaProjection(录屏数据) + MediaCodec H264编码
 */
public class MediaVideoEncoder implements Runnable {
    private static final String MIME_TYPE = VideoUtils.MIME_TYPE;
    private AtomicBoolean isStart = new AtomicBoolean(false);
    private MediaProjection mediaProjection;
    private MediaCodec mediaCodec;
    private Surface surface;
    // 录频相关
    private VirtualDisplay virtualDisplay;
    private byte[] sps;
    private byte[] pps;
    private OnFrameListener onFrameListener;
    private int timeoutUs = 30;


    public MediaVideoEncoder(MediaProjection mediaProjection) {
        this.mediaProjection = mediaProjection;
        initMediaCode();
    }

    private void initMediaCode() {
        try {
            MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, VideoUtils.DECODE_WIDTH, VideoUtils.DECODE_HEIGHT);
            // 码率控制方式 CQP(恒定质量),CRF(恒定码率),VBR(平均码率)。 VBR好一些,不会出现花点
            format.setInteger(MediaFormat.KEY_BITRATE_MODE, VideoUtils.BITRATE_MODE);
            format.setInteger(MediaFormat.KEY_BIT_RATE, VideoUtils.BIT_RATE);
            format.setInteger(MediaFormat.KEY_FRAME_RATE, VideoUtils.FRAME_RATE);
            format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, VideoUtils.I_FRAME_INTERVAL);
            format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
            mediaCodec = MediaCodec.createEncoderByType(MIME_TYPE);
            mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
            surface = mediaCodec.createInputSurface();
            // 录频关联MediaCodec
            virtualDisplay = mediaProjection.createVirtualDisplay(
                    "VirtualDisplay", VideoUtils.DECODE_WIDTH, VideoUtils.DECODE_HEIGHT, 1,
                    DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,
                    surface, null, null);
        } catch (Exception e) {
            e.printStackTrace();
            isStart.set(false);
        }
    }

    public void start() {
        if (isStart.compareAndSet(false, true)) {
            Executors.newSingleThreadExecutor().execute(this);
        }
    }

    public void stop() {
        isStart.set(false);
    }

    @Override
    public void run() {
        if (mediaCodec == null) {
            Log.e("TAG", "initMediaCode 失败...");
            return;
        }
        Log.e("TAG", "start h264 encode...");
        mediaCodec.start();
        MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
        // 计算PTS
        // DTS表示解码时间戳,在什么时候解码这一帧的数据.
        // PTS表示显示时间戳 ,在什么时候显示这一帧。
        long pts = 0;
        while (isStart.get()) {
            int index = mediaCodec.dequeueOutputBuffer(info, timeoutUs);

            if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                // sps pps
                MediaFormat outputFormat = mediaCodec.getOutputFormat();
                ByteBuffer csd0 = outputFormat.getByteBuffer("csd-0");
                ByteBuffer csd1 = outputFormat.getByteBuffer("csd-1");
                if (csd1 != null) {
                    sps = new byte[csd0.remaining()];
                    pps = new byte[csd1.remaining()];
                    csd0.get(sps, 0, csd0.remaining());
                    csd1.get(pps, 0, csd1.remaining());
                }
            } else if (index >= 0) {
                // 其它帧
                ByteBuffer buffer = mediaCodec.getOutputBuffer(index);
                buffer.position(info.offset);
                buffer.limit(info.size);
                if (pts == 0) {
                    pts = info.presentationTimeUs;
                }
                info.presentationTimeUs -= pts;
                // 处理帧数据
                frameData(info, buffer, info.presentationTimeUs);
                mediaCodec.releaseOutputBuffer(index, false);
            }
        }
        // 回收
        release();
        Log.e("TAG", "stop h264 encode...");
    }

    // 回收
    private void release() {
        if (mediaCodec != null) {
            mediaCodec.stop();
            mediaCodec.release();
        }
        if (virtualDisplay != null) {
            virtualDisplay.release();
        }
        if (mediaProjection != null) {
            mediaProjection.stop();
        }
        mediaCodec = null;
        virtualDisplay = null;
        mediaProjection = null;
        onFrameListener = null;
        sps = null;
        pps = null;
    }


    private void frameData(MediaCodec.BufferInfo info, ByteBuffer buffer, long time) {
        // I B P及其它帧
        byte[] data = new byte[info.size];
        buffer.get(data);
        int type = info.flags;
        // 1. 如果是I帧就发sps pps
        if (type == MediaCodec.BUFFER_FLAG_KEY_FRAME) {
            // 1. 发个sps pps
            if (sps != null && pps != null) {
                if (onFrameListener != null) {
                    onFrameListener.onSpsPpsListener(type, sps, sps.length, pps, pps.length);
                }
            }
        }
        // 2. 发帧数据
        if (onFrameListener != null) {
            onFrameListener.onFameListener(type, data, data.length, time);
        }
    }

    public interface OnFrameListener {
        //  sps pps 回调
        void onSpsPpsListener(int type, byte[] sps, int spsLen, byte[] pps, int ppsLen);

        // I B P 帧回调 type = 1 时是I帧
        void onFameListener(int type, byte[] data, int len, long time);
    }

    public void setOnFrameListener(OnFrameListener onFrameListener) {
        this.onFrameListener = onFrameListener;
    }
}
  1. 使用测试类
// 录屏推流类
public class RtmpPushActivity extends AppActivity implements ProjectionObserver, XPermissionListener {
    private ProjectionUtils mProjectionUtils;
    private MediaVideoEncoder mVideoEncoder;

    @Override
    protected void initTitle(DefTitleBar titleBar) {
        titleBar.setTitle("RtmpPush");
    }

    @Override
    public int getContentLayout() {
        return R.layout.activity_push_rtmp;
    }

    @Override
    public void initData(Bundle savedInstanceState) {
        // 录屏帮助类
        mProjectionUtils = new ProjectionUtils();
        // 创建录屏MediaProjection类成功回调
        ProjectionObservable.getInstance().register(this);
    }

    // 1. 开启录屏申请
    public void start(View view) {
        // 1. 方法调用后,到->onActivityResult
        mProjectionUtils.start(this);
    }

    // 2.处理录屏申请
    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
    // 2. 处理录屏申请 -> 到onChange(MediaProjection mediaProjection)
        mProjectionUtils.onActivityResult(requestCode, resultCode, data);
    }

    // 3. 录屏创建MediaProjection成功回调
    @Override
    public void onChange(MediaProjection mediaProjection) {
        if (mVideoEncoder != null){
        mVideoEncoder.stop();
    }
    // 录屏H264编码
        mVideoEncoder = new MediaVideoEncoder(mediaProjection);
    // 开启录屏编码
    mVideoEncoder.start();
    }
    // 4.停止录频
    public void stop(View view) {
        if (mProjectionUtils != null) {
            mProjectionUtils.stop();
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mProjectionUtils != null) {
            mProjectionUtils.stop();
        }
    if (mVideoEncoder != null){
        mVideoEncoder.stop();
    }
        // 反注册录屏回调
        ProjectionObservable.getInstance().unregister(this);
    }

}
4.补充说明
  1. MediaProjection 这个类是屏幕服务创建的。具体请看Android RTMP录频直播一(录屏)
  2. MediaCodec进行H264编码时,sps pps只会编一次,得保存起来,在每次I帧发送前,先发sps pps,否则播放端解码时拿不到sps pps,播放不了视频。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,843评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,538评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,187评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,264评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,289评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,231评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,116评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,945评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,367评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,581评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,754评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,458评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,068评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,692评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,842评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,797评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,654评论 2 354

推荐阅读更多精彩内容