极速简单实现Android 屏幕录制编码为H264并且使用RTMP推流

最近有使用到屏幕录制功能并需要将屏幕数据推送到服务器端。采用RTMP推送,MediaCodec编码,MediaProjection 获取屏幕数据。

(新版本已经上传架构优化加录制音频+camer直播,请直接查看github源码,博文就不修改了。基于rtmp传输,mediacodec编码,流媒体服务器是SRS,播放器是基于ijk的直播播放器。有需要帮忙搭建的可以直接留言)

demo地址:github地址喜欢请点个start

1 屏幕+camer+mediacodec :新地址

   在Android5.0 后可以采用原生的API MediaProjection 来获取一个virtual的display 从而实现屏幕录制。

我们第一步就是要先把屏幕数据拿出来

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void creatVirtualDisPlay(){

        Log.d(TAG, "created virtual display: " +this.mediaProjection);
       VirtualDisplay mVirtualDisplay = mediaProjection.createVirtualDisplay(TAG + "-display",
               width, heigiht, 1, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,
                inputSurface, null, null);
        Log.d(TAG, "created virtual display: " + mVirtualDisplay);

    }

​​​​​​2.数据编码

   在Android 4.1 后我们可以采用原生的MediaCodec 进行编码也就是我们常说的硬件编码。

 @SuppressLint("NewApi")
    private void prepareEncoder()  {
        try {
            String MIME_TYPE = "video/avc";
            MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, width, heigiht);

            format.setInteger(MediaFormat.KEY_COLOR_FORMAT,MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
            format.setInteger(MediaFormat.KEY_BIT_RATE, 1024*1000);
            format.setInteger(MediaFormat.KEY_FRAME_RATE, 30); // 设置帧率
            format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
            format.setInteger(MediaFormat.KEY_BITRATE_MODE,MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ);

            Log.d("chenzhu", "created video format: " + format);
            mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);
            mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
            inputSurface = mEncoder.createInputSurface();
            Log.d(TAG, "created input surface: " + inputSurface);
            mEncoder.start();
        }catch (Exception e){
            Log.d("chenzhu", "created  encoder fail" );
        }

    }

3.数据处理

