目录

一、MediaCodec编码音频

创建音频编码器,指定AAC格式,采样率44100,码率64_000,单声道;

创建AudioRecord录音对象,设置参数与编码器对应;

启动编码器和录音器;

循环从录音器中读取PCM格式的byte数组,放入编码器的输入队列;

循环从编码器的输入队列中读取数据,获得编码好的AAC格式的byte数组,等待后续rtmp封包用。

二、MediaCodec编码视频

申请录屏权限,获取MediaProjection对象

创建视频编码器

将编码器的surface传给MediaProjection对象构建虚拟显示

循环从编码器的输出队列读取数据

三、H264裸流格式分析

四、FLV封装格式分析

FLV文件总体格式:

FLV文件的详细结构。

音频Tag Data格式:

视频Tag Data格式分析:

五、利用librtmp封包发送

librtmp的工作流程:

视频编码器中添加:

音频编码器中添加:

利用librtmp连接流:

音频封包:

视频封包:

1.判断视频类型,是否为AVC序列头:

2.获取SPS和PPS信息:

3.封装AVC序列头信息:

4.封装普通NAL单元信息:

利用librtmp发包:

参考文章链接:

Demo下载链接:


一、MediaCodec编码音频

  • 创建音频编码器,指定AAC格式,采样率44100,码率64_000,单声道;

            //创建编码器
            MediaFormat mediaFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC,
                    44100, 1);
            //编码规格,可以看成质量
            mediaFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, 
                    MediaCodecInfo.CodecProfileLevel.AACObjectLC);
            //码率
            mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 64_000);
            mediacodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
            mediacodec.configure(mediaFormat,null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
  • 创建AudioRecord录音对象,设置参数与编码器对应;

            //创建录音对象
            minBufferSize = AudioRecord.getMinBufferSize(44100, 
                    AudioFormat.CHANNEL_IN_MONO,
                    AudioFormat.ENCODING_PCM_16BIT);
            audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, 44100,
                    AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, minBufferSize);
  • 启动编码器和录音器;

  • 循环从录音器中读取PCM格式的byte数组,放入编码器的输入队列;

            //从录音器读取数据
            readIndex = audioRecord.read(buffer, 0, 1024);

            if (readIndex <= 0){
                continue;
            }
            //获取编码器输入队列的可用位置下标
            inputBufferIndex = mediacodec.dequeueInputBuffer(0);
            if (inputBufferIndex >= 0) {
                //获取输入队列
                inputBuffer = mediacodec.getInputBuffer(inputBufferIndex);
                inputBuffer.clear();//避免缓冲队列有冗余数据,先clear一下
                Log.d("zhangdi", "length = " + buffer.length + " readIndex=" + readIndex);
                inputBuffer.put(buffer);//将需要编码的数据放入队列缓冲区
                //将缓冲队列放回编码器
                mediacodec.queueInputBuffer(inputBufferIndex, 0, readIndex,
                        System.nanoTime() / 1000, 0);
            }
  • 循环从编码器的输入队列中读取数据,获得编码好的AAC格式的byte数组,等待后续rtmp封包用。

            //获取编码器输入队列的可用下标位置,10为等待超时时间
            outputBufferIndex = mediacodec.dequeueOutputBuffer(bufferInfo, 10);
            while (outputBufferIndex >= 0 && isRecording) {
                //获取输出缓冲区
                outputBuffer = mediacodec.getOutputBuffer(outputBufferIndex);
                outBuffer = new byte[bufferInfo.size];
                //获取编码好的AAC数据
                outputBuffer.get(outBuffer);

                //rtmp封包
                

                //释放缓冲区
                mediacodec.releaseOutputBuffer(outputBufferIndex, false);
                outputBufferIndex = mediacodec.dequeueOutputBuffer(bufferInfo, 10);
            }

