FFMPEG函数/扫盲

1,714 阅读35分钟

获取错误信息

av_strerror(code, a, b);

code:错误码

a:char * 用来赋值错误信息

b:a的大小

avformat_open_input

avformat_open_input()  这样的函数打开或创建一个媒体文件时,FFmpeg 会分析文件并填充一些字段,其中就包括 nb_streams

avformat_find_stream_info

avformat_find_stream_info是FFmpeg库中的一个函数,它的主要作用是读取输入的媒体文件,并获取其中的流信息。

当你打开一个媒体文件时,你可能需要知道这个文件中包含了哪些流(例如音频流、视频流等),以及这些流的一些参数(例如编码格式、分辨率、帧率等)。avformat_find_stream_info函数就是用来获取这些信息的。

这个函数会读取文件的一部分数据,并尝试解码,以确定流的参数。这个过程可能需要一些时间,因为它可能需要读取和解码大量的数据。

这个函数的原型如下:

int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);

其中,ic是一个指向AVFormatContext结构体的指针,这个结构体包含了媒体文件的信息。options是一个指向AVDictionary的指针,你可以通过这个参数传递一些选项给函数。

这个函数的返回值是一个整数,如果成功,返回值将为0。如果失败,返回值将为一个负数,表示错误代码。

初始化的属性

avformat_find_stream_info函数会根据媒体文件的类型和内容,填充AVFormatContext结构体中的不同字段。以下是一些可能被填充的字段,以及它们的解释和例子:

  1. streams:这是一个指向AVStream指针数组的指针。每个AVStream都代表了媒体文件中的一个流(例如音频流或视频流)。avformat_find_stream_info函数会为每个流分配一个AVStream结构体,并将其添加到数组中。
  2. duration:这是媒体文件的总时长,以streams的time_base为单位。
  3. start_time:这是媒体文件的开始时间,以streams的time_baseE为单位。
  4. bit_rate:这是媒体文件的比特率。

对于每个AVStreamavformat_find_stream_info函数也会填充许多字段,包括:

  1. codecpar:这是一个AVCodecParameters结构体,包含了流的编解码器相关的参数,例如编解码器类型、编解码器ID、比特率、帧率、视频的宽度和高度等。
  2. start_time:这是流的开始时间,以streams的time_base为单位。
  3. duration:这是流的时长,以streams的time_base为单位。
  4. nb_frames:如果知道的话,这是流中的帧数。

以上只是AVFormatContext中的一部分字段,avformat_find_stream_info函数可能会填充更多的字段,具体取决于媒体文件的类型和内容。例如:

  1. iformat:这是一个指向AVInputFormat的指针,表示媒体文件的格式。例如,如果媒体文件是一个MP4文件,那么iformat可能会指向一个表示MP4格式的AVInputFormat
  2. ctx_flags:这是一个标志位字段,用于存储一些状态信息。例如,如果媒体文件已经被读取过,那么ctx_flags可能会包含AVFMTCTX_NOHEADER标志。
  3. packet_buffer 和 parse_queue:这两个字段是用于存储媒体文件中的数据包的。avformat_find_stream_info函数会读取媒体文件中的数据包,并将它们存储在这两个字段中。
  4. probesize 和 max_analyze_duration:这两个字段用于控制avformat_find_stream_info函数的行为。probesize表示函数应该读取多少数据来分析流的信息,max_analyze_duration表示函数应该花费多少时间来分析流的信息。
  5. metadata:这是一个AVDictionary,用于存储媒体文件的元数据。例如,如果媒体文件是一个MP3文件,那么metadata可能会包含歌曲的标题、艺术家、专辑等信息。
  6. start_time_realtime:这是媒体文件的实际开始时间,以微秒为单位。这个字段通常用于实时流,例如RTSP流。
  7. fps_probe_size:这个字段用于控制函数应该读取多少帧来估计视频流的帧率。
  8. error_recognition:这个字段用于控制函数在遇到错误时的行为。

codecpar实际上是AVStream结构体中的一个字段,它是一个AVCodecParameters结构体,用于存储流的编解码器相关的参数。

AVCodecParameters包含了许多关于流的编解码器的信息,例如编解码器类型(音频、视频、字幕等)、编解码器ID(例如H.264、AAC等)、比特率、帧率、视频的宽度和高度等。这些信息对于后续的解码操作非常重要,因为解码器需要这些信息来正确地解码流中的数据。

例如,如果一个AVStream代表了一个视频流,那么它的codecpar可能会包含以下信息:

  • codec_typeAVMEDIA_TYPE_VIDEO,表示这是一个视频流。
  • codec_idAV_CODEC_ID_H264,表示编解码器是H.264。
  • bit_rate:比特率,例如500000。
  • widthheight:视频的宽度和高度,例如1280x720。
  • frame_rate:帧率,例如30帧/秒。

这些信息都是avformat_find_stream_info函数从媒体文件中读取并填充到AVStreamcodecpar中的。

还可获取到视频类型

查看AVFormatContext结构体中的iformat字段。iformat字段是一个指向AVInputFormat的指针,AVInputFormat结构体中包含了描述媒体文件格式的信息。

例如,你可以通过以下方式获取视频的类型:

AVFormatContext *formatContext;
// Assume formatContext is initialized and opened

const char *formatName = formatContext->iformat->name;

