一、前言
用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;
}