将编码后的数据打包封装成FLV数据

 @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    private void recordVirtualDisplay() {
        while (true) {
            int eobIndex = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_US);
            switch (eobIndex) {
                case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
                    Log.d(TAG,"VideoSenderThread,MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED");
                    break;
                case MediaCodec.INFO_TRY_AGAIN_LATER:
//                     Log.d(TAG,"VideoSenderThread,MediaCodec.INFO_TRY_AGAIN_LATER");
                    break;
                case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
                    Log.d(TAG,"VideoSenderThread,MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:" + mEncoder.getOutputFormat().toString());
                    sendAVCDecoderConfigurationRecord(0, mEncoder.getOutputFormat());
                    break;
                default:
                    Log.d(TAG,"VideoSenderThread,MediaCode,eobIndex=" + eobIndex);
                    if (startTime == 0) {
                        startTime = mBufferInfo.presentationTimeUs / 1000;
                    }
                    /**
                     * we send sps pps already in INFO_OUTPUT_FORMAT_CHANGED
                     * so we ignore MediaCodec.BUFFER_FLAG_CODEC_CONFIG
                     */
                    if (mBufferInfo.flags != MediaCodec.BUFFER_FLAG_CODEC_CONFIG && mBufferInfo.size != 0) {
                        ByteBuffer realData = mEncoder.getOutputBuffers()[eobIndex];
                        realData.position(mBufferInfo.offset + 4);
                        realData.limit(mBufferInfo.offset + mBufferInfo.size);
                        sendRealData((mBufferInfo.presentationTimeUs / 1000) - startTime, realData);
                    }
                    mEncoder.releaseOutputBuffer(eobIndex, false);
                    break;
            }
        }
    }





    private void sendAVCDecoderConfigurationRecord(long tms, MediaFormat format) {
        byte[] AVCDecoderConfigurationRecord = Packager.H264Packager.generateAVCDecoderConfigurationRecord(format);
        int packetLen = Packager.FLVPackager.FLV_VIDEO_TAG_LENGTH +
                AVCDecoderConfigurationRecord.length;
        byte[] finalBuff = new byte[packetLen];
        Packager.FLVPackager.fillFlvVideoTag(finalBuff,
                0,
                true,
                true,
                AVCDecoderConfigurationRecord.length);
        System.arraycopy(AVCDecoderConfigurationRecord, 0,
                finalBuff, Packager.FLVPackager.FLV_VIDEO_TAG_LENGTH, AVCDecoderConfigurationRecord.length);
        RESFlvData resFlvData = new RESFlvData();
        resFlvData.droppable = false;
        resFlvData.byteBuffer = finalBuff;
        resFlvData.size = finalBuff.length;
        resFlvData.dts = (int) tms;
        resFlvData.flvTagType = FLV_RTMP_PACKET_TYPE_VIDEO;
        resFlvData.videoFrameType = RESFlvData.NALU_TYPE_IDR;
        //        TODO send
        RtmpClient.write(jniRtmpPointer, resFlvData.byteBuffer, resFlvData.byteBuffer.length, resFlvData.flvTagType, resFlvData.dts);
    }

    private void sendRealData(long tms, ByteBuffer realData) {
        int realDataLength = realData.remaining();
        int packetLen = Packager.FLVPackager.FLV_VIDEO_TAG_LENGTH +
                Packager.FLVPackager.NALU_HEADER_LENGTH +
                realDataLength;
        byte[] finalBuff = new byte[packetLen];
        realData.get(finalBuff, Packager.FLVPackager.FLV_VIDEO_TAG_LENGTH +
                        Packager.FLVPackager.NALU_HEADER_LENGTH,
                realDataLength);
        int frameType = finalBuff[Packager.FLVPackager.FLV_VIDEO_TAG_LENGTH +
                Packager.FLVPackager.NALU_HEADER_LENGTH] & 0x1F;
        Packager.FLVPackager.fillFlvVideoTag(finalBuff,
                0,
                false,
                frameType == 5,
                realDataLength);
        RESFlvData resFlvData = new RESFlvData();
        resFlvData.droppable = true;
        resFlvData.byteBuffer = finalBuff;
        resFlvData.size = finalBuff.length;
        resFlvData.dts = (int) tms;
        resFlvData.flvTagType = FLV_RTMP_PACKET_TYPE_VIDEO;
        resFlvData.videoFrameType = frameType;
//        TODO send
        RtmpClient.write(jniRtmpPointer, resFlvData.byteBuffer, resFlvData.byteBuffer.length, resFlvData.flvTagType, resFlvData.dts);
    }

4 集成rtmp包并且发送到服务器端

我们采用rtmp推送 对rtmp进行封装

