乐趣区

关于c++:SkeyePlayer源码解析系列之录像写MP4

SkeyePlayer(Windows) 中录像采纳 GPAC 的 MP4Box 库来封装 MP4,上面我将简略介绍 MP4 的封装调用流程和须要留神的点;

一、GPAC 库的编译,GPAC 是跨平台的库,windows 和 linux 都能很不便多编译,再次不做过多赘述,大家可去 GPAC 官网或者 Github 上下载;

二、创立 MP4

bool SkeyeMP4Writer::CreateMP4File(char*filename,int flag)
{SaveFile();
    m_audiostartimestamp=-1;
    m_videostartimestamp=-1;
    if(filename==NULL)
    {char filename2[256]={0};
        sprintf(filename2,"%d-gpac%d.mp4",time(NULL),rand());
        p_file=gf_isom_open(filename2,GF_ISOM_OPEN_WRITE,NULL);// 关上文件
    }else
        p_file=gf_isom_open(filename,GF_ISOM_OPEN_WRITE,NULL);// 关上文件

    if (p_file==NULL)
    {return false;}

    gf_isom_set_brand_info(p_file,GF_ISOM_BRAND_MP42,0);

    //if(flag&ZOUTFILE_FLAG_VIDEO)
    //{//    m_videtrackid=gf_isom_new_track(p_file,0,GF_ISOM_MEDIA_VISUAL,1000);
    //    gf_isom_set_track_enabled(p_file,m_videtrackid,1);
    //}
    //if(flag&ZOUTFILE_FLAG_AUDIO)
    //{//    m_audiotrackid=gf_isom_new_track(p_file,0,GF_ISOM_MEDIA_AUDIO,1000);
    //    gf_isom_set_track_enabled(p_file,m_audiotrackid,1);
    //}
    m_nCreateFileFlag = flag;

    return true;
}

创立 MP4 很简略,调用 gf_isom_open 函数就能轻松搞定,gf_isom_set_brand_info 函数设置以后写 MP4 的版本为 MP4V2;值得注意的中央是:

1>. 创立文件之前须要对所有的参数进行初始化,以及如果文件正在写入则须要将其敞开,这个操作次要是 32 位程序写的 MP4 文件大于 4G 可能呈现不能播放的问题,为了不便写 MP4 文件进行分片,这个将在系列文章后续中进行解说;2>. 大家能够看到上段代码有屏蔽了局部代码 flag&ZOUTFILE_FLAG_VIDEO 和 flag&ZOUTFILE_FLAG_AUDIO 的判断,这两段代码是用来在 MP4 文件中创立音频轨和视频轨(默认各只创立一个),请留神:如果这里曾经创立了音频和视频轨,然而后续的写入过程中如果只写音频或者视频的话,某些播放器可能是播不进去的(比方 windows 自带的播放器),所以,如果只写音频的话只须要创立音频轨就能够了,视频同理。

三、写入视频 H264 的 SPS 和 PPS 头信息

bool SkeyeMP4Writer::WriteH264SPSandPPS(unsigned char*sps,int spslen,unsigned char*pps,int ppslen,int width,int height)
{if (m_nCreateFileFlag&ZOUTFILE_FLAG_VIDEO)
    {m_videtrackid = gf_isom_new_track(p_file, 0, GF_ISOM_MEDIA_VISUAL, 1000);
        gf_isom_set_track_enabled(p_file, m_videtrackid, 1);
    }
    else
    {return false;}
    p_videosample=gf_isom_sample_new();
    p_videosample->data=(char*)malloc(1024*1024);


    p_config=gf_odf_avc_cfg_new();    
    gf_isom_avc_config_new(p_file,m_videtrackid,p_config,NULL,NULL,&i_videodescidx);
    gf_isom_set_visual_info(p_file,m_videtrackid,i_videodescidx,width,height);

    GF_AVCConfigSlot m_slotsps={0};
    GF_AVCConfigSlot m_slotpps={0};
    
    p_config->configurationVersion = 1;
    p_config->AVCProfileIndication = sps[1];
    p_config->profile_compatibility = sps[2];
    p_config->AVCLevelIndication = sps[3];
    
    m_slotsps.size=spslen;
    m_slotsps.data=(char*)malloc(spslen);
    memcpy(m_slotsps.data,sps,spslen);    
    gf_list_add(p_config->sequenceParameterSets,&m_slotsps);
    
    m_slotpps.size=ppslen;
    m_slotpps.data=(char*)malloc(ppslen);
    memcpy(m_slotpps.data,pps,ppslen);
    gf_list_add(p_config->pictureParameterSets,&m_slotpps);
    
    gf_isom_avc_config_update(p_file,m_videtrackid,1,p_config);

    free(m_slotsps.data);
    free(m_slotpps.data);

    return true;
}

