FFmpeg API(上)

FFmpeg 的功能非常强大,FFmpeg 不但有丰富的命令行工具来帮助我们处理音视频数据,而且 FFmpeg 提供了非常易用的 API,通过这些 API 就可以把 FFmpeg 集成到我们自己的程序中,以写代码的方式调用这些 API 来完成对媒体文件的操作。 另外在使用这些 API 的同时可以参考下面的图,该图展示了 FFmpeg 处理流数据基本流程,一定要想搞楚每一步操作在干什么,目的是什么。

FFmpeg 处理流数据基本概念

本文主要是尝试以代码的方式调用 FFmpeg 的 API 完成一些媒体文件的操作,在学习 FFmpeg 的 API 之前需要介绍下一些统一术语:

1、容器/文件(Conainer/File):即特定格式的多媒体文件,比如Mp4、flv、mov等。

2、媒体流(Stream):表示时间轴上的一段连续数据,如一段声音数据、一段视频数据或一段字幕数据,可以是压缩的,也可以是非压缩的,压缩的数据需要关联特定的编解码器。

3、数据帧/数据包(Frame/Packet):通常一个媒体流是由大量的数据帧组成的,对于压缩数据,帧对应着编解码器的最小处理单元,分属于不同媒体流的数据帧交错存储于容器之中。

4、编解码器:编解码器是以帧为单位实现压缩数据和原始数据之间的相互转换的。

前面所介绍的术语,其实就是FFmpeg中抽象出来的概念,在FFmpeg的代码中,存在几个重要的结构体:

1、AVFormatContext:就是对容器个抽象,媒体文件的构成和基本信息上下文

2、AVStream:流的抽象,在每一路流中都会描述这路流的编码格式。

3、AVCodecContext:编解码格式抽象。

4、AVCodec:编解码器抽象。

5、AVPacket:压缩数据抽象,AVPacket作为解码码器的输入。

6、AVFrame:原始数据的抽象,AVFrame作为编码器的输入。

7、AVFilter:对于音视频的处理肯定是针对于原始数据的处理,也就是针对于AVFrame的处理,使用的就是AVFilter。

FFmpeg 模块说明

FFmpeg模块 功能
libavcodec 提供了一系列编码器的实现。
libavformat 实现在流协议,容器格式及其本IO访问。
libavutil 包括了hash器,解码器和各种工具函数。
libavfilter 提供了各种音视频过滤器。
libavdevice 提供了访问捕获设备和回放设备的接口。
libavswresample 实现了混音和重采样。
libavswscale 实现了色彩转换和缩放工能。

FFmpeg API Demo

1、日志打印

主要的API

#include <libavutil.h>

av_log_set_level(AV_LOG_INFO);

av_log(NULL, AV_LOG_INFO, "...%s\n", "")

CMakeLists.txt

cmake_minimum_required(VERSION 3.19)
project(ffmpeg_log)

# C++11
set(CMAKE_CXX_STANDARD 11)

# 指定目标于源文件
add_executable(ffmpeg_log main.cpp)

# 导入头文件
include_directories("/usr/local/ffmpeg/include")

# Import依赖库
set(libs "/usr/local/ffmpeg/lib")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${libs}")

# 目标依赖库
target_link_libraries(
        ffmpeg_log
        # ffmpeg_log这个目标依赖于libavutil.dylib
        avutil
)

main.cpp

#include <iostream>

extern "C"
{
    // 打印日志的头文件
    #include <libavutil/log.h>
}

int main() {
    std::cout << "Hello, World!" << std::endl;
    // 设置最低打印的日志级别为INFO,超过这个级别的日志将不会被打印
    av_log_set_level(AV_LOG_INFO);

    // VERBOSE级别不打印
    av_log(NULL, AV_LOG_VERBOSE, "Hello %s\n", "FFmpeg Log! AV_LOG_VERBOSE");
    
    // DEBUG级别不打印
    av_log(NULL, AV_LOG_DEBUG, "Hello %s\n", "FFmpeg Log! AV_LOG_DEBUG");
    
    // 剩下的全都打印
    av_log(NULL, AV_LOG_INFO, "Hello %s\n", "FFmpeg Log! AV_LOG_INFO");
    av_log(NULL, AV_LOG_WARNING, "Hello %s\n", "FFmpeg Log! AV_LOG_WARNING");
    av_log(NULL, AV_LOG_ERROR, "Hello %s\n", "FFmpeg Log! AV_LOG_ERROR");

    return 0;
}

2、文件删除与重命名

主要的API

#include <libavutil/log.h>
#include <libavformat/avio.h>

avpriv_io_move("srcFilePath", "destFilePath");

avpriv_io_delete("targetFilePath");

CMakeLists.txt同上