在这个例子中,formatName将会是一个字符串,表示媒体文件的格式。例如,如果媒体文件是一个MP4文件,那么formatName将会是"mp4"。如果媒体文件是一个MPEG文件,那么formatName将会是"mpeg"。

avcodec_find_decoder

用来获取一种媒体类型的解码器。

AVCodec *codec = avcodec_find_decoder(AVCodecID)

AVCodecID是一个enum类型,包含着许多媒体文件格式:

image.png

AVCodecID可以在av_context->streams[audioIndex]->codecpar中获取到。而codecpar在avformat_find_stream_info调用后已经赋值了。

avcodec_open2

avcodec_open2是FFmpeg库中的一个函数,它用于打开编解码器并初始化编解码器上下文。这个函数的原型如下:

int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);

这个函数接受三个参数:

  1. avctx:一个指向AVCodecContext的指针,这个结构体包含了编解码器的上下文信息。这个上下文在调用avcodec_open2函数之后会被初始化。
  2. codec:一个指向AVCodec的指针,表示要打开的编解码器。
  3. options:一个指向AVDictionary的指针,可以用于传递一些选项给编解码器。这个参数可以是NULL,表示不传递任何选项。

avcodec_open2函数的返回值是一个整数,如果函数成功执行,返回值将会是0。如果函数执行失败,返回值将会是一个负数,表示错误代码。

在调用avcodec_open2函数之后,你就可以使用avctx来进行编解码操作了。例如,你可以使用avcodec_decode_audio4函数来解码音频数据,或者使用avcodec_encode_audio2函数来编码音频数据。

请注意,当你不再需要avctx时,你应该调用avcodec_close函数来关闭编解码器并释放相关资源。

初始化AVCodecContext


AVCodecContext *av_codec = avcodec_alloc_context3(NULL);  
avcodec_parameters_to_context(av_codec,av_context->streams[audioIndex]->codecpar); 
AVCodec *codec = avcodec_find_decoder(av_codec->codec_id);  
avcodec_open2(av_codec,codec,NULL);

avcodec_parameters_to_contextavcodec_open2两个函数的作用是不同的,它们初始化AVCodecContext的不同部分。

avcodec_parameters_to_context函数用于将AVCodecParameters结构体中的字段复制到AVCodecContext中。AVCodecParameters通常从媒体文件的流(AVStream)中获取,它包含了编解码器的类型、ID、比特率、帧率等信息。这个函数通常在打开编解码器之前调用,以初始化编解码器上下文的一些参数。

然后,avcodec_open2函数用于打开编解码器并进一步初始化AVCodecContext。这个函数会根据提供的编解码器和选项来设置AVCodecContext的其他字段,例如函数指针、时间基、编解码器的状态等。这个函数还会检查AVCodecContext的参数是否正确,并可能会修改一些参数以适应编解码器的需求。

因此,这两个函数都是初始化AVCodecContext的重要步骤,但它们初始化的内容是不同的。你应该首先调用avcodec_parameters_to_context来设置编解码器参数,然后调用avcodec_open2来打开编解码器。

avcodec_open2还会将AVCodecContext的编解码器设置为参数2传递来的codec

涉及到的函数的源码位置:libavcodec\utils.c

AVPacket

AVPacket是FFmpeg库中的一个核心结构体,它主要用于存储编码后的数据(也就是压缩数据)。在解码过程中,AVPacket通常用于从媒体文件中读取数据,然后传递给解码器。在编码过程中,AVPacket则用于接收编码器输出的数据。

AVPacket结构体包含了一些重要的字段,如下:

  • datasize:这两个字段分别表示压缩数据的指针和大小。data指向的内存通常是通过av_malloc函数分配的,需要使用av_packet_unrefav_free_packet函数来释放。
  • ptsdts:这两个字段表示压缩数据的显示时间和解码时间,单位是时间基(time base)。
  • stream_index:这个字段表示压缩数据所属的流的索引,对应于AVFormatContextstreams数组。
  • flags:这个字段包含了一些标志,例如AV_PKT_FLAG_KEY表示这个压缩数据是关键帧。

在使用AVPacket之前,你应该调用av_packet_allocav_init_packet(av_init_packet设置AVPacket为默认值,但这些值是无效的,无法被正常使用)函数来初始化它。当你不再需要AVPacket时,你应该调用av_packet_unrefav_free_packet函数来释放它。

压缩数据

压缩数据是指通过某种压缩算法处理后的数据。在多媒体处理中,压缩是一种常见的技术,用于减小音频、视频或图像数据的大小,以便于存储或传输。

在音频和视频编码中,压缩通常涉及到两种类型的压缩:有损压缩和无损压缩。

  • 有损压缩:这种压缩方法会丢失一些原始数据,以换取更高的压缩比。例如,MP3和AAC是有损压缩的音频编码格式,而H.264和H.265是有损压缩的视频编码格式。有损压缩通常可以大大减小数据的大小,但是可能会降低音频或视频的质量。
  • 无损压缩:这种压缩方法不会丢失任何原始数据,因此可以完全恢复到原始的音频或视频。例如,FLAC是无损压缩的音频编码格式,而HuffYUV是无损压缩的视频编码格式。无损压缩的压缩比通常低于有损压缩,但是音频或视频的质量不会降低。