首先,通过 gf_odf_avc_cfg_new() 创立一个设置 AVC 信息的配置构造 p_config,而后对构造中指定的信息,如:长,宽,SPS 和 PPS 等要害参数写入配置构造,调用 gf_isom_avc_config_update 函数写入参数信息;当然这里只是 H264 格局的参数设置,像其余的格局比方 H265 的设置也相似,这将在后续系列中进行解说;

四、写入音频 AAC 头信息

// 写入 AAC 信息
bool SkeyeMP4Writer::WriteAACInfo(unsigned char*info,int len, int nSampleRate, int nChannel, int nBitsPerSample)
{if (m_nCreateFileFlag&ZOUTFILE_FLAG_AUDIO)
    {m_audiotrackid = gf_isom_new_track(p_file, 0, GF_ISOM_MEDIA_AUDIO, 1000);
        gf_isom_set_track_enabled(p_file, m_audiotrackid, 1);
    }
    else
    {return false;}
    p_audiosample=gf_isom_sample_new();
    p_audiosample->data=(char*)malloc(1024*10);

    GF_ESD*esd=    gf_odf_desc_esd_new(0);
    esd->ESID=gf_isom_get_track_id(p_file,m_audiotrackid);
    esd->OCRESID=gf_isom_get_track_id(p_file,m_audiotrackid);
    esd->decoderConfig->streamType=0x05;
    esd->decoderConfig->objectTypeIndication=0x40;//0x40;
    esd->slConfig->timestampResolution=1000;//1000;// 工夫单元    
    esd->decoderConfig->decoderSpecificInfo=(GF_DefaultDescriptor*)gf_odf_desc_new(GF_ODF_DSI_TAG);
    esd->decoderConfig->decoderSpecificInfo->data=(char*)malloc(len);
    memcpy(esd->decoderConfig->decoderSpecificInfo->data,info,len);
    esd->decoderConfig->decoderSpecificInfo->dataLength=len;
    GF_Err gferr=gf_isom_new_mpeg4_description(p_file, m_audiotrackid, esd,  NULL, NULL, &i_audiodescidx);
    if (gferr!=0)
    {//        TRACE("mpeg4_description:%d\n",gferr);
    }
    gferr=gf_isom_set_audio_info(p_file,m_audiotrackid,i_audiodescidx, nSampleRate,nChannel, nBitsPerSample);//44100 2 16
    if (gferr!=0)
    {//        TRACE("gf_isom_set_audio:%d\n",gferr);
    }
    free(esd->decoderConfig->decoderSpecificInfo->data);
    return true;
}

调几个 API 就搞定了,判若两人的简略 –!,这里说一下一些要害参数的配置:

1> esd->decoderConfig->streamType=0x05,这里的 0x05 标示为 AAC,当然还指出其余的类型,如 MP3,AC3 等等,具体可查问 MP4BOX 相干文档获取;2> 函数出入的头两个参数大家看起来有点费解,这里示意的是音频解码参数组合的一个串,具体格局解析如下:(这个原本想独自开一篇博客来专门论述的,然而鉴于没多少内容就在这里一并表述进去)看上面代码段:
        //         前五位为 AAC object types  LOW          2
        //         接着 4 位为 码率 index        16000        8
        //         采样标记规范://    static unsigned long tnsSupportedSamplingRates[13] = // 音频采样率规范(标记),下标为写入标记
        //    {96000,88200,64000,48000,44100,32000,24000,22050,16000,12000,11025,8000,0};
        //         接着 4 位为 channels 个数                 2
        //        最初 3 位用 0 补齐
        //         应打印出的正确 2 进制模式为  00010 | 1000 | 0010 | 000
        //                                      2       8      2
        //  BYTE ubDecInfoBuff[] =  {0x12,0x10};//00010 0100 0010 000

        // 音频采样率规范(标记),下标为写入标记
        unsigned long tnsSupportedSamplingRates[13] = {96000,88200,64000,48000,44100,32000,24000,22050,16000,12000,11025,8000,0};
        int nI = 0;
        for (nI = 0; nI<13; nI++)
        {if (tnsSupportedSamplingRates[nI] == sample_rate )
            {break;}
        }
        unsigned char ucDecInfoBuff[2] = {0x12,0x10};//

        unsigned short  nDecInfo = (1<<12) | (nI << 7) | (channels<<3);
        int nSize = sizeof(unsigned short);
        memcpy(ucDecInfoBuff, &nDecInfo, nSize);
        SWAP(ucDecInfoBuff[0], ucDecInfoBuff[1]);
        int unBuffSize = sizeof(ucDecInfoBuff)*sizeof(unsigned char);