// 因为ffmpeg的日志打印也是标准/错误输出流
#include <iostream>

extern "C" {
#include <libavutil/log.h>
#include <libavformat/avio.h>
}

int main() {
    av_log_set_level(AV_LOG_DEBUG);
    av_log(NULL, AV_LOG_DEBUG, "start....\n");
    int ret;

    // 移动或者重命名文件
    ret = avpriv_io_move("../README.md", "../my.md");
    if (ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "move README.md failed\n");
        return -1;
    }
    av_log(NULL, AV_LOG_DEBUG, "move README.md success\n");

    // 删除文件
    ret = avpriv_io_delete("../README.md");
    if (ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "delete README.md failed\n");
        return -1;
    }

    av_log(NULL, AV_LOG_DEBUG, "delete README.md success\n");
    return 0;
}

3、文件夹打开与读取

主要的API

avio_open_dir 打开文件夹

avio_read_dir 读取文件夹

avio_close_dir 释放资源

avio_free_directory_entry 释放资源

// 因为ffmpeg的日志打印也是标准/错误输出流
#include <iostream>

extern "C"
{
#include <libavutil/log.h>
#include <libavformat/avio.h>
}

int main(int argc, char const *argv[])
{
    av_log_set_level(AV_LOG_INFO);
    // avio_open_dir();
    // avio_read_dir();
    // avio_close_dir();
    // AVIODirContext 操作目录的上下文
    // AVIODirEntry 目录项,存放文件名、文件大小等信息
    AVIODirContext *context = NULL;
    int ret = avio_open_dir(&context, "./", NULL);
    if(ret < 0) {
        av_log(NULL, AV_LOG_ERROR,"Open dir failed! ret = %d\n", ret);
        avio_close_dir(&context); // 避免内存泄漏
        return -1;
    }

    AVIODirEntry *entry = NULL;
    for(;;){
        int ret = avio_read_dir(context, &entry);
        if(ret < 0) {
            av_log(NULL, AV_LOG_ERROR,"Read dir failed! ret = %d\n", ret);
            return -1;
        }

        // 到末尾说明应该退出
        if(entry == NULL) break;

        av_log(NULL, AV_LOG_INFO, "%12" PRId64" %s \n", entry -> size, entry -> name);
        // 释放AVIODirEntry
        avio_free_directory_entry(&entry);
    }

    avio_close_dir(&context);
    return 0;
}

4、音视频 Meta 信息

FFmpeg 处理的基本流程:

av_register_all

avformat_open_input

av_dump_format

avformat_close_input

#include <iostream>

extern "C" {
#include <libavutil/log.h>
#include <libavformat/avformat.h>
#include <libavformat/avio.h>
}

int main()
{
	const char* inputFilePath = "C:\\Users\\changlin\\Desktop\\input.mp4";
	av_log_set_level(AV_LOG_INFO);
	av_register_all();
	//av_register_input_format();
	AVFormatContext* avContext = NULL;


	// fmt:输入文件格式(默认根据后缀名解析) options: 命令行参数
	int ret = avformat_open_input(&avContext, inputFilePath, NULL, NULL);
	if (ret < 0)
	{
		av_log(NULL, AV_LOG_ERROR, "Open file failed,ret = %d\n", ret);
		return -1;
	}

	// 打印关于输入或输出格式的详细信息
	// param2 需要解析的流索引号,例如0是视频流,1是音频流
	// param4 指定的视音频上下文是输入流(0)还是输出流(1)
	av_dump_format(avContext, 0, inputFilePath, 0);
	avformat_close_input(&avContext);
	return 0;
}

5、抽取AAC音频数据

ADTS全称是(Audio Data Transport Stream),是AAC的一种十分常见的传输格式。

记得第一次做demux的时候,把AAC音频的ES流从FLV、Mp4等封装格式中抽出来送给硬件解码器时,不能播;因为一般的AAC解码器都需要把AAC的ES流打包成ADTS的格式,一般是在AAC ES流前添加7个字节的ADTS header,也就是说你可以把ADTS这个头看作是AAC的frameheader。

ADTS 头中相对有用的信息采样率、声道数、帧长度。想想也是,我要是解码器的话,你给我一堆得AAC音频ES流我也解不出来。每一个带ADTS头信息的AAC流会清晰的告送解码器他需要的这些信息。

#pragma once
#include <cstdint>
/*
 * szAdtsHeader ADTS头缓冲区
 * dataLength 数据长度,也就是Package.size + 7
 */