在FFmpeg中,压缩数据通常存储在AVPacket结构体中,这些数据可以直接写入到媒体文件中,或者传递给解码器进行解码。

源码位置:libavcodec\avpacket.c

AVFrame

AVFrame是FFmpeg库中的一个核心结构体,它主要用于存储解码后的数据(也就是未压缩的音频或视频数据)。在解码过程中,解码器会将解码后的数据填充到AVFrame中。在编码过程中,你需要将未压缩的音频或视频数据放入AVFrame,然后传递给编码器。

AVFrame结构体包含了一些重要的字段,如下:

  • datalinesize:这两个字段分别表示未压缩数据的指针和每行的大小。对于音频数据,data通常指向一个包含所有音频样本的数组。对于视频数据,data数组中的每个元素通常指向一个图像平面。
  • nb_samplessample_rate:这两个字段用于音频数据,分别表示音频样本的数量和采样率。
  • widthheightformat:这三个字段用于视频数据,分别表示图像的宽度、高度和像素格式。
  • pts:这个字段表示未压缩数据的显示时间,单位是时间基(time base)。

在使用AVFrame之前,你应该调用av_frame_alloc函数来分配一个AVFrame。当你不再需要AVFrame时,你应该调用av_frame_free函数来释放它。

AVFrame和AVPacket的联系

在FFmpeg中,视频或音频的解码和编码过程大致如下:

  1. 解码过程:

    • 读取包含压缩数据的AVPacket
    • AVPacket传递给解码器。
    • 解码器解码AVPacket中的压缩数据,将解码后的未压缩数据(也就是原始的音频或视频数据)填充到AVFrame中。
  2. 编码过程:

    • AVFrame中读取未压缩的音频或视频数据。
    • AVFrame传递给编码器。
    • 编码器编码AVFrame中的未压缩数据,生成压缩数据,并将这些数据填充到新的AVPacket中。

所以,AVPacket主要用于存储和传递压缩数据,而AVFrame主要用于存储和传递未压缩数据。在解码和编码过程中,这两个结构体都起到了桥梁的作用,使得解码器和编码器能够正确地处理音频或视频数据。

注:如果你只是想播放一个媒体文件,那么你只需要进行解码的过程。解码器会将媒体文件中的压缩数据转换为未压缩的数据,然后这些未压缩的数据可以直接被播放设备(如电视、电脑、手机等)解析和显示。

编码的过程主要是在你需要存储或传输未压缩的音频或视频数据时使用。例如,如果你修改了视频的内容(如改变了分辨率或格式),并且你想保存修改后的视频,那么你就需要将修改后的未压缩数据进行编码,生成压缩数据,以便有效地存储和传输。

所以,如果你只是播放媒体文件,而不需要修改或保存它,那么你只需要解码,而不需要编码。

AVFrame并不仅仅存在于视频中,音频处理过程中同样会使用到AVFrame。在FFmpeg中,无论是音频还是视频,解码后的数据都会被存储在AVFrame结构体中。

对于音频,AVFrame可能会包含一段时间内的所有音频样本。例如,对于44.1kHz的立体声音频,一个AVFrame可能会包含1024个样本,这意味着它包含了大约23毫秒的音频数据。

对于视频,AVFrame通常会包含一个完整的视频帧。这可能是一个I帧(关键帧),也可能是P帧或B帧(预测帧)。

PCM ffmpeg解码音频后的生成数据格式

PCM(Pulse Code Modulation)通常是音频数据经过解码后的格式。当你使用FFmpeg或其他音频解码库将压缩的音频文件(如MP3或AAC文件)解码时,输出的通常是PCM数据。这是因为PCM是一种无损的、可以直接被音频设备播放的音频格式。

至于视频,PCM并不直接应用于视频数据。视频通常包含两部分数据:视频数据和音频数据。视频数据通常使用像H.264或VP9这样的编码格式进行编码,而音频数据则可能使用像PCM这样的编码格式进行编码。所以,当我们说一个视频文件包含PCM数据时,我们通常是指该视频文件的音频部分是以PCM格式存储的。

总的来说,PCM主要用于音频数据,而不是视频数据。当你使用FFmpeg解码音频文件时,你通常会得到PCM数据。

注:你可以使用FFmpeg或其他音频解码库将MP3音频文件解码为无损的PCM格式。然而,需要注意的是,虽然PCM是一种无损格式,但这并不意味着解码后的音频质量会比原始的MP3文件更好。

MP3是一种有损压缩格式,这意味着在压缩过程中,一些音频信息会被丢弃,以减小文件大小。这些丢弃的信息通常是人耳难以察觉的,所以在大多数情况下,MP3文件的音质对于大多数听众来说已经足够好了。

当你将MP3文件解码为PCM格式时,解码器并不能恢复那些在压缩过程中丢弃的信息。所以,虽然解码后的PCM文件是无损的,但它的音质并不会比原始的MP3文件更好。实际上,它的音质应该和原始的MP3文件相同。

YUV 视频解码后的数据格式

视频解码后的数据通常是以未压缩的像素数据形式存在的,这种格式通常被称为原始视频数据或者YUV数据。YUV是一种颜色编码系统,Y代表亮度(Luminance),U和V代表色度(Chrominance)。在YUV格式中,亮度和色度是分开存储的,这使得它能够更有效地进行视频压缩和传输。

