Qt/C++音視頻開發69-保存監控pcm音頻數據到mp4文件/監控錄像/錄像存儲和回放/264/265/aac/pcm等

一、前言

用ffmpeg做音視頻保存到mp4文件,都會遇到一個問題,尤其是在視頻監控行業,就是監控攝像頭設置的音頻是PCM/G711A/G711U,解碼後對應的格式是pcm_s16be/pcm_alaw/pcm_mulaw,將這個原始的音頻流保存到mp4文件是會報錯的,在調用avformat_write_header寫文件頭的時候提示(-22) Invalid argument,非法的參數,翻閱源碼得知,ffmpeg中的mp4封裝並不支持pcma和pcmu,除非手動更改源碼加入。mp4封裝格式默認支持的音頻格式是aac和mp3,其實mp4文件本身是可以支持pcm音頻數據的,不知道爲何ffmpeg中不加入。通過個更改源碼的形式儘管可以支持,個人還是推薦用另外一種方法,那就是在調用avformat_alloc_output_context2的時候傳入format的時候填mov,而不是填mp4,mov的格式兼容性更強,文件拓展名依然是mp4一點問題沒有。對應avformat_alloc_output_context2函數的說明,format格式參數可以爲空,爲空的話默認從保存的文件名拓展名取,而如果指定了則以指定的爲準。

既然以mov格式存儲到mp4文件,那麼問題來了,會不會導致文件體積或者格式不兼容呢?一開始我也是有這個擔心的,特意找了多個廠家的攝像頭專門測試,發現根本沒有體積變化,所以個人猜測,填mov只是爲了方便跳過檢測,MOV文件可以使用多種編碼格式,包括MPEG-4、H.264、MJPEG等;而MP4文件主要使用H.264編碼。

上面的不僅支持264,同時也支持265,也就是mov格式同時支持264+aac/264+mp3/264+pcm/264+pcma/264+pcmu/265+aac/265+mp3/265+pcm/265+pcma/265+pcmu,這樣原始數據保存到文件最好,不用轉碼重新編碼,可以省下不少的CPU,寫文件基本上不佔CPU,基本上都是磁盤操作,所以性能瓶頸在磁盤讀寫能力和網絡帶寬。

二、效果圖

五、相關代碼

bool FFmpegSaveHelper::rtmp_pcm = false;
QStringList FFmpegSaveHelper::vnames_file = QStringList() << "h264" << "hevc";
QStringList FFmpegSaveHelper::anames_pcm = QStringList() << "pcm_mulaw" << "pcm_alaw" << "pcm_s16be";
QStringList FFmpegSaveHelper::anames_file = QStringList() << "aac" << "mp2" << "mp3" << "ac3" << anames_pcm;
QStringList FFmpegSaveHelper::anames_rtmp = QStringList() << "aac" << "mp3";
QStringList FFmpegSaveHelper::anames_rtsp = QStringList() << "aac" << "mp3" << anames_pcm;