void adts_header(char* szAdtsHeader, int dataLength) {
	int profile = 2; // AAC LC
	int freqIdx = 4; // 44.1KHz
	int chanCfg = 2; // CPE

	// fill in ADTS data
	szAdtsHeader[0] = (char)0xFF;
	szAdtsHeader[1] = (char)0xF9;
	szAdtsHeader[2] = (char)(((profile - 1) << 6) + (freqIdx << 2) + (chanCfg >> 2));
	szAdtsHeader[3] = (char)(((chanCfg & 3) << 6) + (dataLength >> 11));
	szAdtsHeader[4] = (char)((dataLength & 0x7FF) >> 3);
	szAdtsHeader[5] = (char)(((dataLength & 7) << 5) + 0x1F);
	szAdtsHeader[6] = (char)0xFC;
}

接下来抽取音频,并给给每帧AAC数据都添加上AAC帧头部信息:

#include <iostream>
#include <stdio.h>

extern "C" {
#include <libavutil/log.h>
#include <libavformat/avformat.h>
#include <libavformat/avio.h>
#include <libavcodec/packet.h>
#include <libavcodec/avcodec.h>
#include "adts_header.h"
}

int main()
{
	const char* inputFilePath = "C:\\Users\\changlin\\Desktop\\input.mp4";
	const char* outputFilePath = "C:\\Users\\changlin\\Desktop\\output.aac";
	av_log_set_level(AV_LOG_INFO);
	av_register_all();
	//av_register_input_format();
	AVFormatContext* avContext = NULL;

	// fmt:输入文件格式(默认根据后缀名解析) options: 命令行参数
	int ret = avformat_open_input(&avContext, inputFilePath, NULL, NULL);
	if (ret < 0)
	{
		av_log(NULL, AV_LOG_ERROR, "Open file failed,ret = %d\n", ret);
		return -1;
	}
	// 打印出基本信息
	av_dump_format(avContext, 0, inputFilePath, 0);
	
	FILE* outputFile = fopen(outputFilePath, "wb+");
	if(outputFile == NULL)
	{
		av_log(NULL, AV_LOG_ERROR, "Open outFile failed!\n");
		avformat_close_input(&avContext);
		return -1;
	}
	
	int audio_index = 0;
	// 寻找最佳音频流
	audio_index = av_find_best_stream(avContext, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
	if(audio_index < 0)
	{
		av_log(NULL, AV_LOG_ERROR, "Can't find the best stream!\n");
		fclose(outputFile);
		return -1;
	}
	AVPacket av_packet;
	// 初始化包
	av_init_packet(&av_packet);
	size_t writeLength;
	while(av_read_frame(avContext, &av_packet) >= 0)
	{
		if(audio_index == av_packet.stream_index)
		{
			// 添加ADTS头信息
			char adts_header_buf[7];
			adts_header(adts_header_buf, av_packet.size + 7);
			fwrite(adts_header_buf, 1, 7, outputFile);

			// 写入AAC帧
			writeLength = fwrite(av_packet.data, 1, av_packet.size, outputFile);
			if(writeLength != av_packet.size)
			{
				av_log(NULL, AV_LOG_WARNING, "write length not equal packet.size\n");
			}
		}
		// 释放上次使用的包
		av_packet_unref(&av_packet);
	}
	avformat_close_input(&avContext);
	if(outputFile)
	{
		fclose(outputFile);
	}
	return 0;
}

6、抽取H264视频数据

#include <iostream>

extern "C"
{
#include <libavutil/log.h>
#include <libavformat/avio.h>
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavfilter/avfilter.h>
#include <stdio.h>
#include "extr_video.h"
}

/**
 * 概念一:StartCode
 * 帧与帧之间如何区分?
 *  - 1、特征码,比如解析到某个特征码的时候就知道了这一帧数据结束了。
 *  - 2、头部添加帧长度,知道了此帧有多长,就按照长度解析即可。
 * H264采用的就是第一种,StartCode这个概念就是特征码。
 * 
 * 概念二:SPS/PPS
 * 解码用到的视频参数,视频帧的宽高、帧率等等参数。
 * 如果是相同的参数,比如宽高、帧率一直不变,那么只需要一份SPS/PPS就可以了。
 * 但是如果是变化的场景,比如直播流,参数会发生变化,每次发生变化的时候都会更新SPS/PPS,
 * 解码器在参数发生变化的时候就会重新初始化解码器,从而继续正确的解码数据。另外在直播流等
 * 通过网络传输的场景下容易发生丢包,往往会在关键帧设置SPS/PPS,避免错误解码等情况的出现。
 * 但是这样会导致网络数据传输量增加,增加网络负载吗?其实不会,SPS/PPS只有几个字节或者十
 * 几个字节,占用空间非常小,对于整个视频流来说基本上可以忽略不计。
 * 
 * 通过FFmpeg如何获取SPS/PPS呢?
 * codec —> extratata
 */

/**
 * @brief 抽取H264数据
 * 
 * @param argc 
 * @param argv 
 * @return int 
 */
int main(int argc, char const *argv[])
{
    const char *intputPath = "/Users/zchanglin/Downloads/input.mp4";
    const char *outputPath = "/Users/zchanglin/Downloads/output.h264";

    av_register_all();

    AVFormatContext *avContext;
    int open_ret = avformat_open_input(&avContext, intputPath, NULL, NULL);

    if (open_ret < 0)
    {
        av_log(NULL, AV_LOG_ERROR, "Open input file failed!\n");
        return -1;
    }
    // 打印视频信息
    av_dump_format(avContext, NULL, intputPath, NULL);

    // 打开输出文件
    FILE *outputFile = fopen(outputPath, "wb");
    if (outputPath == NULL)
    {
        av_log(NULL, AV_LOG_ERROR, "Open outputFile failed!\n");
        avformat_close_input(&avContext);
        return -1;
    }

    AVPacket av_package;
    av_init_packet(&av_package);
    av_package.size = 0;
    av_package.data = NULL;

    int bestStreamIndex = av_find_best_stream(avContext,
                                              AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
    if (bestStreamIndex < 0)
    {
        av_log(NULL, AV_LOG_ERROR, "Find best stream failed!\n");
        avformat_close_input(&avContext);
        fclose(outputFile);
        return -1;
    }

    // 拿到视频流的索引号
    while(av_read_frame(avContext, &av_package) >= 0){
        // 添加SPS/PPS数据
        if(av_package.stream_index == bestStreamIndex){
            h264_mp4toannexb(avContext, &av_package, outputFile);
        }
        av_packet_unref(&av_package);
    }

    avformat_close_input(&avContext);
    if (outputFile != NULL)
    {
        fclose(outputFile);
    }

    av_log(NULL, AV_LOG_INFO, "==========================\n");
    av_log(NULL, AV_LOG_INFO, "Detach video data success!\n");
    av_log(NULL, AV_LOG_INFO, "==========================\n");

    return 0;
}

extr_video.h

#ifndef __EXTR_VIDEO_H__
#define __EXTR_VIDEO_H__
#include <stdio.h>
#include <libavutil/log.h>
#include <libavformat/avio.h>
#include <libavformat/avformat.h>

#ifndef AV_WB32
#define AV_WB32(p, val)                  \
    do                                   \
    {                                    \
        uint32_t d = (val);              \
        ((uint8_t *)(p))[3] = (d);       \
        ((uint8_t *)(p))[2] = (d) >> 8;  \
        ((uint8_t *)(p))[1] = (d) >> 16; \
        ((uint8_t *)(p))[0] = (d) >> 24; \
    } while (0)
#endif

#ifndef AV_RB16
#define AV_RB16(x)                      \
    ((((const uint8_t *)(x))[0] << 8) | \
     ((const uint8_t *)(x))[1])
#endif

static int alloc_and_copy(AVPacket *out,
                          const uint8_t *sps_pps, uint32_t sps_pps_size,
                          const uint8_t *in, uint32_t in_size);

int h264_extradata_to_annexb(const uint8_t *codec_extradata, 
                            const int codec_extradata_size, 
                            AVPacket *out_extradata, int padding);

int h264_mp4toannexb(AVFormatContext *fmt_ctx, AVPacket *in, FILE *dst_fd);

#endif //!__EXTR_VIDEO_H__

extr_video.c

#include "extr_video.h"

static int alloc_and_copy(AVPacket *out,
                          const uint8_t *sps_pps, uint32_t sps_pps_size,
                          const uint8_t *in, uint32_t in_size)
{
    uint32_t offset = out->size;
    uint8_t nal_header_size = offset ? 3 : 4;
    int err;

    err = av_grow_packet(out, sps_pps_size + in_size + nal_header_size);
    if (err < 0)
        return err;

    if (sps_pps)
        memcpy(out->data + offset, sps_pps, sps_pps_size);
    memcpy(out->data + sps_pps_size + nal_header_size + offset, in, in_size);
    if (!offset)
    {
        AV_WB32(out->data + sps_pps_size, 1);
    }
    else
    {
        (out->data + offset + sps_pps_size)[0] =
            (out->data + offset + sps_pps_size)[1] = 0;
        (out->data + offset + sps_pps_size)[2] = 1;
    }

    return 0;
}

int h264_extradata_to_annexb(const uint8_t *codec_extradata, const int codec_extradata_size, AVPacket *out_extradata, int padding)
{
    uint16_t unit_size;
    uint64_t total_size = 0;
    uint8_t *out = NULL, unit_nb, sps_done = 0,
            sps_seen = 0, pps_seen = 0, sps_offset = 0, pps_offset = 0;
    const uint8_t *extradata = codec_extradata + 4;
    static const uint8_t nalu_header[4] = {0, 0, 0, 1};
    int length_size = (*extradata++ & 0x3) + 1; // retrieve length coded size, 用于指示表示编码数据长度所需字节数

    sps_offset = pps_offset = -1;

    /* retrieve sps and pps unit(s) */
    unit_nb = *extradata++ & 0x1f; /* number of sps unit(s) */
    if (!unit_nb)
    {
        goto pps;
    }
    else
    {
        sps_offset = 0;
        sps_seen = 1;
    }

    while (unit_nb--)
    {
        int err;

        unit_size = AV_RB16(extradata);
        total_size += unit_size + 4;
        if (total_size > INT_MAX - padding)
        {
            av_log(NULL, AV_LOG_ERROR,
                   "Too big extradata size, corrupted stream or invalid MP4/AVCC bitstream\n");
            av_free(out);
            return AVERROR(EINVAL);
        }
        if (extradata + 2 + unit_size > codec_extradata + codec_extradata_size)
        {
            av_log(NULL, AV_LOG_ERROR, "Packet header is not contained in global extradata, "
                                       "corrupted stream or invalid MP4/AVCC bitstream\n");
            av_free(out);
            return AVERROR(EINVAL);
        }
        if ((err = av_reallocp(&out, total_size + padding)) < 0)
            return err;
        memcpy(out + total_size - unit_size - 4, nalu_header, 4);
        memcpy(out + total_size - unit_size, extradata + 2, unit_size);
        extradata += 2 + unit_size;
    pps:
        if (!unit_nb && !sps_done++)
        {
            unit_nb = *extradata++; /* number of pps unit(s) */
            if (unit_nb)
            {
                pps_offset = total_size;
                pps_seen = 1;
            }
        }
    }

    if (out)
        memset(out + total_size, 0, padding);

    if (!sps_seen)
        av_log(NULL, AV_LOG_WARNING,
               "Warning: SPS NALU missing or invalid. "
               "The resulting stream may not play.\n");

    if (!pps_seen)
        av_log(NULL, AV_LOG_WARNING,
               "Warning: PPS NALU missing or invalid. "
               "The resulting stream may not play.\n");

    out_extradata->data = out;
    out_extradata->size = total_size;

    return length_size;
}

/**
 * @brief 设置SPS/PPS
 * 
 * @param fmt_ctx 
 * @param in 
 * @param dst_fd 
 * @return int 
 */
int h264_mp4toannexb(AVFormatContext *fmt_ctx, AVPacket *in, FILE *dst_fd)
{

    AVPacket *out = NULL;
    AVPacket spspps_pkt;

    int len;
    uint8_t unit_type;
    int32_t nal_size;
    uint32_t cumul_size = 0;
    const uint8_t *buf;
    const uint8_t *buf_end;
    int buf_size;
    int ret = 0, i;

    out = av_packet_alloc();

    buf = in->data;
    buf_size = in->size;
    buf_end = in->data + in->size;

    do
    {
        ret = AVERROR(EINVAL);
        if (buf + 4 > buf_end)
            goto fail;

        for (nal_size = 0, i = 0; i < 4; i++)
            nal_size = (nal_size << 8) | buf[i];

        buf += 4;
        unit_type = *buf & 0x1f;

        if (nal_size > buf_end - buf || nal_size < 0)
            goto fail;

        if (unit_type == 5)
        {

            h264_extradata_to_annexb(fmt_ctx->streams[in->stream_index]->codec->extradata,
                                     fmt_ctx->streams[in->stream_index]->codec->extradata_size,
                                     &spspps_pkt,
                                     AV_INPUT_BUFFER_PADDING_SIZE);

            if ((ret = alloc_and_copy(out,
                                      spspps_pkt.data, spspps_pkt.size,
                                      buf, nal_size)) < 0)
                goto fail;
           
        }else {
            if ((ret = alloc_and_copy(out, NULL, 0, buf, nal_size)) < 0)
                goto fail;
        }

        len = fwrite(out->data, 1, out->size, dst_fd);
        if (len != out->size)
        {
            av_log(NULL, AV_LOG_DEBUG, "warning, length of writed data isn't equal pkt.size(%d, %d)\n",
                   len,
                   out->size);
        }
        fflush(dst_fd);

    next_nal:
        buf += nal_size;
        cumul_size += nal_size + 4; //s->length_size;
    } while (cumul_size < buf_size);
fail:
    av_packet_free(&out);

    return ret;
}

7、mp4格式转flv

#include <libavutil/log.h>
#include <libavutil/timestamp.h>
#include <libavformat/avformat.h>

int main(int argc, char **argv)
{
    // 创建输出的format
    AVOutputFormat *ofmt = NULL;
    // 创建两个上下文
    AVFormatContext *ifmt_ctx = NULL, *ofmt_ctx = NULL;
    // 创建packet
    AVPacket pkt; v
    const char *in_filename, *out_filename;
    int ret, i;
    int stream_index = 0;
    int *stream_mapping = NULL;
    int stream_mapping_size = 0;

    in_filename  = "/Users/zchanglin/Downloads/input.mp4";
    out_filename = "/Users/zchanglin/Downloads/output.flv";


    if ((ret = avformat_open_input(&ifmt_ctx, in_filename, 0, 0)) < 0) {
        fprintf(stderr, "Could not open input file '%s'", in_filename);
        goto end;
    }
    // 如果当前输入文件没有任何流的信息直接返回
    if ((ret = avformat_find_stream_info(ifmt_ctx, 0)) < 0) {
        fprintf(stderr, "Failed to retrieve input stream information");
        goto end;
    }
    // 输出输出文件基本信息
    av_dump_format(ifmt_ctx, 0, in_filename, 0);
    
    // 创建输出文件上下文,是ffpmeg根据out_filename自主生成对应的
    avformat_alloc_output_context2(&ofmt_ctx, NULL, NULL, out_filename);
    if (!ofmt_ctx) {
        fprintf(stderr, "Could not create output context\n");
        ret = AVERROR_UNKNOWN;
        goto end;
    }
    // 获取输入文件流个数和大小并申请空间
    stream_mapping_size = ifmt_ctx->nb_streams;
    stream_mapping = av_mallocz_array(stream_mapping_size, sizeof(*stream_mapping));
    if (!stream_mapping) {
        ret = AVERROR(ENOMEM);
        goto end;
    }

    ofmt = ofmt_ctx->oformat;

    for (i = 0; i < ifmt_ctx->nb_streams; i++) {
        AVStream *out_stream;
        AVStream *in_stream = ifmt_ctx->streams[i];
        AVCodecParameters *in_codecpar = in_stream->codecpar;
	// 只要音频流A、视频流V、和字幕流S
        if (in_codecpar->codec_type != AVMEDIA_TYPE_AUDIO &&
            in_codecpar->codec_type != AVMEDIA_TYPE_VIDEO &&
            in_codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE) {
            stream_mapping[i] = -1;
            continue;
        }
	// 记录流个数
        stream_mapping[i] = stream_index++;
	// 创建输出的流文件
        out_stream = avformat_new_stream(ofmt_ctx, NULL);
        if (!out_stream) {
            fprintf(stderr, "Failed allocating output stream\n");
            ret = AVERROR_UNKNOWN;
            goto end;
        }
	// 从输入流中拷贝参数,如sps、pps、dts 一类到输出流中
        ret = avcodec_parameters_copy(out_stream->codecpar, in_codecpar);
        if (ret < 0) {
            fprintf(stderr, "Failed to copy codec parameters\n");
            goto end;
        }
        out_stream->codecpar->codec_tag = 0;
    }
    // 打印输出流的基本信息
    av_dump_format(ofmt_ctx, 0, out_filename, 1);

    if (!(ofmt->flags & AVFMT_NOFILE)) {
    
        ret = avio_open(&ofmt_ctx->pb, out_filename, AVIO_FLAG_WRITE);
        if (ret < 0) {
            fprintf(stderr, "Could not open output file '%s'", out_filename);
            goto end;
        }
    }

    // 写入头信息
    ret = avformat_write_header(ofmt_ctx, NULL);
    if (ret < 0) {
        fprintf(stderr, "Error occurred when opening output file\n");
        goto end;
    }

    while (1) {
        AVStream *in_stream, *out_stream;

        ret = av_read_frame(ifmt_ctx, &pkt);
        if (ret < 0)
            break;

        in_stream  = ifmt_ctx->streams[pkt.stream_index];
        // 查看流信息是否正确 前面也已经标注了
        if (pkt.stream_index >= stream_mapping_size ||
            stream_mapping[pkt.stream_index] < 0) {
            av_packet_unref(&pkt);
            continue;
        }
	// 对应好输入流和输出流
        pkt.stream_index = stream_mapping[pkt.stream_index];
        out_stream = ofmt_ctx->streams[pkt.stream_index];
        
        // 统一采样率刻度
        pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
        pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
        pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
        pkt.pos = -1;
        
	// 写入数据
        ret = av_interleaved_write_frame(ofmt_ctx, &pkt);
        if (ret < 0) {
            fprintf(stderr, "Error muxing packet\n");
            break;
        }
        // 释放包资源
        av_packet_unref(&pkt);
    }

    av_write_trailer(ofmt_ctx);

    av_log(NULL, AV_LOG_INFO, "==========================\n");
    av_log(NULL, AV_LOG_INFO, "Mp4 to flv success!\n");
    av_log(NULL, AV_LOG_INFO, "==========================\n");
end:

    avformat_close_input(&ifmt_ctx);

    /* close output */
    if (ofmt_ctx && !(ofmt->flags & AVFMT_NOFILE))
        avio_closep(&ofmt_ctx->pb);
    avformat_free_context(ofmt_ctx);

    av_freep(&stream_mapping);

    if (ret < 0 && ret != AVERROR_EOF) {
        fprintf(stderr, "Error occurred: %s\n", av_err2str(ret));
        return 1;
    }
    return 0;
}

8、裁剪mp4文件

#include <stdlib.h>
#include <libavutil/log.h>
#include <libavutil/timestamp.h>
#include <libavformat/avformat.h>

static void log_packet(const AVFormatContext *fmt_ctx, const AVPacket *pkt, const char *tag)
{
	AVRational *time_base = &fmt_ctx->streams[pkt->stream_index]->time_base;

	printf("%s: pts:%s pts_time:%s dts:%s dts_time:%s duration:%s duration_time:%s stream_index:%d\n",
		tag,
		av_ts2str(pkt->pts), av_ts2timestr(pkt->pts, time_base),
		av_ts2str(pkt->dts), av_ts2timestr(pkt->dts, time_base),
		av_ts2str(pkt->duration), av_ts2timestr(pkt->duration, time_base),
		pkt->stream_index);
}

int cut_video(double from_seconds, double end_seconds, const char* in_filename, const char* out_filename) {
	AVOutputFormat *ofmt = NULL;
	AVFormatContext *ifmt_ctx = NULL, *ofmt_ctx = NULL;
	AVPacket pkt;
	int ret, i;

	av_register_all();

	if ((ret = avformat_open_input(&ifmt_ctx, in_filename, 0, 0)) < 0) {
		fprintf(stderr, "Could not open input file '%s'", in_filename);
		goto end;
	}

	if ((ret = avformat_find_stream_info(ifmt_ctx, 0)) < 0) {
		fprintf(stderr, "Failed to retrieve input stream information");
		goto end;
	}

	av_dump_format(ifmt_ctx, 0, in_filename, 0);

	avformat_alloc_output_context2(&ofmt_ctx, NULL, NULL, out_filename);
	if (!ofmt_ctx) {
		fprintf(stderr, "Could not create output context\n");
		ret = AVERROR_UNKNOWN;
		goto end;
	}

	ofmt = ofmt_ctx->oformat;

	for (i = 0; i < ifmt_ctx->nb_streams; i++) {
		AVStream *in_stream = ifmt_ctx->streams[i];
		AVStream *out_stream = avformat_new_stream(ofmt_ctx, in_stream->codec->codec);
		if (!out_stream) {
			fprintf(stderr, "Failed allocating output stream\n");
			ret = AVERROR_UNKNOWN;
			goto end;
		}

		ret = avcodec_copy_context(out_stream->codec, in_stream->codec);
		if (ret < 0) {
			fprintf(stderr, "Failed to copy context from input to output stream codec context\n");
			goto end;
		}
		out_stream->codec->codec_tag = 0;
		if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
			out_stream->codec->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
	}
	av_dump_format(ofmt_ctx, 0, out_filename, 1);

	if (!(ofmt->flags & AVFMT_NOFILE)) {
		ret = avio_open(&ofmt_ctx->pb, out_filename, AVIO_FLAG_WRITE);
		if (ret < 0) {
			fprintf(stderr, "Could not open output file '%s'", out_filename);
			goto end;
		}
	}

	ret = avformat_write_header(ofmt_ctx, NULL);
	if (ret < 0) {
		fprintf(stderr, "Error occurred when opening output file\n");
		goto end;
	}

	// int64_t start_from = 8*AV_TIME_BASE;
	ret = av_seek_frame(ifmt_ctx, -1, from_seconds*AV_TIME_BASE, AVSEEK_FLAG_ANY);
	if (ret < 0) {
		fprintf(stderr, "Error seek\n");
		goto end;
	}

	int64_t *dts_start_from = malloc(sizeof(int64_t) * ifmt_ctx->nb_streams);
	memset(dts_start_from, 0, sizeof(int64_t) * ifmt_ctx->nb_streams);
	int64_t *pts_start_from = malloc(sizeof(int64_t) * ifmt_ctx->nb_streams);
	memset(pts_start_from, 0, sizeof(int64_t) * ifmt_ctx->nb_streams);

	while (1) {
		AVStream *in_stream, *out_stream;

		ret = av_read_frame(ifmt_ctx, &pkt);
		if (ret < 0)
			break;

		in_stream = ifmt_ctx->streams[pkt.stream_index];
		out_stream = ofmt_ctx->streams[pkt.stream_index];

		log_packet(ifmt_ctx, &pkt, "in");

		if (av_q2d(in_stream->time_base) * pkt.pts > end_seconds) {
			av_free_packet(&pkt);
			break;
		}

		if (dts_start_from[pkt.stream_index] == 0) {
			dts_start_from[pkt.stream_index] = pkt.dts;
			printf("dts_start_from: %s\n", av_ts2str(dts_start_from[pkt.stream_index]));
		}
		if (pts_start_from[pkt.stream_index] == 0) {
			pts_start_from[pkt.stream_index] = pkt.pts;
			printf("pts_start_from: %s\n", av_ts2str(pts_start_from[pkt.stream_index]));
		}

		// Copy过程
		pkt.pts = av_rescale_q_rnd(pkt.pts - pts_start_from[pkt.stream_index], in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
		pkt.dts = av_rescale_q_rnd(pkt.dts - dts_start_from[pkt.stream_index], in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
		if (pkt.pts < 0) {
			pkt.pts = 0;
		}
		if (pkt.dts < 0) {
			pkt.dts = 0;
		}
		pkt.duration = (int)av_rescale_q((int64_t)pkt.duration, in_stream->time_base, out_stream->time_base);
		pkt.pos = -1;
		log_packet(ofmt_ctx, &pkt, "out");
		printf("\n");

		ret = av_interleaved_write_frame(ofmt_ctx, &pkt);
		if (ret < 0) {
			fprintf(stderr, "Error muxing packet\n");
			break;
		}
		av_free_packet(&pkt);
	}
	free(dts_start_from);
	free(pts_start_from);

	av_write_trailer(ofmt_ctx);
end:

	avformat_close_input(&ifmt_ctx);

	// 释放资源
	if (ofmt_ctx && !(ofmt->flags & AVFMT_NOFILE))
		avio_closep(&ofmt_ctx->pb);
	avformat_free_context(ofmt_ctx);

	if (ret < 0 && ret != AVERROR_EOF) {
		fprintf(stderr, "Error occurred: %s\n", av_err2str(ret));
		return 1;
	}

	return 0;
}

int main(int argc, char *argv[]) {
	const char* inputFile = "C:\\Users\\changlin\\Desktop\\input.mp4";
	const char* outputFile = "C:\\Users\\changlin\\Desktop\\output.mp4";
	cut_video(6, 10, inputFile, outputFile);
	return 0;
}

Visual Studio下如何引入FFmpeg

如果是 Visual Studio 开发环境,首先可以确定的是 Visual Studio2017 已经支持 CMake 了,通过编写CMakeLists.txt 照样可以达到和 Mac 或者 Linux 一样的效果,不过常规方式引入也是OK的。

如果下载的64位的库,那么生成的目标平台也必须是64位的:

然后点击项目属性,把FFmpeg包含的头文件路径配置进去:

添加链接库目录:

添加需要使用的库,以使用静态库为例:

上述配置好以后,main.c测试一下,OK

#include <stdio.h>
#include <libavutil/avutil.h>

int main()
{
	av_log_set_level(AV_LOG_INFO);
	av_log(NULL, AV_LOG_INFO, "Hello FFmpeg!\n");
	return 0;
}

CMake如何引入FFmpeg

虽然前面用到了CMake去引入FFmpeg,但是是通过设置 C_FLAG/C_XX_FALG 属性来设置的链接库,这种方式就等于在链接的时候使用了-L 选项,其实这算是一种方式,但却不是一种优雅的方式,很多时候CMake在设置了 target_link_libraries却没有生效,其实是因为设置顺序的问题,对于一个目标文件,在 add_executable 之前就应该设置link_directories,下面是一个引入FFmpeg日志库的 Demo:

cmake_minimum_required (VERSION 3.8)

# 设置工程名称
project ("FFmpegAPI")

# 设置FFmpeg的头文件目录和库目录
set(ffmpeg_include_dir "D:\\FFmpeg\\ffmpeg-4.4-full_build-shared\\include")
set(ffmpeg_lib_dir "D:\\FFmpeg\\ffmpeg-4.4-full_build-shared\\lib")

# 添加头文件目录
include_directories(ffmpeg_include_dir)

# 添加链接库目录
link_directories(ffmpeg_lib_dir)

# 将源代码添加到此项目的可执行文件
add_executable (FFmpegAPI "FFmpegAPI.c" )

# 目标依赖库
target_link_libraries(
        FFmpegAPI
        avutil
)

所以应该在add_executable 之前就应该设置link_directories,这一点是非常重要的。