在FFmpeg中,解码后的视频数据通常存储在AVFrame结构体中,其中的data字段就是存储这些原始像素数据的地方。AVFrame中的其他字段,如widthheightformat,则用于描述这些像素数据的属性,例如它们的宽度、高度和像素格式。

需要注意的是,这些解码后的数据通常不能直接用于播放或显示,因为它们是未压缩的,数据量非常大。在实际使用中,这些数据通常会被送入一个视频渲染器,由渲染器将这些数据转换为可以在屏幕上显示的图像。

读取一段原始数据到AVPacket

av_read_frame

return 0 if OK, < 0 on error or end of file

while (av_read_frame(fmt_ctx, packet) == 0) {
    ...处理..解码...
}

解码


while(av_read_frame(av_context,packet)==0){  
    avcodec_send_packet(av_codec,packet);  
    avcodec_receive_frame(av_codec,frame);  
}

可能会存在多个音频类型的stream

一个媒体文件中确实可能存在多个音频流(AVMEDIA_TYPE_AUDIO)。这种情况在多语言或多音轨的媒体文件中尤为常见。例如,一个电影文件可能包含原声音轨、配音音轨、导演解说音轨等,这些都会作为独立的音频流存在。

在使用FFmpeg处理这种文件时,你可以通过检查每个流的AVStream.codecpar.codec_type字段来确定其类型。如果这个字段的值为AVMEDIA_TYPE_AUDIO,那么这个流就是音频流。

然后,你可以根据需要选择处理哪个音频流,或者同时处理多个音频流。

在FFmpeg中,音轨的类型(例如,是否为解说音轨)通常不会直接标记在音轨本身。这是因为音轨的类型更多地依赖于音频内容本身,而不是音频流的元数据。

然而,一些媒体文件可能会在元数据中包含这种信息。例如,MKV(Matroska)格式就支持在音轨的元数据中标记音轨的类型。在这种情况下,你可以通过检查音轨的元数据来确定其类型。

在FFmpeg中,你可以使用av_dict_get函数来获取流的元数据。例如,你可以这样获取一个流的语言:

AVDictionaryEntry *lang = av_dict_get(stream->metadata, "language", NULL, 0);
if (lang)
    printf("Language: %s\n", lang->value);

然而,要注意的是,这种方法并不总是可行的,因为并非所有的媒体文件都会在元数据中包含音轨的类型信息。在这种情况下,你可能需要使用其他方法来确定音轨的类型,例如通过人工听识别或使用音频处理算法进行分析。

也可能会存在多个视频轨

一个媒体文件中也可能存在多个视频流(AVMEDIA_TYPE_VIDEO)。这种情况虽然不常见,但在某些特殊的媒体文件中,例如一些多视角的视频或者包含额外内容(如特效等)的视频中,可能会有多个视频流。

在使用FFmpeg处理这种文件时,你可以通过检查每个流的AVStream.codecpar.codec_type字段来确定其类型。如果这个字段的值为AVMEDIA_TYPE_VIDEO,那么这个流就是视频流。

然后,你可以根据需要选择处理哪个视频流,或者同时处理多个视频流。

在FFmpeg中,视频流本身并不直接包含类型或名字这样的信息。然而,一些媒体文件格式(如MKV或MP4)可能会在元数据中包含这样的信息。这些信息可能包括流的语言、描述、标题等。

你可以使用FFmpeg的API来获取这些元数据。例如,你可以使用av_dict_get函数来获取流的元数据。以下是一个示例,展示了如何获取流的语言和标题:

AVDictionaryEntry *lang = av_dict_get(stream->metadata, "language", NULL, 0);
if (lang)
    printf("Language: %s\n", lang->value);

AVDictionaryEntry *title = av_dict_get(stream->metadata, "title", NULL, 0);
if (title)
    printf("Title: %s\n", title->value);

然而,需要注意的是,并非所有的媒体文件都会在元数据中包含这样的信息。在这种情况下,你可能需要使用其他方法来识别视频流的类型或名字,例如通过分析视频内容。

ffmpeg中的媒体类型

    case AVMEDIA_TYPE_VIDEO:      return "video";//视频流
    case AVMEDIA_TYPE_AUDIO:      return "audio";//音频流
    case AVMEDIA_TYPE_DATA:       return "data";//还不知道干啥用的,应该是不透明的信息
    case AVMEDIA_TYPE_SUBTITLE:   return "subtitle";//字幕流
    case AVMEDIA_TYPE_ATTACHMENT: return "attachment";//附件流类型,用于存储与媒体相关的附加数据。

linesize和stride什么意思

linesize

linesize 在 AVFrame 结构中是一个非常重要的属性,它表示的是每一行图像数据的字节大小,而不是像素大小。在视频编码和解码中,图像数据通常会被存储在一个二维数组中,其中每一行可能包含多个像素。linesize 的值就是这个数组中每一行的长度,以字节为单位。

这个值的大小取决于多个因素,包括图像的宽度、每个像素的位深度(即每个像素使用多少位来表示),以及可能的内存对齐要求。例如,如果一个图像的宽度是 1280 像素,每个像素是 24 位(即 3 字节),那么 linesize 的值就可能是 1280 * 3 = 3840 字节。但是,如果内存对齐要求使得每行必须是 4 字节的倍数,那么 linesize 的值可能就会被增加到 3844 字节。