void FFmpegSaveHelper::checkEncode(FFmpegSave *thread, const QString &videoCodecName, const QString &audioCodecName, bool &videoEncode, bool &audioEncode, EncodeAudio &encodeAudio, bool &needAudio)
{
    //推流和錄製要區分判斷(推流更嚴格/主要限定在流媒體服務器端)
    bool notSupportVideo = false;
    bool notSupportAudio = false;
    SaveMode saveMode = thread->getSaveMode();
    QString mediaUrl = thread->property("mediaUrl").toString();
    if (saveMode == SaveMode_File) {
        notSupportVideo = !vnames_file.contains(videoCodecName);
        notSupportAudio = !anames_file.contains(audioCodecName);
    } else {
        //具體需要根據實際需求進行調整
        if (saveMode == SaveMode_Rtmp) {
            notSupportVideo = (videoCodecName != "h264");
            notSupportAudio = !anames_rtmp.contains(audioCodecName);
        } else if (saveMode == SaveMode_Rtsp) {
            notSupportVideo = !vnames_file.contains(videoCodecName);
            notSupportAudio = !anames_rtsp.contains(audioCodecName);
        }

        //特定格式過濾
        if (mediaUrl.endsWith(".m3u8")) {
            notSupportAudio = true;
        }
    }

    if (notSupportVideo) {
        thread->debug(0, "視頻格式", QString("警告: %1").arg(videoCodecName));
        videoEncode = true;
    }

    if (notSupportAudio) {
        thread->debug(0, "音頻格式", QString("警告: %1").arg(audioCodecName));
        audioEncode = true;
    }

    //0. 因爲還沒有搞定萬能轉換/所以暫時做下面的限制
    //1. 保存文件模式下純音頻統一編碼成pcma
    //2. 保存文件模式下視音頻且啓用了轉碼則禁用音頻
    //3. 推流RTMP模式下啓用了轉碼則禁用音頻
    //4. 推流RTSP模式下純音頻且啓用了轉碼則編碼成pcma
    //5. 推流RTSP模式下啓用了轉碼則禁用音頻
    //6. 純音頻aac格式在推流的時候可選轉碼/有些流媒體程序必須要求轉碼才能用
    bool encodeAac = false;
    bool onlySaveAudio = thread->getOnlySaveAudio();
    bool onlyAac = (onlySaveAudio && audioCodecName == "aac");
    if (encodeAudio == EncodeAudio_Auto) {
        if (saveMode == SaveMode_File) {
            if (onlySaveAudio || audioCodecName == "pcm_s16le") {
                encodeAudio = EncodeAudio_Pcma;
            } else if (audioEncode) {
                needAudio = false;
            }
        } else if (saveMode == SaveMode_Rtmp) {
            if (audioEncode) {
                needAudio = false;
            } else if (onlyAac && encodeAac) {
                encodeAudio = EncodeAudio_Aac;
            }
        } else if (saveMode == SaveMode_Rtsp) {
            if (audioEncode) {
                encodeAudio = EncodeAudio_Pcma;
            } else if (onlyAac && encodeAac) {
                encodeAudio = EncodeAudio_Pcma;
            }
        }
    }

    //如果設置過需要檢查B幀/有B幀推流需要轉碼/否則一卡卡
    if (!videoEncode && !onlySaveAudio && saveMode != SaveMode_File) {
        bool checkB = thread->property("checkB").toBool();
        bool isFile = thread->property("isFile").toBool();
        if (checkB && isFile && FFmpegUtil::hasB(mediaUrl)) {
            videoEncode = true;
        }
    }

    //部分流媒體服務支持推pcma和pcmu
    if (rtmp_pcm && saveMode == SaveMode_Rtmp && anames_pcm.contains(audioCodecName)) {
        needAudio = true;
        encodeAudio = EncodeAudio_Pcma;
    }

    //音頻需要強轉則必須設置啓用音頻編碼
    if (encodeAudio != EncodeAudio_Auto) {
        audioEncode = true;
    }
}

const char *FFmpegSaveHelper::getFormat(AVDictionary **options, QString &fileName, bool mov, const QString &flag)
{
    //默認是mp4/mov更具兼容性比如音頻支持pcma等
    const char *format = mov ? "mov" : "mp4";
    if (fileName.startsWith("rtmp://")) {
        format = "flv";
    } else if (fileName.startsWith("rtsp://")) {
        format = "rtsp";
        av_dict_set(options, "stimeout", "3000000", 0);
        av_dict_set(options, "rtsp_transport", "tcp", 0);
    } else if (fileName.startsWith("udp://")) {
        format = "mpegts";
    } else {
        QByteArray temp;
        if (!flag.isEmpty()) {
            temp = flag.toUtf8();
            format = temp.constData();
            QString suffix = fileName.split(".").last();
            fileName.replace(suffix, flag);
        }
    }

    return format;
}

