ffmpeg合并音频和视频

发布时间 2023-07-29 10:58:04作者: keep-minding

ffmpeg合并音频和视频

命令行

ffmpeg -i video.m4s -i audio.m4s -acodec copy -vcodec copy out.mp4

使用ffmpeg的api


extern "C" {
#include "libavformat/avformat.h"
#include "libavutil/dict.h"
#include "libavutil/opt.h"
#include "libavutil/timestamp.h"
#include "libswscale/swscale.h"
#include "libswresample/swresample.h"
#include "libavutil/imgutils.h"
#include "libavcodec/avcodec.h"
}


extern "C"
int mergeVideo(const char* audio_src, const char* video_src, const char* dst) {
    int ret = 0;
    printf("mergeVideo enter\n");
    printf("%s\n", audio_src);
    printf("%s\n", video_src);
    printf("%s\n", dst);

    // 注册FFmpeg库中的所有编解码器、格式和协议(高版本不再需要注册)
//     av_register_all();

    // 创建输入音频和视频文件的AVFormatContext
    AVFormatContext *inputAudioFormatCtx = NULL;
    AVFormatContext *inputVideoFormatCtx = NULL;
    if (avformat_open_input(&inputAudioFormatCtx, audio_src, NULL, NULL) != 0 ||
        avformat_find_stream_info(inputAudioFormatCtx, NULL) < 0) {
        printf("无法打开音频输入文件\n");
        return -1;
    }
    if (avformat_open_input(&inputVideoFormatCtx, video_src, NULL, NULL) != 0 ||
        avformat_find_stream_info(inputVideoFormatCtx, NULL) < 0) {
        printf("无法打开视频输入文件\n");
        return -1;
    }

    // 创建输出文件的AVFormatContext
    AVFormatContext *outputFormatCtx = NULL;
//     if (avformat_alloc_output_context2(&outputFormatCtx, NULL, "mpegts", dst) < 0) {
    if (avformat_alloc_output_context2(&outputFormatCtx, NULL, NULL, dst) < 0) {
        printf("无法创建输出文件\n");
        return -1;
    }

    // 遍历输入音频流
    int audio_index = av_find_best_stream(inputAudioFormatCtx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
    AVStream* st1 = inputAudioFormatCtx->streams[audio_index];

//     const AVCodec* codec = NULL;
//     codec = avcodec_find_decoder(st1->codecpar->codec_id);
//     if (!codec)
//     {
//         fprintf(stderr, "Codec not found\n");
//         exit(1);
//     }
//     AVCodecContext* codec_ctx = NULL;
//     codec_ctx = avcodec_alloc_context3(codec);
//     if (!codec_ctx)
//     {
//         exit(1);
//     }
//     avcodec_parameters_to_context(codec_ctx, inputAudioFormatCtx->streams[audio_index]->codecpar);
//     if ((ret = avcodec_open2(codec_ctx, codec, NULL) < 0))
//     {
//         return -1;
//     }

    // 遍历输入视频流
    int video_index = av_find_best_stream(inputVideoFormatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
    AVStream* st2 = inputVideoFormatCtx->streams[video_index];

//     const AVCodec* codec2 = NULL;
//     codec2 = avcodec_find_decoder(st2->codecpar->codec_id);
//     if (!codec2)
//     {
//         fprintf(stderr, "Codec not found\n");
//         exit(1);
//     }
//     AVCodecContext* codec_ctx2 = NULL;
//     codec_ctx2 = avcodec_alloc_context3(codec2);
//     if (!codec_ctx2)
//     {
//         exit(1);
//     }
//     avcodec_parameters_to_context(codec_ctx2, inputVideoFormatCtx->streams[video_index]->codecpar);
//     if ((ret = avcodec_open2(codec_ctx2, codec2, NULL) < 0))
//     {
//         return -1;
//     }

    // 创建输出视频流
    // 每创建一个流,ctx->nb_streams数量就会加1
    AVStream* out1 = avformat_new_stream(outputFormatCtx, NULL);
    if (!out1)
    {
        return -1;
    }
    //复制配置信息
    AVCodecParameters* codecpar = inputAudioFormatCtx->streams[audio_index]->codecpar;
    avcodec_parameters_copy(out1->codecpar, codecpar);
    out1->codecpar->codec_tag = 0;

    //创建输出视频流
    AVStream* out2 = avformat_new_stream(outputFormatCtx, NULL);
    if (!out2)
    {
        return -1;
    }
    //复制配置信息
    AVCodecParameters* codecpar2 = inputVideoFormatCtx->streams[video_index]->codecpar;
    avcodec_parameters_copy(out2->codecpar, codecpar2);
    out2->codecpar->codec_tag = 0;

    // 打开输出文件
    if (!(outputFormatCtx->oformat->flags & AVFMT_NOFILE)) {
        if (avio_open(&outputFormatCtx->pb, dst, AVIO_FLAG_WRITE) < 0) {
            printf("无法打开输出文件\n");
            return -1;
        }
    }

    // 写入文件头
    if (avformat_write_header(outputFormatCtx, NULL) < 0) {
        printf("无法写入文件头\n");
        return -1;
    }

    // 复制音频和视频数据
    AVPacket pkt1, pkt2;
    int ret1 = 0, ret2 = 0;
    // 音视频合成
    while (1) {
        if (ret1 < 0 && ret2 < 0) {
            break;
        }
//         printf("ret1:%d\tret2:%d\tst->cur_dts:%ld\tst2->cur_dts:%ld\tpkt1.dts:%ld\tpkt2.dts:%ld\ttime_base:%f %f\tdst:%s\t\n",
//                ret1, ret2, st1->cur_dts, st2->cur_dts,  pkt1.dts, pkt2.dts, av_q2d(st1->time_base), av_q2d(st2->time_base), dst);
//         printf("avg_frame_rate:%f %f\tr_frame_rate:%f %f\tduration:%ld %ld\n", av_q2d(st1->avg_frame_rate),
//                av_q2d(st1->r_frame_rate), av_q2d(st2->avg_frame_rate), av_q2d(st2->r_frame_rate), pkt1.duration, pkt2.duration);
        // 根据时间轮流写音频和视频,音频和视频在现实世界长度相同不意味着存储的帧数相同
        // PTS:Presentation Time Stamp。PTS 主要用于度量解码后的视频帧什么时候被显示出来。
        // DTS:Decode Time Stamp。DTS 主要是标识读入内存中的Bit流在什么时候开始送入解码器中进行解码。
        // 有时读到的数据包的pts是空的,所以使用dts比较时间
        // android上st1没有cur_dts成员,可以使用pkt.pts,但是裸流pts和dts都是空的
        // 可能需要计算pts和dts并对pkt进行设置,这个可能有帮助:https://zhuanlan.zhihu.com/p/468346396
//         if ((st1->cur_dts < st2->cur_dts && ret1 >= 0) || ret2 < 0) {
        if ((pkt1.dts < pkt2.dts && ret1 >= 0) || ret2 < 0) {
            // 音频时间落后,或视频已写完时写音频
            ret1 = av_read_frame(inputAudioFormatCtx, &pkt1);
            if (ret1 < 0) {
                continue;
            }
            if (pkt1.stream_index == audio_index) {
                pkt1.stream_index = 0; // 设置音频流索引
                av_packet_rescale_ts(&pkt1, st1->time_base, out1->time_base);
                pkt1.pos = -1;
                ret = av_interleaved_write_frame(outputFormatCtx, &pkt1); // 写入音频包
                if (ret < 0) {
                    break;
                }
            }else {
                av_packet_unref(&pkt1);
            }
        } else {
            // 视频时间落后,或音频已写完时写视频
            ret2 = av_read_frame(inputVideoFormatCtx, &pkt2);
            if (ret2 < 0) {
                continue;
            }
            if (pkt2.stream_index == video_index) {
                pkt2.stream_index = 1; // 设置视频流索引
                    // 单纯的音频或视频文件可能使用的都是0通道,既有音频又有视频的情况,不能使用同一个通道
                av_packet_rescale_ts(&pkt2, st2->time_base, out2->time_base);   // 重新设置时间基,否则可能导致新视频播放时间不准
                pkt2.pos = -1;
                ret = av_interleaved_write_frame(outputFormatCtx, &pkt2); // 写入视频包
                if (ret < 0) {
                    break;
                }
            }else {
                av_packet_unref(&pkt2);
            }
        }
    }

    // 写入文件尾
    av_write_trailer(outputFormatCtx);

    // 释放资源
    avformat_close_input(&inputAudioFormatCtx);
    avformat_close_input(&inputVideoFormatCtx);
    avformat_free_context(outputFormatCtx);

    return ret;
}

CMakeLists.txt:

cmake_minimum_required(VERSION 3.0)

project(merge LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_COMPILER g++)

set(CMAKE_BUILD_TYPE "RELEASE")
# set(CMAKE_BUILD_TYPE "DEBUG")


# set(FFMPEG_LIBS_DIR /usr/lib/x86_64-linux-gnu)
# set(FFMPEG_HEADERS_DIR /usr/include/x86_64-linux-gnu)

set(FFMPEG_LIBS_DIR /home/light/FFmpeg-master-linux/FFmpeg-master/build/x86-64/include)
set(FFMPEG_HEADERS_DIR /home/light/FFmpeg-master-linux/FFmpeg-master/build/x86-64/lib)


include_directories(${FFMPEG_HEADERS_DIR})
link_directories(${FFMPEG_LIBS_DIR})
set(FFMPEG_LIBS libavcodec.so libavformat.so libswscale.so libavdevice.so libavutil.so)


include_directories(include/)
aux_source_directory(src/ srcfiles)


add_executable(merge main.cpp ${srcfiles})
target_link_libraries(merge ${FFMPEG_LIBS})

install(TARGETS merge RUNTIME DESTINATION bin)