大家看懂了吧,比方当初有个示意解码信息的串为 00010 | 0100 | 0010 | 000,那么它则示意为 AAC-LC 44100 采样率 双声道音频,是不是很好了解呢!!!

五、解析 H264 帧写入 MP4

 限于篇幅,这里就不贴代码了(否则有靠代码凑字数的嫌疑,尽管我曾经贴了好多了,哈哈哈......),上面用文字描述,分三步走:1> 解析 H264 nal 头,获取 SPS 和 PPS, 因为咱们曾经通过设置函数设置了 SPS 和 PPS 等解码要害信息,所以咱们写入文件时,H264 帧将转换为 AVC 格局,什么意思,就是说将以 00000001 以及 000001 结尾的 NAL 单元转换为以该 NAL 单元的长度来填满该四个字节 (留神:所有的 H264 帧中的 0x00000001 和 0x000001 都要替换成 NAL 的长度,否则未替换的局部解码会花屏),默认三个字节的 000001 也用四个字节补齐,这次要是见于一帧多 NAL 的状况,这里有疑难我将在后续系列文章中解说;2> 写入 SPS 和 PPS 头;3> 写入以 NAL 长度为头四个字节的 AVC 帧,具体实现如下:
// 写入一帧,前四字节为该帧 NAL 长度
bool SkeyeMP4Writer::WriteVideoFrame(unsigned char*data,int len,bool keyframe,long timestamp)
{if (!p_videosample)
    {return false;}
    if (m_videostartimestamp==-1&&keyframe)
    {m_videostartimestamp=timestamp;}
    if (m_videostartimestamp!=-1)
    {
        p_videosample->IsRAP=keyframe;
        p_videosample->dataLength=len;
        memcpy(p_videosample->data,data,len);
        p_videosample->DTS=timestamp-m_videostartimestamp;
        p_videosample->CTS_Offset=0;    
        GF_Err gferr=gf_isom_add_sample(p_file,m_videtrackid,i_videodescidx,p_videosample);            
        if (gferr==-1)
        {
            p_videosample->DTS=timestamp-m_videostartimestamp+15;
            gf_isom_add_sample(p_file,m_videtrackid,i_videodescidx,p_videosample);
        }
    }
    
    return true;
}

六、AAC 写入 MP4(是否带 ADTS 头)

 同写视频相似,写音频同样要先写如音频解码参数,上文曾经剖析过如何写解码参数,这里只需把解码参数信息组织成串,通过 WriteAACInfo()函数写入即可。写音频数据,实现和视频一样,调用 gf_isom_add_sample 函数即可;须要留神:因为咱们曾经写入了音频解码信息,那么如果 AAC 数据中带有 ADTS 头,则须要去掉则 7 个字节的头,否则可能局部播放器不能失常播放,ADTS 头以 0xFFF 开始;

七、写入 MP4 封装头,保留文件

 保留文件,开释缓存和系统资源:
// 保留文件
bool SkeyeMP4Writer::SaveFile()
{if (m_psps)
    {
        delete m_psps;
        m_psps = NULL;
    }
    if (m_ppps)
    {
        delete m_ppps;
        m_ppps = NULL;
    }
    m_spslen=0;
    m_ppslen=0;
    if (m_pvps)
    {
        delete m_pvps;
        m_pvps = NULL;
    }
    m_vpslen = 0;

    m_audiostartimestamp=-1;
    m_videostartimestamp=-1;
    if (p_file)
    {gf_isom_close(p_file);
        p_file=NULL;
    }
    if(p_config)
    {
    //    delete p_config->pictureParameterSets;
        p_config->pictureParameterSets=NULL;
    //    delete p_config->sequenceParameterSets;
        p_config->sequenceParameterSets=NULL;
        gf_odf_avc_cfg_del(p_config);
        p_config=NULL;
    }
    if (p_hevc_config)
    {gf_odf_hevc_cfg_del(p_hevc_config);
        p_hevc_config = NULL;
    }
    if(p_audiosample)
    {if(    p_audiosample->data)
        {free(p_audiosample->data);
            p_audiosample->data=NULL;
        }
        gf_isom_sample_del(&p_audiosample);
        p_audiosample=NULL;
    }

    if(p_videosample)
    {if(    p_videosample->data)
        {free(p_videosample->data);
            p_videosample->data=NULL;
        }
        gf_isom_sample_del(&p_videosample);
        p_audiosample=NULL;
    }
    m_bwriteaudioinfo = false;
    m_bwritevideoinfo = false;
    return true;
}

获取更多信息

邮件:support@Skeyedarwin.org

WEB:www.SkeyeDarwin.org

Copyright © SkeyeDarwin.org 2012-2016

[外链图片转存中 …(img-M61RFUJj-1652408925267)]

退出移动版