bool FFmpegSave::initStream()
{
    //如果存在祕鑰則啓用加密
    AVDictionary *options = NULL;
    FFmpegHelper::initEncryption(&options, this->property("cryptoKey").toByteArray());

    QString flag;
    if (getOnlySaveAudio() && encodeAudio != EncodeAudio_Aac) {
        flag = "wav";
    }

    //既可以是保存到文件也可以是推流(對應格式要區分)
    bool mov = audioCodecName.startsWith("pcm_");
    const char *format = FFmpegSaveHelper::getFormat(&options, fileName, mov, flag);

    //開闢一個格式上下文用來處理視頻流輸出(末尾url不填則rtsp推流失敗)
    QByteArray fileData = fileName.toUtf8();
    const char *url = fileData.data();
    int result = avformat_alloc_output_context2(&formatCtx, NULL, format, url);
    if (result < 0) {
        debug(result, "創建格式", "");
        return false;
    }

    //創建輸出視頻流
    if (!this->initVideoStream()) {
        goto end;
    }

    //創建輸出音頻流
    if (!this->initAudioStream()) {
        goto end;
    }

    //打開輸出文件
    if (!(formatCtx->oformat->flags & AVFMT_NOFILE)) {
        //記錄開始時間並設置回調用於超時判斷
        startTime = av_gettime();
        formatCtx->interrupt_callback.callback = FFmpegSaveHelper::openAndWriteCallBack;
        formatCtx->interrupt_callback.opaque = this;

        tryOpen = true;
        result = avio_open2(&formatCtx->pb, url, AVIO_FLAG_WRITE, &formatCtx->interrupt_callback, NULL);
        tryOpen = false;
        if (result < 0) {
            debug(result, "打開輸出", "");
            goto end;
        }
    }

    //寫入文件開始符
    result = avformat_write_header(formatCtx, &options);
    if (result < 0) {
        debug(result, "寫文件頭", "");
        goto end;
    }

    writeHeader = true;
    debug(0, "打開輸出", QString("格式: %1").arg(format));
    return true;

end:
    //關閉釋放並清理文件
    this->close();
    this->deleteFile(fileName);
    return false;
}

bool FFmpegSave::initVideoStream()
{
    if (needVideo) {
        videoIndexOut = 0;
        AVStream *stream = avformat_new_stream(formatCtx, NULL);
        if (!stream) {
            return false;
        }

        //設置旋轉角度(沒有編碼的數據是源頭帶有旋轉角度的/編碼後的是正常旋轉好的)
        if (!videoEncode) {
            FFmpegHelper::setRotate(stream, rotate);
        }

        //複製解碼器上下文參數(不編碼從源頭流拷貝/編碼從設置的編碼器拷貝)
        int result = -1;
        if (videoEncode) {
            stream->r_frame_rate = videoCodecCtx->framerate;
            result = FFmpegHelper::copyContext(videoCodecCtx, stream, true);
        } else {
            result = FFmpegHelper::copyContext(videoStreamIn, stream);
        }

        if (result < 0) {
            debug(result, "複製參數", "");
            return false;
        }
    }

    return true;
}

bool FFmpegSave::initAudioStream()
{
    if (needAudio) {
        audioIndexOut = (videoIndexOut == 0 ? 1 : 0);
        AVStream *stream = avformat_new_stream(formatCtx, NULL);
        if (!stream) {
            return false;
        }

        //複製解碼器上下文參數(不編碼從源頭流拷貝/編碼從設置的編碼器拷貝)
        int result = -1;
        if (audioEncode) {
            result = FFmpegHelper::copyContext(audioCodecCtx, stream, true);
        } else {
            result = FFmpegHelper::copyContext(audioStreamIn, stream);
        }

        if (result < 0) {
            debug(result, "複製參數", "");
            return false;
        }
    }

    return true;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章