总的来说,linesize 是一个非常重要的属性,它可以帮助我们理解和操作图像数据。

stride

在JNI开发中,ANativeWindow_Buffer是一个结构体,用于描述一个图像帧的属性。其中的stride字段表示的是图像的行跨度。

在图像处理中,"stride"通常指的是一行像素(或一列)在内存中的实际宽度,以像素为单位。这可能大于图像的实际宽度,因为为了内存对齐,每行的末尾可能会有一些填充像素。例如,如果一个图像的宽度是5像素,但是系统要求每行必须是4像素的倍数,那么stride就会是8,而不是5。

因此,在JNI开发中,ANativeWindow_Buffer中的stride字段就是用来表示这个行跨度的。当你需要访问图像数据时,不能只依赖于图像的宽度。

av_image_fill_arrays

av_image_fill_arrays是FFmpeg库中的一个函数,它的主要作用是设置数据指针和行大小以便于填充图像平面。

这个函数接收以下参数:

  • 数据指针数组,这个数组将被填充以指向图像平面的数据。
  • 每个平面的行大小数组。
  • 图像数据缓冲区。
  • 图像的像素格式。
  • 图像的宽度。
  • 图像的高度。
  • 对齐。

当你有一个包含图像数据的缓冲区,并且你知道这个图像的像素格式、宽度和高度时,你可以使用av_image_fill_arrays函数来获取指向图像各个平面的数据的指针,以及每个平面的行大小。这对于进一步处理图像,例如解码、转换格式或显示等,是非常有用的。

请注意,这个函数不会分配或释放任何内存,它只是设置指针和行大小。如果你需要分配内存,你可以使用av_image_alloc函数。

举个例子:


auto rgba_frame = av_frame_alloc();  
av_image_fill_arrays(rgba_frame->data, rgba_frame->linesize,  
    ragb_data,  
    AV_PIX_FMT_RGBA,  
    frame->width, frame->height, 1);

当向ragb_data中填充数据时,我们也可以通过rgba_frame->data获取到,因为rgba_frame->data指向了ragb_data,但是rgba_frame->data是一个二维地址,ragb_data是一维地址。我们通过av_image_fill_arrays这个函数可以完成这个功能,同时还对传进来的rgba_frame->linesize也进行了赋值,这些赋值操作依据是根据图像的格式、宽度、高度实现的。

av_image_fill_arrays源码: image.png

YUV转RGBA,用来显示到显示屏上


SwsContext *sws = sws_getContext(frame->width, frame->height, av_codec->pix_fmt,  

frame->width, frame->height, AV_PIX_FMT_RGBA,  
        0,  
        nullptr, nullptr, nullptr);  
    
    sws_scale(sws, frame->data, frame->linesize, 0, frame->height,  
    rgba_frame->data, frame->linesize);  
  
    sws_freeContext(sws);


YUV和RGBA的linesize区别

注意了:

RGBA是一种直接色彩模型,每个像素由四个部分组成:红色(R)、绿色(G)、蓝色(B)和透明度(A)。在内存中,这四个部分通常连续存储,形成一个32位的数据单元。因此,对于RGBA图像,linesize数组只有第一个元素是有效的,表示一行像素的字节大小。

相比之下,YUV是一种间接色彩模型,它将颜色信息分为亮度信息(Y)和色度信息(UV)。在YUV模型中,色度信息通常被子采样,也就是说,多个像素可能共享同一组UV值。因此,Y、U、V三个分量可能会分别存储在不同的内存区域。对于YUV图像,linesize数组的前三个元素通常都是有效的,分别表示Y、U、V三个分量一行的字节大小。

先介绍下SwsContext是干嘛的!

SwsContext

SwsContextffmpeg 中的一个结构体类型,用于表示一个视频帧在进行颜色空间转换时使用的上下文信息。 在 ffmpeg 中,颜色空间转换通常是由 sws_getContext()sws_scale() 函数完成的。sws_getContext() 函数用于创建一个新的 SwsContext 对象,并配置它以执行特定的颜色空间转换操作。而 sws_scale() 函数则使用这个上下文对象来实际地执行颜色空间转换操作。 具体来说,SwsContext 包含了一些关于输入和输出颜色空间的信息,例如源颜色空间、目标颜色空间、变换类型等。这些信息被用于计算出如何将源帧的颜色值转换为目标帧的颜色值。 通过使用 SwsContextffmpeg 可以高效地进行颜色空间转换,而不必每次都重新计算转换公式。

sws_scale

sws_scaleffmpeg 中的一个函数,用于进行图像的缩放和格式转换。 sws_scale 的基本语法如下:

int sws_scale(SwsContext *src_ctx,
              const uint8_t **src,
              int srcStride[],
              int srcSliceHeight,
              int dstHeight,
              uint8_t **dst,
              int dstStride[]);