二、MediaCodec编码视频

  • 申请录屏权限,获取MediaProjection对象

     /**
     * 开始直播
     * @param url 直播服务器地址
     * @param context 接收录屏请求回调的activity
     */
    public void startLive(String url, Activity context){

        this.url = url;
        mediaProjectionManager = (MediaProjectionManager) context.
                getSystemService(Context.MEDIA_PROJECTION_SERVICE);
        Intent intent = mediaProjectionManager.createScreenCaptureIntent();
        context.startActivityForResult(intent, 101);
    }

    /**
     * 屏幕录制权限申请后处理,返回结果,成功则开启屏幕推流,否则返回false
     * @param requestCode
     * @param resultCode
     * @param data
     * @return true 授权成功开启推流
     *         false 授权失败
     */
    public boolean onActivityResult(int requestCode, int resultCode, @Nullable Intent data){
        if (requestCode == 101 && resultCode == Activity.RESULT_OK){

            mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data);
            LiveExecutors.getInstance().excute(this);
            return true;
        }else {
            return false;
        }

    }
  • 创建视频编码器

       指定AVC格式(对应H264),宽高使用360*640,码率采用400_000(宽高和码率的设置为经验值,这里参考腾讯云实时音视频给出的参考值),帧率15fps,关键帧帧间间隔2s,本地原始视频格式采用MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface(录屏输出的格式);

  • 将编码器的surface传给MediaProjection对象构建虚拟显示

       也就是让录屏画面通过编码器的surface进入编码器的输入队列;

MediaFormat mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC,360, 640);
        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 400_000);//设置比特率
        mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 15);//帧率
        mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2);//关键帧间隔
        mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
        try {
            mediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
            mediaCodec.configure(mediaFormat,null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
            Surface surface = mediaCodec.createInputSurface();
            
            //将编码器的surface传给mediaProjection
            virtualDisplay = mediaProjection.createVirtualDisplay("screen-vd",
                    360, 640, 1,
                    DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,surface,null,null);

            start();//启动线程
        } catch (IOException e) {
            e.printStackTrace();
        }

 

  • 循环从编码器的输出队列读取数据

       获得编码好的AVC格式的byte数组,留待后续rtmp封包使用。

    while (isEncoding){

            //编码器指定的关键帧间隔可能失效,所以需要手动2s刷新一下关键帧
            if (lastTimeTamp != 0){
                if (System.currentTimeMillis() - lastTimeTamp > 2000) {//每2秒强制刷新关键帧
                    Bundle bundle = new Bundle();
                    bundle.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0);
                    mediaCodec.setParameters(bundle);
                    lastTimeTamp = System.currentTimeMillis();
                }
            } else {
                lastTimeTamp = System.currentTimeMillis();
            }

            //获取输出获取冲可用下标,timeoutUs是从队列中取数据的超时时间,这是个阻塞方法,如果超时则不继续等待
            int index = mediaCodec.dequeueOutputBuffer(bufferInfo, 10);
            if (index >= 0) {
                //获取输出缓冲区队列
                ByteBuffer byteBuffer = mediaCodec.getOutputBuffer(index);
                byte[] bytes = new byte[bufferInfo.size];
                //从输出缓冲区队列读取数据
                byteBuffer.get(bytes);

                //获得了编码好的数据,封装出rmpt视频包

                //释放,让队列中index位置能放新数据
                mediaCodec.releaseOutputBuffer(index, false);
            }
        }

三、H264裸流格式分析

此时我们就获取到了AAC音频裸流和H264视频裸流。H264码流在网络中传输时实际是以NALU形式传输的,NALU就是NAL UNIT,NAL单元。NAL全称是NetWork Abstract Layer,即网络抽象层。我们此时得到的编码数据就是NAL数据。

一段包含了N个图像的H264裸数据,每个NAL之间由:

00 00 00 01 或者 00 00 01进行分割。

在分隔符之后的第一个字节就是表示这个NAL的类型:

0x67 : sps

0x68 : pps

0x65 : IDR首个关键帧

分析这些是因为,我们后续往RTMP包中封装的视频数据就是NAL数据,但是需要去掉每个NAL单元之间的分隔符,且根据NAL类型的不同,封装的数据格式也有区别。

为了更加形象的解释NAL单元我将获取到的AVC视频byte数组转换为16进制打印出来了,大家可以看一下。

四、FLV封装格式分析

FLV(Flash Video)是Adobe公司设计开发的一种流行的流媒体格式,由于其视频文件体积轻巧、封装简单等特点,使其很适合在互联网上进行应用。此外,FLV可以使用Flash Player进行播放,而Flash Player插件已经安装在全世界绝大部分浏览器上,这使得通过网页播放FLV视频十分容易。目前主流的视频网站如优酷网,土豆网,乐视网等网站无一例外地使用了FLV格式。FLV封装格式的文件后缀通常为“.flv”。

FLV文件总体格式:

FLV包括文件头(File Header)和文件体(File Body)两部分,其中文件体由一系列的Tag组成。因此一个FLV文件是如图结构。

我们主要关心Tag就可以,因为我们RTMP封包的数据就是由FLV中的Tag数据构成的。每个Tag前面还包含了Previous Tag Size字段,表示前面一个Tag的大小。Tag的类型可以是视频、音频和Script,每个Tag只能包含以上三种类型的数据中的一种。下图展示了

FLV文件的详细结构。

我们主要就分析音频类型的Tag和视频类型的Tag,不涉及script data类型。两种类的header结构相同,所以下面主要分析他们的Tag data格式。

音频Tag Data格式:

音频Tag开始的第1个字节包含了音频数据的参数信息,从第2个字节开始为音频流数据即我们从编码器获取的音频流数据。

第一个字节参数信息分析:

前四位的音频编码类型如图所示:

2位的采样率的取值如图所示:

1位精度取值如图所示:

1位声道类型,单声道、双声道取值如下图所示,但是这里需要注意AAC格式的值总为1

所以根据咱们上面代码中配置的编码格式,咱们最后的取值为10101111,转为16进制就是0xAF。

视频Tag Data格式分析:

下面这个图很重要,rtmp封包的时候里面的数据按照下面的格式封装。

注:图中sps[1]为profile sps[2]为兼容性 sps[3]为profile level

接下来用三张flv视频的格式分析图片来对照看看,我把AVC序列头类型的视频Tag分析写了一下,普通NAL单元类型视频和音频自己试试分析吧。

视频类型AVC序列头类型:

视频类型普通NAL单元类型:

音频类型:

五、利用librtmp封包发送

  • 利用librtmp连接流;
  • 根据数据类型封装分别封装视频类型和音频类型的package;
  • 利用librtmp发送package;

librtmp的工作流程:

首先我们Java层需要定义一个类RTMPPacket用来封装编码器读出的数据;

然后补全我们上面两种编码器中缺失的编码数据封装,

视频编码器中添加:

                //获得了编码好的数据,封装出rmpt视频包
                RTMPPackage rtmpPackage = new RTMPPackage();
                rtmpPackage.setBuffer(bytes);
                rtmpPackage.setType(RTMPPackage.RTMP_PACKET_TYPE_VIDEO);
                if (firstTimeStamp == 0) {//记录第一次获取编码数据的时间
                    firstTimeStamp = bufferInfo.presentationTimeUs/1000;
                }
                //tms需要的是一个相对时间
                rtmpPackage.setTms(bufferInfo.presentationTimeUs/1000 - firstTimeStamp);
                screenLive.addPackage(rtmpPackage);

音频编码器中添加:

需要注意的是在循环发送音频数据前,需要先发送一个音频数据的头部信息,

如果使用的是单声道就是 0x12 ,0x08,

双声道就是 0x12 ,0x10 

        //rtmp中音频数据封包头部数据,只用发送一次
        RTMPPackage rtmpPackage = new RTMPPackage();
        rtmpPackage.setType(RTMPPackage.RTMP_PACKET_TYPE_AUDIO_HEAD);
        rtmpPackage.setBuffer(new byte[]{0x12, 0x08});
        Log.d("RTMP", "AudioCodec addPackage");
        screenLive.addPackage(rtmpPackage);


        //rtmp音频封包
        rtmpPackage = new RTMPPackage();
        rtmpPackage.setType(RTMPPackage.RTMP_PACKET_TYPE_AUDIO_DATA);
        rtmpPackage.setBuffer(outBuffer);
        if (firstTimeStamp == 0) {
              firstTimeStamp = bufferInfo.presentationTimeUs / 1000;
        }
        rtmpPackage.setTms(bufferInfo.presentationTimeUs / 1000 - firstTimeStamp);
        screenLive.addPackage(rtmpPackage);

 接下来是JNI方法:

利用librtmp连接流:

extern "C"
JNIEXPORT jboolean JNICALL
Java_com_mpass_xxapp_rtmp_ScreenLive_connect(JNIEnv *env, jobject thiz, jstring url_) {
    // TODO: implement connect()
    const char *url = env->GetStringUTFChars(url_, 0);

    int ret = 0;
    do{
        live = static_cast<Live *>(malloc(sizeof(Live)));
        memset(live,0, sizeof(Live));
        live->rtmp = RTMP_Alloc();//申请内存
        RTMP_Init(live->rtmp);//初始化

        //设置URL
        if (!(ret = RTMP_SetupURL(live->rtmp, const_cast<char *>(url)))) break;
        //开启输出模式
        RTMP_EnableWrite(live->rtmp);
        //连接服务器
        if (!(ret = RTMP_Connect(live->rtmp, 0))) break;
        //连接流
        if (!(ret = RTMP_ConnectStream(live->rtmp, 0))) break;
        LOGI("connect success");
    }while (0);


    if (!ret && live){
        free(live);
        live = NULL;
    }

    env->ReleaseStringUTFChars(url_, url);
    return ret;
}

音频封包:

RTMPPacket * buildAudioPackage(int8_t *buffer, int len, int type, long tms, RTMP *rtmp){

    RTMPPacket *audioPacket = static_cast<RTMPPacket *>(malloc(sizeof(RTMPPacket)));
    RTMPPacket_Alloc(audioPacket, len + 2);//申请内存
    audioPacket->m_body[0] = 0xAF;//设置音频参数,编码类型、采样率、精度、类型
    audioPacket->m_body[1] = 0x01;//非指定类型,不是头信息
    if (type == 1) {
        audioPacket->m_body[1] = 0x00;//头信息类型
    }

    memcpy(&audioPacket->m_body[2], buffer, len);//拷贝音频数据

    audioPacket->m_headerType = RTMP_PACKET_SIZE_LARGE;
    audioPacket->m_hasAbsTimestamp = 0;//是否使用绝对时间
    audioPacket->m_nTimeStamp = tms;//时间戳
    audioPacket->m_nBodySize = len + 2;//音频数据长度+头部的参数和类型2字节
    audioPacket->m_packetType = RTMP_PACKET_TYPE_AUDIO;
    audioPacket->m_nChannel = 0x05;
    audioPacket->m_nInfoField2 = rtmp->m_stream_id;

    return audioPacket;
}

视频封包:

1.判断视频类型,是否包含SPS和PPS信息:

int sendVideo(int8_t *byte, int len, long tms) {
    int ret;
    do{
        //视频数据的前四个字节是分隔符
        if (byte[4] == 0x67) {//如果是sps信息则需要保存到结构体中,随着下一次的AVC序列头发送

            if (live && (!live->pps || !live->sps)) {
                saveSequenceHeadMsg(byte, len, live);
            }
        } else{

            if (byte[4] == 0x65){//发送AVC序列头
                RTMPPacket *packet = buildVideoSequencePackage(live);
                if (!(ret = sendPackageData(packet))) {
                    break;
                }
            }
            //发送普通NAL单元
            RTMPPacket *packet = buildVideoPackage(byte, len, tms, live->rtmp);
            ret = sendPackageData(packet);
        }
    }while (0);
    return ret;
}

2.获取SPS和PPS信息:

void saveSequenceHeadMsg(int8_t *buffer, int len, Live *live){

    for (int i = 0; i < len-4; ++i) {

        if (buffer[i] == 0x00 &&
        buffer[i+1] == 0x00 &&
        buffer[i+2] == 0x00 &&
        buffer[i+3] == 0x01) {//H264以NALU单元组成,每个单元之间以0x00 0x00 0x00 0x01分割

            if (buffer[i+4] == 0x68) {//67为sps,68为pps

                live->sps_len = i-4;
                live->sps = static_cast<int8_t *>(malloc(sizeof(int8_t)));
                memcpy(live->sps, buffer+4, live->sps_len);
                live->pps_len = len-i-4;
                live->pps = static_cast<int8_t *>(malloc(sizeof(int8_t)));
                memcpy(live->pps, buffer+i+4, live->pps_len);
                LOGI("sps:%d pps:%d", live->sps_len, live->pps_len);
                break;
            }
        }
    }
}

3.封装AVC序列头信息:

RTMPPacket* buildVideoSequencePackage(Live *live){
    RTMPPacket *packet = static_cast<RTMPPacket *>(malloc(sizeof(RTMPPacket)));
    //视频数据(0x17)1字节+类型(0x00序列头)1字节+合成时间3个字节全为0+版本(0x01)1字节+
    // 编码规格3字节(sps[1]+sps[2]+sps[3])+NALU长度1个字节(0xFF)+sps个数1个字节(0xE1)
    // +sps长度2个字节+sps内容+pps个数1个字节(0x01)+pps长度2个字节+pps内容
    int bodySize = 16+live->pps_len+live->sps_len;
    RTMPPacket_Alloc(packet, bodySize);

    int i=0;
    packet->m_body[i++] = 0x17;//数据类型 1关键帧 7AVC
    packet->m_body[i++] = 0x00;//序列头类型
    packet->m_body[i++] = 0x00;//合成时间
    packet->m_body[i++] = 0x00;
    packet->m_body[i++] = 0x00;
    packet->m_body[i++] = 0x01;//版本
    packet->m_body[i++] = live->sps[1];//编码规格sps[1]
    packet->m_body[i++] = live->sps[2];//编码规格sps[2]
    packet->m_body[i++] = live->sps[3];//编码规格sps[3]
    packet->m_body[i++] = 0xFF;//NALU长度
    packet->m_body[i++] = 0xE1;//sps个数
    packet->m_body[i++] = (live->sps_len>>8) & 0xff;//sps长度高位
    packet->m_body[i++] = live->sps_len & 0xff;//sps长度低位
    memcpy(&packet->m_body[i], live->sps, live->sps_len);//sps内容
    i += live->sps_len;
    packet->m_body[i++] = 0x01;//pps个数
    packet->m_body[i++] = (live->pps_len>>8) & 0xff;//pps长度高位
    packet->m_body[i++] = live->pps_len & 0xff;//pps长度低位
    memcpy(&packet->m_body[i], live->pps, live->pps_len);//pps内容

    packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
    packet->m_nInfoField2 = live->rtmp->m_stream_id;
    packet->m_nChannel =0x04;
    packet->m_nBodySize = bodySize;
    packet->m_nTimeStamp = 0;
    packet->m_hasAbsTimestamp = 0;
    packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
    return packet;
}

4.封装普通NAL单元信息:

RTMPPacket* buildVideoPackage(int8_t *buffer, int len, long tms, RTMP *rtmp){
    buffer += 4;//去掉分隔符
    len -= 4;

    RTMPPacket *videoPacket = static_cast<RTMPPacket *>(malloc(sizeof(RTMPPacket)));
    RTMPPacket_Alloc(videoPacket, len+9);//申请空间大小需要加上头部的9个字节
    videoPacket->m_body[0] = 0x27;
    if (buffer[0] == 0x65) {//关键帧
        videoPacket->m_body[0] = 0x17;
        LOGI("发送关键帧 data");
    }

    videoPacket->m_body[1] = 0x01;
    videoPacket->m_body[2] = 0x00;
    videoPacket->m_body[3] = 0x00;
    videoPacket->m_body[4] = 0x00;

    //长度
    videoPacket->m_body[5] = (len >> 24) & 0xff;
    videoPacket->m_body[6] = (len >> 16) & 0xff;
    videoPacket->m_body[7] = (len >> 8) & 0xff;
    videoPacket->m_body[8] = (len) & 0xff;

    memcpy(&videoPacket->m_body[9], buffer, len);

    videoPacket->m_packetType = RTMP_PACKET_TYPE_VIDEO;
    videoPacket->m_nInfoField2 = rtmp->m_stream_id;
    videoPacket->m_nChannel =0x04;
    videoPacket->m_nBodySize = len+9;
    videoPacket->m_nTimeStamp = tms;
    videoPacket->m_hasAbsTimestamp = 0;
    videoPacket->m_headerType = RTMP_PACKET_SIZE_LARGE;

    return videoPacket;
}

利用librtmp发包:

int sendPackageData(RTMPPacket *packet){
    int ret = RTMP_SendPacket(live->rtmp, packet, 1);//1代表加入发包队列
    RTMPPacket_Free(packet);
    free(packet);
    return ret;
}

好了基本思路就是这样的,至于写完如何验证程序正确性,你可以自己搭建Nginx服务器,也可以选择白嫖斗鱼的服务器,我用的后者,申请斗鱼主播,然后开播后会给你一个rtmp推流地址和推流码,拼接后就是librtmp需要的url地址,不过推流码有时间限制,最后看个推流成功的动态图结束吧。

参考文章链接:

lv格式详解+实例剖析

视音频编解码学习工程:FLV封装格式分析器

将h.264视频流封装成flv格式文件

H264参数SPS(序列参数集)和PPS(图像参数集)说明

Demo下载链接:

https://download.csdn.net/download/wozuihaole/12692722

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