extern "C"
JNIEXPORT jlong JNICALL
Java_com_example_administrator_mypush_RtmpClient_open(JNIEnv *env, jclass type, jstring url_,
                                                      jboolean isPublishMode) {
    const char *url = (env)->GetStringUTFChars( url_, 0);
    LOGD("RTMP_OPENING:%s",url);
    RTMP* rtmp = RTMP_Alloc();
    if (rtmp == NULL) {
        LOGD("RTMP_Alloc=NULL");
        return NULL;
    }

    RTMP_Init(rtmp);
    int ret = RTMP_SetupURL(rtmp, const_cast<char *>(url));

    if (!ret) {
        RTMP_Free(rtmp);
        rtmp=NULL;
        LOGD("RTMP_SetupURL=ret");
        return NULL;
    }
    if (isPublishMode) {
        RTMP_EnableWrite(rtmp);
    }

    ret = RTMP_Connect(rtmp, NULL);
    if (!ret) {
        RTMP_Free(rtmp);
        rtmp=NULL;
        LOGD("RTMP_Connect=ret");
        return NULL;
    }
    ret = RTMP_ConnectStream(rtmp, 0);

    if (!ret) {
        ret = RTMP_ConnectStream(rtmp, 0);
        RTMP_Close(rtmp);
        RTMP_Free(rtmp);
        rtmp=NULL;
        LOGD("RTMP_ConnectStream=ret");
        return NULL;
    }
    (env)->ReleaseStringUTFChars( url_, url);
    LOGD("RTMP_OPENED");
    return reinterpret_cast<jlong>(rtmp);
}extern "C"
JNIEXPORT jint JNICALL
Java_com_example_administrator_mypush_RtmpClient_write(JNIEnv *env, jclass type_, jlong rtmpPointer,
                                                       jbyteArray data_, jint size, jint type,
                                                       jint ts) {
    LOGD("start write");
    jbyte *buffer = (env)->GetByteArrayElements( data_, NULL);
    RTMPPacket *packet = (RTMPPacket*)malloc(sizeof(RTMPPacket));
    RTMPPacket_Alloc(packet, size);
    RTMPPacket_Reset(packet);
    if (type == RTMP_PACKET_TYPE_INFO) { // metadata
        packet->m_nChannel = 0x03;
    } else if (type == RTMP_PACKET_TYPE_VIDEO) { // video
        packet->m_nChannel = 0x04;
    } else if (type == RTMP_PACKET_TYPE_AUDIO) { //audio
        packet->m_nChannel = 0x05;
    } else {
        packet->m_nChannel = -1;
    }

    packet->m_nInfoField2  =  ((RTMP*)rtmpPointer)->m_stream_id;

    LOGD("write data type: %d, ts %d", type, ts);

    memcpy(packet->m_body,  buffer,  size);
    packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
    packet->m_hasAbsTimestamp = FALSE;
    packet->m_nTimeStamp = ts;
    packet->m_packetType = type;
    packet->m_nBodySize  = size;
    int ret = RTMP_SendPacket((RTMP*)rtmpPointer, packet, 0);
    RTMPPacket_Free(packet);
    free(packet);
    (env)->ReleaseByteArrayElements( data_, buffer, 0);
    if (!ret) {
        LOGD("end write error %d", ret);
        return ret;
    }else
    {
        LOGD("end write success");
        return 0;
    }

以上就是整体的思路

最后附上效果图:

大概有1s左右的延时

优化后的ijk播放器可以达到100ms

ijk播放优化地址:ijk播放器优化到100ms 思路

云手机同步效果

  • 7
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 28
    评论
Android平台上实现RTMP推流可以通过使用第三方的库或者自己编写相关代码来实现。下面是一个简单的步骤来实现Android平台上的RTMP推流: 1. 导入第三方库:首先,需要将第三方库添加到Android项目中。目前较为常用的第三方库有librtmpffmpeg等。 2. 初始化推流参数:在开始推流之前,需要初始化相关的推流参数,例如RTMP服务器地址、推流地址等。可以通过设置参数为其赋值,确保推流的正确性。 3. 创建推流线程:为了避免在主线程中执行推流操作导致界面卡顿,可以在新的线程中执行推流操作。可以通过创建一个推流线程来实现。 4. 连接RTMP服务器:使用已经设置好的RTMP服务器地址,建立与服务器的连接。连接成功后即可开始推流。 5. 采集视频、音频:通过Android平台提供的相应API,可以采集相机的视频数据和麦克风的音频数据。可以使用Camera和MediaRecorder类来进行视频的采集和编码使用AudioRecord类来进行音频的采集和编码。 6. 推流:将采集到的视频、音频数据进行编码后,使用RTMP协议将数据发送给服务器。可以使用librtmp库提供的接口或者使用第三方库提供的特定接口来实现推流操作。 7. 结束推流:当推流完成或者需要停止推流时,需要释放相关资源并断开与RTMP服务器的连接。 需要注意的是,实现RTMP推流的过程中需要根据具体需求来设置相应的配置并处理异常情况。同时,还需要对Android相机、音频等操作有一定的了解,并进行适当的错误处理和资源管理。 以上是一种简单实现RTMP推流的方式,具体实现可能涉及的内容较多,还需根据具体的项目需求进行相应的调整和优化。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 28
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值