其中,src_ctx 是一个指向 SwsContext 结构体的指针,用于指定要使用的上下文信息。src 是一个指向输入图像数据的指针数组,每个元素都是一个指向输入图像行的指针。srcStride 是一个包含输入图像每一行的数据长度的整数数组。srcSliceHeight 是输入图像的高度,如果输入图像是一块连续的矩形区域,则可以设置为 -1dstHeight 是输出图像的高度。dst 是一个指向输出图像数据的指针数组,每个元素都是一个指向输出图像行的指针。dstStride 是一个包含输出图像每一行的数据长度的整数数组。 sws_scale 函数会根据上下文信息和输入/输出参数,将输入图像转换为输出图像。它可以进行多种类型的转换,包括但不限于颜色空间转换、尺寸调整、滤镜应用等。 需要注意的是,sws_scale 函数返回一个整数,表示是否成功进行了转换。如果转换失败,则返回非零值。此外,由于 sws_scale 函数可能需要大量的内存和计算资源,因此在使用时应尽量避免频繁调用。

假如上面的代码中av_codec->pix_fmtYUV420P,那么上面代码的作用就是将YUV420P转成RGBA,同时高度宽度改变成传递进去的参数。

注: YUV420p和RGBA是两种不同的颜色空间或颜色编码系统。YUV420p是一种常用于视频编码的格式,其中Y表示亮度信息,UV表示色度信息,420则表示色度信息的采样方式。而RGBA则是一种常用于图像显示的格式,其中R、G、B分别表示红绿蓝三原色,A表示透明度。

显示器,如电脑屏幕或电视,通常使用的颜色空间是RGB或RGBA。这是因为显示器的物理原理是通过发射或过滤红绿蓝三原色的光来产生颜色。因此,显示器需要的输入信号也必须是RGB或RGBA格式的。

虽然有些专业的视频设备或软件可以直接处理YUV格式的信号,但是大部分常见的显示设备,如电脑屏幕和电视,还是只能接收RGB或RGBA格式的信号。因此,如果你有一份YUV420p格式的视频数据,想要在这些设备上显示,就需要先将其转换为RGB或RGBA格式。

转换过程涉及到一些数学运算,可以通过专门的算法或工具完成,比如FFmpeg库中的SwsContext相关函数就提供了这样的功能。

显示到安卓手机上所用到的ANativeWindow

如果要使用ANativeWindow,需要导入库和头文件

cmake中:


find_library(android android)
target_link_libraries(你的库名字 ${android})

源文件中:

#include <android/native_window.h>  
#include <android/native_window_jni.h>

ok

创建一个ANativeWindow,你需要在java层传进来一个Surface.

ANativeWindow *nativeWindow = ANativeWindow_fromSurface(env, surface);

传递数据前,先对其初始化:

ANativeWindow_setBuffersGeometry(nativeWindow, frame->width, frame->height,  
WINDOW_FORMAT_RGBA_8888);

创建和初始化数据缓存:

ANativeWindow_Buffer windowBuffer;  //创建ANativeWindow_Buffer
ANativeWindow_lock(nativeWindow, &windowBuffer, NULL);//初始化windowBuffer,比如设置windowBuffer的stride

ANativeWindow_lock做了什么,我们还需要去源码里找答案:

image.png

源码地址:android.googlesource.com/platform/fr…

在源码中发现ANativeWindow实际上是一个Suface的父类,Surface的cpp类所在位置为:android.googlesource.com/platform/fr…

Suface的父类,用到了模板代码 android.googlesource.com/platform/fr…

实现地方在: android.googlesource.com/platform/fr…

而lock函数正是给SurfaceInfo进行了赋值。

上截图:

image.png

后来ANativeWindow_Buffer又通过SurfaceInfo给自己的属性赋值。

我们这就知道了ANativeWindow_lock除了给当前缓存加锁防止异步操作外,还执行了对ANativeWindow_Buffer的赋值操作。

向缓存写入图像数据

for (int i = 0; i < frame->height; i++) {  
    memcpy(dstData + i * dstStride, ragb_data + i * frame->linesize[0],  
    frame->linesize[0]);  
}

解锁缓存准备下次赋值

ANativeWindow_unlockAndPost(nativeWindow);

通过ffmpeg加载视频到屏幕上 完整代码


