美文网首页
android rtmp+opensl es+opengl es

android rtmp+opensl es+opengl es

作者: 有心人2021 | 来源:发表于2021-10-11 19:25 被阅读0次

    观看一些户外直播时,我们观众端看到的是主播摄像头的内容,这是如何实现的呢?这篇将手写一个直播Demo。
    在上一篇中,可以拍摄camera的数据,并加上背景音乐,其实只要解决了如何推流到服务器就可以了。我们使用 Rtmp 来传输 Rtmp Packet 数据,需要用到 NDK 开发。
    基本流程

    • 获取camera和录音数据(byte[])
    • 对数据进行 h264 编码
    • 封装Rtmp 数据包
    • 上传到直播服务器推流地址


    一.前期准备

    1. 因为要用到推流服务器,所以需要自己自行搭建流媒体服务器,可以参照这篇,使用Nginx+rtmp搭建流媒体服务器 - 简书 (jianshu.com),需要对linux懂一些常识。

    2. 至于服务器,最开始想用vmware的网络转发来对外,然后手机连接使用,但是发现电脑上可以ping通,但是手机上ping不了,还是得买个带公网的云服务器,翻了下,腾讯云有轻量云服务,首年几十块钱挺合适,自己搭建做些探索性的工作够了。

    3.配置cmakeList
    需要加入rtmp包



    然后配置配置cmakeList

    # 添加 define  -DNO_CRYPTO,在c文件可使用,
    #1. CMAKE_C_FLAGS介绍:https://cloud.tencent.com/developer/article/1433578
    #2. define介绍:https://blog.csdn.net/chouhuan1877/article/details/100808689
    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DNO_CRYPTO")
    #或者使用 add_definitions(-DTEST_DEBUG),这样在cxx_flags,c_flags都有
    
    AUX_SOURCE_DIRECTORY(${CMAKE_SOURCE_DIR} SRC_LIST)
    AUX_SOURCE_DIRECTORY(${CMAKE_SOURCE_DIR}/librtmp RTMP_LIST)
    
    add_library( # Sets the name of the library.
                 live-push
    
                 # Sets the library as a shared library.
                 SHARED
    
                 ${RTMP_LIST}
    
                 ${SRC_LIST}
    
                 # Provides a relative path to your source file(s).
                native-lib.cpp
                DZPacketQueue.cpp
                DZJNICall.cpp
                DZLivePush.cpp
                )
    

    要注意,-DNO_CRYPTO得加上,要不然编译不通过,这是一个变量,和define的全局变量有点像,NO_CRYPTO = ture。

    二.代码

    视频推流

    在上一篇基础上,VideoEncoderThread中加入打印代码,就可以看到视频的数据,可以先行打印看下,然后对照着sps、pps、I、P等帧的类型,推的流其实就是遵守一定协议的,一串二进制码。

       // 返回有效数据填充的输出缓冲区的索引
        int outputBufferIndex = mVideoCodec.dequeueOutputBuffer(bufferInfo,0);
        if(outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED){
            //TODO
    
            ByteBuffer byteBuffer = mVideoCodec.getOutputFormat().getByteBuffer("csd-0");
            mVideoSps = new byte[byteBuffer.remaining()];
            byteBuffer.get(mVideoSps,0,mVideoSps.length);
    
            String videoData = parseByte2HexStr(mVideoPps);
            Log.e(TAG+" sps",videoData);
    
            byteBuffer = mVideoCodec.getOutputFormat().getByteBuffer("csd-1");
            mVideoPps = new byte[byteBuffer.remaining()];
            byteBuffer.get(mVideoPps,0,mVideoPps.length);
    
            videoData = parseByte2HexStr(mVideoSps);
            Log.e(TAG+" pps",videoData);
        }
    
    // 获取数据
        ByteBuffer outBuffer = mVideoCodec.getOutputBuffers()[outputBufferIndex];
    
        outBuffer.position(bufferInfo.offset);
        outBuffer.limit(bufferInfo.offset+bufferInfo.size);
    
        // 修改视频的 pts,基准时间戳
        if(videoPts ==0)
            videoPts = bufferInfo.presentationTimeUs;
        bufferInfo.presentationTimeUs -= videoPts;
    
    
        byte[] mVideoBytes = new byte[outBuffer.remaining()];
        outBuffer.get(mVideoBytes,0,mVideoBytes.length);
        String v1 = parseByte2HexStr(mVideoBytes);
        Log.e(TAG+":",v1);
    

    可以看到结果:



    都是以00000001开头,然后跟着类型码及数据。最开始的为sps、pps打头。
    41十六进制转为二进制为1000001,对照下表6-7位,可以看到为P帧。
    65十六进制转为二进制为1100101,为I帧。
    每隔30个P帧,为一个I帧。


    VideoEncoderThread整体改造后如下:

     private class VideoEncoderThread extends Thread{
    
            WeakReference<BaseVideoPush> videoRecorderWf;
            private boolean shouldExit =false;
    
            private MediaCodec mVideoCodec;
            MediaCodec.BufferInfo bufferInfo;
            CyclicBarrier stopCb;
            long videoPts = 0;
    
            /**
             * 视频轨道
             */
            private int mVideoTrackIndex = -1;
    
            byte[] mVideoPps;
            byte[] mVideoSps;
    
            public VideoEncoderThread(WeakReference<BaseVideoPush> videoRecorderWf){
                this.videoRecorderWf = videoRecorderWf;
                this.mVideoCodec = videoRecorderWf.get().mVideoCodec;
                this.stopCb = videoRecorderWf.get().stopCb;
                bufferInfo = new MediaCodec.BufferInfo();
    
            }
    
            @Override
            public void run() {
                mVideoCodec.start();
    
                while (true){
                    try {
                        if(shouldExit){
                            onDestroy();
                            return;
                        }
    
                            // 返回有效数据填充的输出缓冲区的索引
                            int outputBufferIndex = mVideoCodec.dequeueOutputBuffer(bufferInfo,0);
                            if(outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED){
                                //TODO
    
                                ByteBuffer byteBuffer = mVideoCodec.getOutputFormat().getByteBuffer("csd-0");
                                mVideoSps = new byte[byteBuffer.remaining()];
                                byteBuffer.get(mVideoSps,0,mVideoSps.length);
    
                                String videoData = parseByte2HexStr(mVideoPps);
                                Log.e(TAG+" pps",videoData);
    
                                byteBuffer = mVideoCodec.getOutputFormat().getByteBuffer("csd-1");
                                mVideoPps = new byte[byteBuffer.remaining()];
                                byteBuffer.get(mVideoPps,0,mVideoPps.length);
    
                                videoData = parseByte2HexStr(mVideoSps);
                                Log.e(TAG+" sps",videoData);
                            }else {
                                while (outputBufferIndex >= 0){
    
                                    if(bufferInfo.flags == MediaCodec.BUFFER_FLAG_KEY_FRAME){
                                        videoRecorderWf.get().livePush.pushSpsPps(mVideoSps,mVideoSps.length,mVideoPps,mVideoPps.length);
                                        String v1 = parseByte2HexStr(mVideoSps);
                                        Log.e(TAG+"sps data:",v1);
                                        String v2 = parseByte2HexStr(mVideoPps);
                                        Log.e(TAG+"pps data:",v2);
                                    }
    
                                    // 获取数据
                                    ByteBuffer outBuffer = mVideoCodec.getOutputBuffers()[outputBufferIndex];
    
                                    outBuffer.position(bufferInfo.offset);
                                    outBuffer.limit(bufferInfo.offset+bufferInfo.size);
    
                                    // 修改视频的 pts,基准时间戳
                                    if(videoPts ==0)
                                        videoPts = bufferInfo.presentationTimeUs;
                                    bufferInfo.presentationTimeUs -= videoPts;
    
    
                                    byte[] mVideoBytes = new byte[outBuffer.remaining()];
                                    outBuffer.get(mVideoBytes,0,mVideoBytes.length);
                                    String v1 = parseByte2HexStr(mVideoBytes);
                                    Log.e(TAG+":",v1);
    
                                    videoRecorderWf.get().livePush.pushVideo(mVideoBytes,mVideoBytes.length,
                                            bufferInfo.flags == MediaCodec.BUFFER_FLAG_KEY_FRAME);
    
    
                                    if(videoRecorderWf.get().recordInfoListener != null){
                                        // us,需要除以1000转为 ms
                                        videoRecorderWf.get().recordInfoListener.onTime( bufferInfo.presentationTimeUs / 1000);
                                    }
    
                                    // 释放 outBuffer
                                    mVideoCodec.releaseOutputBuffer(outputBufferIndex,false);
                                    outputBufferIndex = mVideoCodec.dequeueOutputBuffer(bufferInfo,0);
                                }
                        }
                    } catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }
    
            private void onDestroy() {
                try {
                    if (mVideoCodec != null){
                        mVideoCodec.stop();
                        mVideoCodec.release();
                        mVideoCodec = null;
                    }
    
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
    
            public void requestExit() {
                shouldExit = true;
            }
        }
    

    视频的数据分为sps、pps,还有i帧及p帧,需要sps、pps的数据先推上去

    videoRecorderWf.get().livePush.pushSpsPps(mVideoSps,mVideoSps.length,mVideoPps,mVideoPps.length)
    

    这里调用JNI的代码
    先把几个数据帧的结构发一下



    更细一点的结构如下,也是在代码里要用到的:

     // frame type : 1关键帧,2 非关键帧 (4bit)
        // CodecID : 7表示 AVC (4bit)  , 与 frame type 组合起来刚好是 1 个字节  0x17
        // fixed : 0x00 0x00 0x00 0x00 (4byte)
        // configurationVersion  (1byte)  0x01版本
        // AVCProfileIndication  (1byte)  sps[1] profile
        // profile_compatibility (1byte)  sps[2] compatibility
        // AVCLevelIndication    (1byte)  sps[3] Profile level
        // lengthSizeMinusOne    (1byte)  0xff   包长数据所使用的字节数
    
        // sps + pps 的数据
        // sps number            (1byte)  0xe1   sps 个数
        // sps data length       (2byte)  sps 长度
        // sps data                       sps 的内容
        // pps number            (1byte)  0x01   pps 个数
        // pps data length       (2byte)  pps 长度
        // pps data                       pps 的内容
    

    因此,可以编写

    void DZLivePush::pushSpsPps(jbyte* sps_data, jint sps_length, jbyte* pps_data, jint pps_length) {
    
        // frame type : 1关键帧,2 非关键帧 (4bit)
        // CodecID : 7表示 AVC (4bit)  , 与 frame type 组合起来刚好是 1 个字节  0x17
        // fixed : 0x00 0x00 0x00 0x00 (4byte)
        // configurationVersion  (1byte)  0x01版本
        // AVCProfileIndication  (1byte)  sps[1] profile
        // profile_compatibility (1byte)  sps[2] compatibility
        // AVCLevelIndication    (1byte)  sps[3] Profile level
        // lengthSizeMinusOne    (1byte)  0xff   包长数据所使用的字节数
    
        // sps + pps 的数据
        // sps number            (1byte)  0xe1   sps 个数
        // sps data length       (2byte)  sps 长度
        // sps data                       sps 的内容
        // pps number            (1byte)  0x01   pps 个数
        // pps data length       (2byte)  pps 长度
        // pps data                       pps 的内容
    
        int bodySize = sps_length + pps_length + 16;
        RTMPPacket* rtmpPacket =  (RTMPPacket *) malloc(sizeof(RTMPPacket));
        RTMPPacket_Alloc(rtmpPacket,bodySize);
        RTMPPacket_Reset(rtmpPacket);
    
        int index = 0;
        char* body = rtmpPacket->m_body;
        //标识位 sps pps,AVC sequence header 与IDR一样
        body[index++] = 0x17;
    
        //跟着的补齐
        body[index++] = 0x00;
        body[index++] = 0x00;
        body[index++] = 0x00;
        body[index++] = 0x00;
    
        //版本
        body[index++] = 0x01;
    
        //编码规格
        body[index++] = sps_data[1];
        body[index++] = sps_data[2];
        body[index++] = sps_data[3];
        // reserved(111111) + lengthSizeMinusOne(2位 nal 长度) 总是0xff
        body[index++] = 0xff;
        // reserved(111) + lengthSizeMinusOne(5位 sps 个数) 总是0xe1
        body[index++] = 0xe1;
    
        //sps length 2字节
        body[index++] = (sps_length >> 8) & 0xff; //第0个字节
        body[index++] = sps_length & 0xff; //第1个字节
        // sps data
        memcpy(&body[index], sps_data, sps_length);
        index += sps_length;
    
        //pps
        body[index++] = 0x01;
        body[index++] = (pps_length >> 8) & 0XFF;
        body[index++] = pps_length & 0xFF;
    
        memcpy(&body[index], pps_data, pps_length);
    
        rtmpPacket->m_hasAbsTimestamp = 0;
        rtmpPacket->m_nTimeStamp = 0;
        rtmpPacket->m_nBodySize = bodySize;
        rtmpPacket->m_packetType = RTMP_PACKET_TYPE_VIDEO;
        rtmpPacket->m_headerType = RTMP_PACKET_SIZE_LARGE;
        rtmpPacket->m_nChannel = 0x04;
        rtmpPacket->m_nInfoField2 = rtmp->m_stream_id;
    
    //    LOGE("sps pps 发送到dzPacketQueue");
        dzPacketQueue->push(rtmpPacket);
    }
    

    推送video的I帧、P帧如下

    void DZLivePush::pushVideo(jbyte *videoByte, jint length,jboolean isKeyFrame) {
    
        // frame type : 1关键帧,2 非关键帧 (4bit)
        // CodecID : 7表示 AVC (4bit)  , 与 frame type 组合起来刚好是 1 个字节  0x17
        // fixed : 0x01 0x00 0x00 0x00 (4byte)  0x01  表示 NALU 单元
    
        // video data length       (4byte)  video 长度
        // video data
        // 数据的长度(大小) =  dataLen + 9
        int bodySize = 9+length;
        RTMPPacket* packet = (RTMPPacket *)(malloc(sizeof(RTMPPacket)));
        RTMPPacket_Alloc(packet,bodySize);
        RTMPPacket_Reset(packet);
    
        int index = 0;
        char* body = packet->m_body;
        // frame type : 1关键帧,2 非关键帧 (4bit)
        // CodecID : 7表示 AVC (4bit)  , 与 frame type 组合起来刚好是 1 个字节  0x17
    
        if(isKeyFrame)
            body[index++] =0x17;
        else
            body[index++] =0x27;
    
        body[index++] =0x01;
        body[index++] =0x00;
        body[index++] =0x00;
        body[index++] =0x00;
    
        body[index++] =(length >> 24) & 0xFF;
        body[index++] =(length >> 16) & 0xFF;
        body[index++] =(length >> 8) & 0xFF;
        body[index++] =length & 0xFF;
    
        memcpy(&body[index],videoByte,length);
    
        packet->m_nBodySize = bodySize;
        packet->m_nChannel = 0x04;
    
        packet->m_nTimeStamp = RTMP_GetTime() - startTime;
        packet->m_hasAbsTimestamp = 0;
        packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
        packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
        packet->m_nInfoField2 = rtmp->m_stream_id;
    
    //    LOGE("I P 发送到dzPacketQueue");
        dzPacketQueue->push(packet);
    }
    

    这些都是固定写法,packet->m_nChannel=0x04,音频推送时也需要04,要不音频推送写个05,会发现无法正常播放声音。

    音频推流

    现在推音频流,使用AudioRecord 采集音频数据

      bufferSizeInBytes = AudioRecord.getMinBufferSize(
                        AUDIO_SAMPLE_RATE,
                        AudioFormat.CHANNEL_IN_STEREO,
                        AudioFormat.ENCODING_PCM_16BIT);
    
                audioRecord = new AudioRecord(
                        MediaRecorder.AudioSource.MIC,
                        AUDIO_SAMPLE_RATE,
                        AudioFormat.CHANNEL_IN_STEREO,
                        AudioFormat.ENCODING_PCM_16BIT,
                        bufferSizeInBytes);
                mAdudioData = new byte[bufferSizeInBytes];
    
         //开始采集
         audioRecord.startRecording();
         //录取录制的数据
         audioRecord.read(mAdudioData,0,bufferSizeInBytes);
    

    因为原始的音频文件如采样率之类的,可能不是我们规定的,这里推到mediacodec中,再拿到数据推流

        audioRecord.read(mAdudioData,0,bufferSizeInBytes);
        
        int inputBufferTrack = mAudioCodec.dequeueInputBuffer(0);
        if(inputBufferTrack >= 0){
            ByteBuffer inputBuffer = mAudioCodec.getInputBuffers()[inputBufferTrack];
            inputBuffer.clear();
        
            inputBuffer.put(mAdudioData);
        
            //0.41795918 *1000 000
            audioPts += 1000000 * bufferSizeInBytes * 1.0f / AUDIO_SAMPLE_RATE * AUDIO_CHANNELS * 2;
            //数据放入mAudioCodec的队列中
            mAudioCodec.queueInputBuffer(inputBufferTrack,0,bufferSizeInBytes,audioPts,0);
        }
    

    和视频一样的取数据

     mAudioCodec.start();
    
                while (true){
                    try {
                        if(shouldExit){
                            onDestroy();
                            return;
                        }
                        // 返回有效数据填充的输出缓冲区的索引
                        int outputBufferIndex = mAudioCodec.dequeueOutputBuffer(bufferInfo,0);
    
                        while (outputBufferIndex >= 0){
    
    //                            Log.e(TAG,"outputBufferIndex:"+outputBufferIndex+" count:"+index);
                            // 获取数据
                            ByteBuffer outBuffer = mAudioCodec.getOutputBuffers()[outputBufferIndex];
    
                            outBuffer.position(bufferInfo.offset);
                            outBuffer.limit(bufferInfo.offset+bufferInfo.size);
    
                            // 修改视频的 pts,基准时间戳
                            if(audioPts ==0)
                                audioPts = bufferInfo.presentationTimeUs;
                            bufferInfo.presentationTimeUs -= audioPts;
    
                            byte[] audioData = new byte[outBuffer.remaining()];
                            outBuffer.get(audioData,0,audioData.length);
    
                            recorderReference.get().livePush.pushAudio(audioData,audioData.length);
    
                            // 释放 outBuffer
                            mAudioCodec.releaseOutputBuffer(outputBufferIndex,false);
                            outputBufferIndex = mAudioCodec.dequeueOutputBuffer(bufferInfo,0);
                        }
                    } catch (Exception e){
                        e.printStackTrace();
                    }
                }
    

    RTMP 包中封装的音视频数据流,其实和FLV/tag封装音频和视频数据的方式是相同的,所以我们只需要按照FLV格式封装音视频即可。


    void DZLivePush::pushAudio(jbyte *audioData, jint audioLen) {
    
    
        // 2 字节头信息
        // 前四位表示音频数据格式 AAC  10(A)
        // 五六位表示采样率 0 = 5.5k  1 = 11k  2 = 22k  3(11) = 44k
        // 七位表示采样采样的精度 0 = 8bits  1 = 16bits
        // 八位表示音频类型  0 = mono  1 = stereo
        // 组合起来:1010 1111 -,算出来第一个字节是 0xAF
        // 0x01 代表 aac 原始数据
        int bodySize = audioLen+2;
        RTMPPacket* packet = (RTMPPacket *)(malloc(sizeof(RTMPPacket)));
        RTMPPacket_Alloc(packet,bodySize);
        RTMPPacket_Reset(packet);
    
        char * body = packet->m_body;
        //上面推算出
        body[0] = 0xaf;
    
        body[1] = 0x01;
    
        memcpy(&body[2],audioData,audioLen);
    
        packet->m_packetType = RTMP_PACKET_TYPE_AUDIO;
        packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
        packet->m_nInfoField2 = rtmp->m_stream_id;
        packet->m_hasAbsTimestamp = 0;
        packet->m_nTimeStamp = RTMP_GetTime()-startTime;
        packet->m_nChannel = 0x04;
        packet->m_nBodySize = bodySize;
    
        LOGE("AAC 发送到dzPacketQueue");
    
        dzPacketQueue->push(packet);
    }
    

    这样就可以了。

    停止

    因为是开启的线程推流到服务器

    void DZLivePush::initConnect() {
        pthread_create(&initConnectTid,NULL, initConnectRun,this);
    }
    void *initConnectRun(void * context){
      //不断循环取数据上传到服务器
    ...
      while (pLivePush->isPushing){
    
            RTMPPacket* packet = pLivePush->dzPacketQueue->pop();
            if(packet != NULL){
                int send_result = RTMP_SendPacket(pLivePush->rtmp,packet,1);
                LOGE("send_result: %d",send_result);
    
                RTMPPacket_Free(packet);
                free(packet);
                packet = NULL;
            }
        }
    ...
    }
    
    

    所以加一个退出标识,然后pthread_join等待线程完成退出。

    void DZLivePush::stop() {
        isPushing = false;
        pthread_join(initConnectTid,NULL);
        LOGE("等待停止");
    }
    

    这样代码就写完了。

    验证

    可以使用下载的
    Builds - CODEX FFMPEG @ gyan.dev
    ffmpeg for windows,使用ffplay rtmp://自己的流媒体IP:1935/cctvf/mystream来播放了。

    代码在这里:livepush at github

    参考:

    1. (2条消息) H.264再学习 -- 详解 H.264 NALU语法结构_不积跬步,无以至千里-CSDN博客_h

    2. Android RTMP 投屏直播推流实现.md · 苦涩冰糖/myBlogMarkdown - Gitee.com

    3. (2条消息) librtmp发送AVC,AAC数据包_影音视频技术-CSDN博客

    相关文章

      网友评论

          本文标题:android rtmp+opensl es+opengl es

          本文链接:https://www.haomeiwen.com/subject/gyvloltx.html