jlong openVideo(char *path, ANativeWindow *nativeWindow) {  
auto av_context = avformat_alloc_context();  
  
auto success = avformat_open_input(&av_context, path, NULL, NULL);  
  
if (success == 0) {  
  
  
int streamIndex = avformat_find_stream_info(av_context, NULL);  
  
success = 1;  
  
  
auto findRe = streamIndex;  
  
if (findRe < 0) {  
return 10;  
}  
  
int audioIndex = -1;  
  
int sCount = av_context->nb_streams;  
  
for (int i = 0; i < sCount; i++) {  
  
if (av_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {  
audioIndex = i;  
break;
}  
  
}  
  
  
if (audioIndex != -1) {  
  
AVCodecContext *av_codec = avcodec_alloc_context3(NULL);  
avcodec_parameters_to_context(av_codec, av_context->streams[audioIndex]->codecpar);  
  
AVCodec *codec = avcodec_find_decoder(av_codec->codec_id);  
  
avcodec_open2(av_codec, codec, NULL);  
  
AVPacket *packet = av_packet_alloc();//存储原数据  
  
AVFrame *frame = av_frame_alloc();//存储解码后的数据  
  
AVDictionaryEntry *tag = NULL;  
  
  
while (av_read_frame(av_context, packet) == 0) {  
if (packet->stream_index == audioIndex) {  
avcodec_send_packet(av_codec, packet);  
avcodec_receive_frame(av_codec, frame);  
  
ANativeWindow_setBuffersGeometry(nativeWindow, frame->width, frame->height,  
WINDOW_FORMAT_RGBA_8888);  
  
// 锁定 ANativeWindow 的缓冲区  
ANativeWindow_Buffer windowBuffer;  
ANativeWindow_lock(nativeWindow, &windowBuffer, NULL);  
  
// 将 AVFrame 的数据复制到 ANativeWindow 的缓冲区  
uint8_t *dstData = (uint8_t *) windowBuffer.bits;  
// windowBuffer.stride *  
int dstStride = windowBuffer.stride * 4; // RGBA 格式,每个像素占用 4 个字节  
  
int buffer_size = av_image_get_buffer_size(AV_PIX_FMT_RGBA,  
frame->width, frame->height, 1);  
  
uint8_t *ragb_data = (uint8_t *) av_malloc(buffer_size * sizeof(uint8_t));  
  
auto rgba_frame = av_frame_alloc();  
av_image_fill_arrays(rgba_frame->data, rgba_frame->linesize,  
ragb_data,  
AV_PIX_FMT_RGBA,  
frame->width, frame->height, 1);  
  
  
SwsContext *sws = sws_getContext(frame->width, frame->height, av_codec->pix_fmt,  
frame->width, frame->height, AV_PIX_FMT_RGBA,  
0,  
nullptr, nullptr, nullptr);  
sws_scale(sws, frame->data, frame->linesize, 0, frame->height,  
rgba_frame->data, rgba_frame->linesize);  
  
sws_freeContext(sws);  
  
  
for (int i = 0; i < frame->height; i++) {  
//由于rgba的linesize只有第一位有效,所以只使用rgba_frame->linesize[0]即可。
memcpy(dstData + i * dstStride, ragb_data + i * rgba_frame->linesize[0],  
rgba_frame->linesize[0]);  
}  
  
// 解锁 ANativeWindow  
ANativeWindow_unlockAndPost(nativeWindow);    
} 
__android_log_write(ANDROID_LOG_INFO, "日志", "解析结束");  
  
  
// 释放 ANativeWindow  
ANativeWindow_release(nativeWindow);  
  
av_packet_unref(packet);  
av_frame_free(&frame);  
avcodec_close(av_codec);  
avformat_close_input(&av_context);  
  
}  
  
  
}  
  
  
return (long) av_context;  
}

释放资源

av_free和av_freep

释放av_malloc() or av_realloc()分配的内存

av_freeav_freep都是用于释放内存的函数,但它们之间有一个主要的区别。

  • av_free(void *ptr):这个函数将释放ptr指向的内存块,但不会改变ptr本身。也就是说,调用av_free后,ptr仍然指向已经被释放的内存。如果你试图再次使用或者释放这块内存,可能会导致程序崩溃或其他未定义行为。
  • av_freep(void **ptr):这个函数也会释放ptr指向的内存块,但之后它还会把ptr设置为NULL。这样可以避免无意中再次使用或者释放已经被回收的内存。

所以,在大多数情况下,使用av_freep可能会更安全一些,因为它能够帮助你防止一些常见的编程错误。然而,请注意你必须传入一个指向指针的指针(即二级指针)给av_freep。如果你只有一个普通的指针(即一级指针),那么你应该使用av_free

下面是两个函数在实际使用中的例子:

// 使用 av_free
void* buffer = av_malloc(size);
// ... 使用buffer...
av_free(buffer);  // 注意:此时buffer仍然指向已经被回收的内存

// 使用 av_freep
void* buffer = av_malloc(size);
// ... 使用buffer...
av_freep(&buffer);  // 注意:此时buffer已经被设置为NULL

其他

FFmpeg库中有很多资源需要在使用完后进行适当的释放,以防止内存泄漏。以下是一些常见的资源类型及其对应的释放函数:

  1. AVFormatContext:这个结构体用于存储媒体文件的上下文信息。你可以使用avformat_close_input()来释放一个AVFormatContext。
AVFormatContext* formatCtx = NULL;
// ... 使用formatCtx...
avformat_close_input(&formatCtx);
  1. AVCodecContext:这个结构体用于存储编解码器的上下文信息。你可以使用avcodec_free_context()来释放一个AVCodecContext。
AVCodecContext* codecCtx = NULL;
// ... 使用codecCtx...
avcodec_free_context(&codecCtx);
  1. AVFrame:这个结构体代表了一帧视频或音频数据。你可以使用av_frame_free()来释放一个AVFrame。
AVFrame* frame = NULL;
// ... 使用frame...
av_frame_free(&frame);
  1. AVPacket:这个结构体代表了一段压缩的数据(例如,一个视频帧或音频帧)。你可以使用av_packet_free()来释放它。
AVPacket* packet = av_packet_alloc();
// ... 使用packet...
av_packet_unref(packet);

进阶

过滤

AVFilterGraph

AVFilterGraph是用于处理音频和视频过滤的主要结构体。它提供了一个框架,允许你将多个过滤器连接在一起,形成一个过滤链。

每个AVFilterGraph都包含一组互相连接的AVFilterContexts(也就是过滤器实例)。这些AVFilterContexts可以表示源(如输入文件或设备),目标(如输出文件或设备),或者处理步骤(如缩放、裁剪、重采样等)。

当你创建一个新的AVFilterGraph时,你需要添加并配置适当的AVFilterContexts,并将它们按照所需的处理顺序连接起来。然后,你可以通过调用avfilter_graph_run()来执行过滤操作。

AVFilterContext

在FFmpeg中,AVFilterContext是一个重要的结构体,它代表了过滤器图(filter graph)中的一个过滤器实例。

每个AVFilterContext都包含一些信息和函数,用于处理音频或视频数据。例如,它包含了指向对应的AVFilter结构体的指针(该结构体描述了过滤器的类型和功能),以及一些输入/输出端口(称为“pads”),这些端口可以连接到其他过滤器。

当你创建一个新的AVFilterContext时,你需要提供一些参数来配置它。这可能包括输入/输出格式、缓冲区大小、选项字符串等。然后,你可以将多个AVFilterContext连接在一起,形成一个处理链。

总的来说,在FFmpeg中使用AVFilterGraph进行音频或视频处理时,你会大量地使用到AVFilterContext。通过正确地配置和链接多个AVFilterContext,你可以执行各种复杂的转换和处理任务。

步骤

使用FFmpeg的AVFilterGraph进行视频过滤通常需要以下几个步骤:

  1. 创建一个AVFilterGraph实例。
  2. 为源(source)和目标(sink)创建对应的AVFilterContext实例,并添加到AVFilterGraph中。源通常是一个表示输入文件或设备的过滤器,而目标则是一个表示输出文件或设备的过滤器。
  3. 添加并配置其他需要的过滤器。例如,如果你想要对视频进行缩放、裁剪或者色彩转换等操作,你就需要添加相应的过滤器。
  4. 将这些过滤器按照正确的顺序连接起来。每个过滤器都有一个或多个输入口(input pad)和输出口(output pad),你需要确保每个输出口都连接到了正确的输入口上。
  5. 执行过滤操作。这通常通过调用avfilter_graph_run()函数来完成。

下面是一个简单的示例,它演示了如何创建一个只包含源和目标两个过滤器的AVFilterGraph:

// 创建一个新的AVFilterGraph
AVFilterGraph* filter_graph = avfilter_graph_alloc();

// 获取源和目标过滤器的定义
const AVFilter* src_filter = avfilter_get_by_name("buffer");
const AVFilter* sink_filter = avfilter_get_by_name("buffersink");

// 创建并配置源过滤器
AVBufferSrcParameters* src_params = av_buffersrc_parameters_alloc();
// ... 设置src_params...
AVFilterContext* src_ctx;
avfilter_graph_create_filter(&src_ctx, src_filter, "in", NULL, src_params, filter_graph);

// 创建并配置目标过滤器
AVBufferSinkParams* sink_params = av_buffersink_params_alloc();
// ... 设置sink_params...
AVFilterContext* sink_ctx;
avfilter_graph_create_filter(&sink_ctx, sink_filter, "out", NULL, sink_params, filter_graph);

av_buffersrc_add_frame

av_buffersink_get_frame

其中buffer和buffersink是两个默认是过滤器,你可以在buffersrc.c和buffersink.c源码中找到他们

"buffer"和"buffersink"是FFmpeg中两个特殊的过滤器,它们通常被用作过滤器链的起点和终点。具体来说:

  • "buffer"过滤器:这个过滤器用于将原始数据(例如解码后的视频帧或音频样本)输入到过滤器链中。你需要为它提供一些参数,例如源数据的格式、尺寸等。在实际使用中,你通常会从一个AVFrame(或者其他类型的数据结构)中获取这些数据,并通过av_buffersrc_add_frame()函数将其发送到"buffer"过滤器。
  • "buffersink"过滤器:这个过滤器用于从过滤器链中获取处理后的数据。你可以使用av_buffersink_get_frame()函数来从它那里读取数据。在创建这个过滤器时,你可以指定一些期望的输出参数,例如希望输出的像素格式、采样率等。

除了上述功能外,这两个过滤器还有一些其他特性。例如,你可以设置一个时间基(time base),以便在处理帧时正确地计算时间戳。此外,如果你需要处理多个不同格式或尺寸的源文件,也可以为每个文件创建一个独立的"buffer"过滤器。

总之,通过正确地使用这两个过滤器,你可以将任意复杂度的音频/视频处理任务表示为一个过滤器链,从而利用FFmpeg强大的过滤和转换能力。

后面可以接续看juejin.cn/post/703184… 这篇文章

avfilter_graph_create_filter

函数原型

int avfilter_graph_create_filter(AVFilterContext **filt_ctx, const AVFilter *filt,
                                 const char *name, const char *args, void *opaque,
                                 AVFilterGraph *graph_ctx)

其中args用来设置该过滤器的参数,也就是我当前过滤器的功能或者输出的视频格式。 并将创建成功的过滤器实例赋值给filt_ctx,

time_base

time_base的值不一定是帧率的倒数,也可能是采样率的倒数,比如H264的time_base就是1/90000,并不是帧率的倒数。尽管time_base的定义方式有所不同pts*time_base的计算结果都是当前帧应该显示的时间,因为pts的值是依赖time_base的。

H264的time_base值需要从AVStream的time_base获取到,如果从AVCodecContext获取time_base是无效的