FFmpeg Java版 基础上

213 阅读36分钟

我们做一个音视频数据无非是

拉流(采集数据)--> 像素数据 / 音频数据 --> 解码 --> 封装数据 --> 协议推流

基础知识概括

图像封装格式:

常见的封装格式有:PNG、JPG、BMP、GIF 等等。

  • BMP 是无损格式且不压缩,里面的数据格式就是图像头信息加上原始的像素数据。
  • PNG 是无损压缩格式。
  • JPG/GIF 等都是有损压缩格式。压缩图片可以有效节省硬盘空间。

图像像素数据:

图像像素数据指的是 YUV、RGB、RGBA、BGR、GBRA 等图像像素格式。

经过编码后才是视频帧。例如常见的 H264 编码,编码实际上是对图像像素数据的压缩。图像像素数据的压缩:

图像像素格式是原始数据,未经过任何压缩或编码。 图像封装格式可以是无损压缩或有损压缩,通过封装减少存储空间。 图像像素数据与视频帧的区别在于,视频帧是通过对图像像素数据进行编码压缩而来。

视频帧中常常提到的I帧、B帧和P帧

  • I帧(关键帧):是完整的静态图像,独立存在,不依赖其他帧解码。
  • B帧和P帧:不是完整图像,依赖前后的帧来解码,B帧依赖前后帧,P帧依赖前帧。
  • 使用场景:在视频流中,I帧用得较少,因为它占用的空间大,但解码简单;B帧和P帧用得较多,因为它们占用空间小,但需要更多的硬件性能来解码。
  • 应用:直播中常使用I帧增加传输数据量,但能更快显示首帧画面。

编码?封装?

编码

压缩算法,用于减少视频文件的大小(如H264、HEVC)。

封装格式

结构化存储多种流媒体数据(音视频、字幕、元数据等),常见的有MP4、AVI、MKV、FLV等。

封装格式包含视频头信息和索引,便于播放设备读取和导航。

压缩图片格式

与视频编码格式类似,都是对图像数据进行有损/无损压缩。

转封装

比喻:将视频数据从一个盒子(封装格式)转移到另一个盒子,不改变数据本身。

为什么转封装比转码消耗更少?

转封装不改变视频数据,只是改变存储方式;转码需要重新编码,消耗更多资源。

音/视频源

音/视频源可以是视频文件、音频文件、流媒体源、设备等。

  • 视频源:从摄像设备采集到的是图像数据,通常是BGR或RGB像素数据。例如,从电脑或手机摄像头采集的视频。
  • 音频源:从录音设备采集到的是音频数据,通常是PCM采样数据。例如,从麦克风采集的声音。

流媒体协议

  • RTSP协议
  • RTMP协议
  • HLS
  • HTTP-FLV(严格来说,FLV不能算是流媒体协议,它是一个无缝大的FLV文件)

这些协议的底层原理是建立在TCP/UDP基础上的应用层传输协议

流媒体服务

支持音视频存储/分发的服务叫流媒体服务。

常见流媒体服务:

  • SRS(开源的RTMP流媒体服务器):支持RTMP、HLS、HTTP-FLV等协议的分发。
  • Nginx:通过安装模块可以支持RTMP、HLS、HTTP-FLV分发。 其他服务:
  • 还有一些开源的和大型公司的付费流媒体服务,适合不同的应用场景。

知识概念图

image.png

JavaCPP和JavaCV调用FFmpeg详解

Javacpp 是什么?

JavaCPP 是一个桥梁,它连接了 Java 和 FFmpeg 之间的世界,使得我们能够在 Java 中方便地调用 FFmpeg 的功能。FFmpeg 是一个用 C 语言编写的著名音视频库,通过 JavaCPP,我们可以在 Java 中调用这些 C 语言编写的功能,而无需直接编写 C 代码。

调用 FFmpeg 时的性能损失

通过 JavaCPP 调用 FFmpeg 时,虽然存在一些性能损失,但这个损失与使用 C++ 调用 FFmpeg 时的损失相差不大。因此,Java 开发者可以放心使用 JavaCPP 来调用 FFmpeg 的功能,而无需担心性能问题。

内存管理

调用 FFmpeg 需要注意的是,这些操作在 JVM 中是以 native 方法进行的。这意味着通过 FFmpeg 创建的结构体实例、常量和方法等都使用的是堆外内存(即不在 JVM 的堆内存中)。因此,开发者需要像在 C 语言中那样手动释放这些资源,以避免内存泄漏或溢出问题。JVM 不会自动回收这些 native 方法使用的堆外内存。

avformat 类

FFmpeg 库中的一个核心类,提供了多媒体文件(如视频、音频文件)进行读写操作的功能

avformat_alloc_context 静态方法

分配并初始化一个空的 AVFormatContext 对象,用于存储多媒体文件的信息和管理流。

avformat_open_input 静态方法

打开多媒体文件并初始化 AVFormatContext 对象

  • 参数一:为 AVFormatContext 对象填充文件的格式和流信息
  • 参数二:要打开的输入文件的路径或 URL(本地文件或者网络地址)
  • 参数三:指定输入文件的格式。如果设置为 null,FFmpeg 将自动检测文件格式。
  • 参数四:控制打开文件的行为。可以设置为 null,使用默认选项

返回值:0 成功,负数失败

public static native int avformat_open_input
(AVFormatContext ps, String url, AVInputFormat fmt, AVDictionary options);

avformat_find_stream_info 静态方法

用于查找输入文件中流的信息,包括每个流的编解码器、分辨率、比特率等参数。

  • 参数一:已经加载视频后的 AVFormatContext 信息,进行填充流的数据信息。
  • 参数二:控制查找流信息的行为。可以设置为 null,使用默认选项。

返回值:大于0 成功,小于 0 错误

重载说明

使用 PointerPointer<Pointer> 作为参数

是一个指向指针数组的指针,其中每个指针指向一个 AVDictionary 对象。

允许为每个流单独传递不同的选项。如果媒体文件有多个流,每个流可以有不同的选项设置,但也更复杂,需要处理指针数组。

使用 AVDictionary 作为参数

这种方式用于为所有流传递相同的选项。适用于所有流都使用相同设置的情况。更简单

public static native int avformat_find_stream_info
(AVFormatContext ic, AVDictionary options);

public static native int avformat_find_stream_info
(AVFormatContext ic,  PointerPointer options);

avformat_flush 静态方法

用于丢弃所有内部缓冲的数据。这在处理字节流中的不连续性时非常有用。

主要功能包括:

  • 丢弃缓冲数据:清空 AVFormatContext 内部缓冲的数据。
  • 处理不连续性:在字节流中处理不连续性时非常有用。

注意事项

流集合、检测到的持续时间、流参数和编解码器不会改变

调用此方法时,这些信息保持不变。如果需要完全重置,最好打开一个新的 AVFormatContext。

不刷新 AVIOContext

此方法不会刷新 AVIOContext(即 s->pb)。如果有必要,请在调用此方法之前调用 avio_flush(s->pb)。

@NoException
public static native int avformat_flush(AVFormatContext s);

av_dump_format 静态方法

打印多媒体文件或流的详细信息

参数一:指向 AVFormatContext 结构,表示多媒体文件的处理上下文。 参数二:流的索引。如果传入负数,表示打印文件中的所有流的信息。 参数三:文件或流的 URL,通常是文件路径或流的 URL 地址。 参数四:标志位,表示这个多媒体上下文是输入还是输出。0 表示输入,非 0 表示输出。

包括:持续时间、比特率、流信息、容器信息、节目信息、元数据、边数据、编解码器、时间基准。

public static native void av_dump_format
(AVFormatContext ic, int index, String url, int is_output);

av_read_frame

作用:用于从多媒体文件中读取一个包(packet)。这个包包含一个视频帧或多个音频帧,并且可以直接用于解码。

返回值:该函数读取的文件数据,处理后,将数据分割为帧,每次调用返回一帧,注意包含无效数据,如果成功则返回 0,如果出错或到达文件末尾则返回 < 0。出错时,AVPacket 将为空

  • 参数一:AVFormatContext 对象,该对象包含了多媒体文件的格式信息和流信息。(比如获取是从视频中读取数据,还是从设备上读取数据)
  • 参数二:AVPacket 对象,该对象将存储读取的数据包(帧)

资源释放:返回的包是引用计数的,这个指针被正确设置,指向数据所在的内存缓冲区,当不再需要时,必须使用 av_packet_unref() 释放该数据包。

视频帧 / 音频帧

对于视频数据,每个数据包(packet)通常包含一个完整的视频帧。

对于音频数据,如果每个音频帧的大小是固定的(例如 PCM 或 ADPCM 编码的数据),一个数据包可能包含多个完整的音频帧。

对于那些每个音频帧大小可变的音频格式(例如 MPEG 音频),一个数据包只会包含一个音频帧。

public static void main(String[] args) {
    String filePath = "视频文件路径";
    AVFormatContext formatContext = new AVFormatContext(null);

    // 打开输入文件
    if (avformat.avformat_open_input(formatContext, filePath, null, null) != 0) {
        System.err.println("无法打开文件: " + filePath);
        return;
    }

    // 获取流信息
    if (avformat.avformat_find_stream_info(formatContext, (AVDictionary) null) < 0) {
        System.err.println("无法找到流信息");
        return;
    }

    // 初始化一个新的 AVPacket 用于存储数据
    AVPacket packet = new AVPacket();

    // 从文件中读取帧
    while (avformat.av_read_frame(formatContext, packet) >= 0) {
        // 检查数据包是否有效
        if (packet.buf() != null) {
            // 处理数据包(例如,解码和显示)
            System.out.println("收到数据包,大小: " + packet.size());

            // 完成处理后释放数据包
            avutil.av_packet_unref(packet);
        }
    }

    // 关闭输入文件
    avformat.avformat_close_input(formatContext);
}

av_guess_format

一句话描述:查找匹配的输出格式

作用:用于在注册的输出格式列表中找到与给定参数最匹配的输出格式。如果没有匹配的格式,它将返回 NULL。这个函数在处理视频和音频文件时非常有用,尤其是在决定输出文件格式时。

  • 参数一:如果提供了这个参数,函数会检查这个名称是否与注册的输出格式的名称匹配。通常,这是文件扩展名的简短形式(例如 "mp4" 或 "avi")。
  • 参数二:如果提供了这个参数,函数会检查文件名的扩展名是否与注册的输出格式的扩展名匹配。例如,如果文件名是 "video.mp4",函数会检查是否有输出格式支持 ".mp4" 扩展名。
  • 参数三:如果提供了这个参数,函数会检查 MIME 类型是否与注册的输出格式的 MIME 类型匹配。例如,MIME 类型 "video/mp4"。

返回值

  • 成功: 返回一个 AVOutputFormat 对象,该对象表示最佳匹配的输出格式。
  • 失败: 返回 NULL,如果没有找到匹配的格式。
// 假设我们要找到适合 ".mp4" 文件的输出格式
String filename = "example.mp4";
// 调用 av_guess_format 查找最佳匹配的输出格式
AVOutputFormat outputFormat = av_guess_format(null, filename, null);
if (outputFormat != null) {
    System.out.println("找到输出格式: " + outputFormat.long_name().getString());
} else {
    System.out.println("未找到匹配的输出格式。");
}

avio_open

作用:用于创建和初始化一个 AVIOContext,以访问由 url 指定的资源。此函数可以用来打开文件、网络流等资源,以便进行读写操作。

  • 参数一:指向一个 AVIOContext 指针的指针,用于返回创建的 AVIOContext 的指针。在失败的情况下,该指针将被设置为 NULL。
  • 参数二:指向要访问的资源的 URL(如文件路径或网络流 URL)。
  • 参数三控制如何打开由 url 指定的资源的标志。常见的标志包括
    • AVIO_FLAG_READ:以只读模式打开资源。
    • AVIO_FLAG_WRITE:以只写模式打开资源。
    • AVIO_FLAG_READ_WRITE:以读写模式打开资源。

返回值

  • 成功时返回值大于或等于 0,表示 AVIOContext 已成功创建和初始化。
  • 失败时返回一个负值,对应于一个 AVERROR 代码,表示具体的错误原因。
// 创建一个AVIOContext指针
AVIOContext ioContext = new AVIOContext(null);

// 打开一个文件用于写操作
String url = "output.mp4";
int flags = avformat.AVIO_FLAG_WRITE;
int ret = avformat.avio_open(ioContext, url, flags);

if (ret < 0) {
    // 打开文件失败,打印错误信息
    byte[] errorBuffer = new byte[1024];
    avutil.av_strerror(ret, errorBuffer, errorBuffer.length);
    System.err.println("无法打开文件: " + new String(errorBuffer));
    return;
}

avformat_new_stream

作用:用于向媒体文件中添加一个新的流。

  • 参数一:代表了媒体文件的上下文。添加的新流将被添加到这个媒体文件中。
  • 参数二:用于设置流的编码器,可以为 NULL

返回值

  • 成功: 返回新创建的 AVStream 对象
  • 失败: 返回 NULL,表示在添加流的过程中发生了错误

avformat_write_header

用于初始化媒体文件的写入,包括分配流的私有数据和写入流头。这个函数在进行媒体文件的 muxing(复用)时非常重要,它确保输出文件的格式和编码设置符合预期。

  • 参数一:AVFormatContext 对象,表示媒体文件的上下文,必须通过 avformat_alloc_context() 分配并初始化。这个上下文包含了要写入的媒体文件的格式和状态信息。
    • 必须设置输出格式--oformat
    • 须设置为已经打开的 AVIOContext,负责实际的 I/O 操作--pb
  • options: 选项字典 (AVDictionary),用于设置 AVFormatContext 和 muxer(复用器)的私有选项。(可以为 NULL)

返回值

  • 成功
    • AVSTREAM_INIT_IN_WRITE_HEADER: 如果编码器在 avformat_init_output() 中未完全初始化,表示成功。
    • AVSTREAM_INIT_IN_INIT_OUTPUT: 如果编码器已经完全初始化,表示成功。
  • 失败: 返回负的 AVERROR 码,表示出错。
// 初始化 AVFormatContext
AVFormatContext formatContext = new AVFormatContext();
formatContext.oformat(outputFormat); // 设置输出格式
formatContext.pb(ioContext); // 设置已打开的 AVIOContext

// 创建并设置编码选项字典
AVDictionary options = new AVDictionary();
av_dict_set(options, "some_option", "some_value", 0);

// 写入文件头
int result = avformat_write_header(formatContext, options);
if (result < 0) {
    // 处理错误
    System.err.println("Failed to write header: " + result);
    return;
}

// 继续处理媒体流

av_find_input_format

用于查找和获取输入格式。

  • 参数:输入格式的短名称,如 "mp4"、"avi" 等。

返回值

  • 成功: 返回指向 AVInputFormat 结构体的指针。这个结构体包含了关于找到的输入格式的详细信息。
  • 失败: 如果没有找到对应的输入格式,则返回 NULL。
// 根据输入格式,查找设备
AVInputFormat avInputFormat = avformat.av_find_input_format("mp4");
// 检查并打印结果
if (avInputFormat != null) {
    System.out.println("找到输入格式:");
    System.out.println("名称: " + avInputFormat.name().getString()); // 名称: mov,mp4,m4a,3gp,3g2,mj2
    System.out.println("长名称: " + avInputFormat.long_name().getString()); // 长名称: QuickTime / MOV
} else {
    System.out.println("未找到输入格式。");
}

avcodec 类

avcodec_alloc_context3

用于分配并初始化一个 AVCodecContext

参数一:AVCodec 类,用于指定编解码器。如果传入 null,则不会为特定的编解码器进行初始化。

public static native AVCodecContext avcodec_alloc_context3(AVCodec codec);

avcodec_parameters_to_context

用于将 AVCodecParameters 类中的参数复制到 AVCodecContext 类中,这些参数包括编码类型、比特率、宽度、高度等。

成功时返回 0,失败时返回负值,表示错误代码。

public static native int avcodec_parameters_to_context(AVCodecContext codec, AVCodecParameters par);

avcodec_find_decoder

根据编解码器唯一ID查找视频流的解码器

返回解码器 AVCodec

public static native AVCodec avcodec_find_decoder(int id);

avcodec_open2

作用:根据编解码器上下文和编解码器打开视频,并设置相关信息

参数解释

  • acvtx:编解码器上下文。包含了处理音视频的配置信息(初始化编解码器上下文,以便使用)
  • codec:具体的编解码器,使用它来初始化编解码器上下文。
  • options:字典选项,可以为空。

返回 0 成功 小于 0 错误码

public static native int avcodec_open2(AVCodecContext avctx, @Const AVCodec codec, PointerPointer options);
public static native int avcodec_open2(AVCodecContext avctx, AVCodec codec, AVDictionary options);

第一个重载方法: 通过 PointerPointer 传递选项,适用于 JNI 中将 AVDictionary* 转换为 Java 对象时。

第二个重载方法: 直接使用 AVDictionary*,适用于原生的 C/C++ 代码或 JNI 直接操作的场景。

avcodec_send_packet

作用:用于将原始数据包传递给解码器。以便解码器在内部处理并生成解码后的帧。

  • 参数一:解码器上下文。这个上下文包含了解码器的状态信息和配置。注意点:在调用之前,必须通过 avcodec_open2() 打开解码器上下文。
  • 参数二:输入的数据包,可以为 NULL(或者数据为 NULL 且大小为 0 的 AVPacket),这种情况下,它被认为是一个冲刷包,表示流的结束。

返回值:

  • 0: 成功。
  • AVERROR(EAGAIN): 当前状态下不接受输入,用户必须使用
  • avcodec_receive_frame() 读取输出(读取完所有输出后,应重新发送数据包,调用将不再以 EAGAIN 失败)。
  • AVERROR_EOF: 解码器已被冲刷,无法再发送新的数据包(如果发送多个冲刷包也会返回)。
  • AVERROR(EINVAL): 解码器未打开,或这是一个编码器,或需要冲刷。
  • AVERROR(ENOMEM): 无法将数据包添加到内部队列,或类似问题。其他负值错误代码: 合法的解码错误。

处理过程中:如果数据包包含多个帧(例如某些音频编解码器),需要多次调用 avcodec_receive_frame() 才能完全解码这些帧。

// 发送数据包给解码器
int ret = avcodec_send_packet(avctx, avpkt);
if (ret < 0) {
    if (ret == avutil.AVERROR_EAGAIN()) {
        // 当前状态下不接受输入,需要先读取解码器输出
    } else if (ret == avutil.AVERROR_EOF()) {
        // 解码器已被冲刷,无法再发送新的数据包
    } else {
        // 其他错误处理
    }
}

avcodec_receive_frame

作用:用于从解码器中接收解码后的数据帧。

  • 参数一:解码器上下文,包含解码器的状态和配置信息。必须在调用之前,通过 avcodec_open2 打开解码器。
  • 参数二:解码器将解码后的数据存储在这个 AVFrame 对象中。在函数调用之前,frame 对象会被 av_frame_unref(frame) 清除,以确保它不含有旧的或无效的数据。

返回值说明

  • 0: 成功,函数返回了一个有效的帧数据。frame 中包含了解码后的数据。
  • AVERROR(EAGAIN): 当前没有解码后的帧可用。需要更多的数据包或者需要先读取更多的帧数据。通常在这种情况下,应该继续调用 avcodec_send_packet 发送更多的数据包。
  • AVERROR_EOF: 解码器已经被完全刷新,且不会有更多的输出帧。这通常发生在数据流结束或解码器完成处理时。
  • AVERROR(EINVAL): 解码器尚未打开,或者它是一个编码器而不是解码器,或者
  • AV_CODEC_FLAG_RECON_FRAME 标志没有启用。需要确保解码器已经打开且正确配置。 其他负值错误代码: 其他解码错误,表示在解码过程中发生了错误。
ret = avcodec_receive_frame(avctx, frame);
if (ret == 0) {
    // 成功接收到帧,处理帧数据
    // 例如:将帧数据渲染到屏幕、保存到文件等
} else if (ret == avutil.AVERROR_EAGAIN()) {
    // 需要更多的数据包,或者需要先读取更多的帧数据
    break; // 或者继续处理
} else if (ret == avutil.AVERROR_EOF()) {
    // 解码器已结束,所有帧已经处理完
    break;
} else {
    // 其他错误处理
}

av_new_packet

它会分配数据缓冲区

用于分配 AVPacket 包数据,设置大小。

  • 参数一:AVPacket 对象的引用,表示数据包。这个数据包将被分配内存并初始化。
  • 参数二:表示需要为数据包分配的大小。

返回值

  • 0: 如果操作成功,则返回 0。
  • 负值: 如果操作失败,则返回一个负数,表示相应的错误代码。
    • AVERROR(ENOMEM) 表示内存分配失败。
// 创建 AVPacket 实例
AVPacket avPacket = new AVPacket();

// 假设需要的有效负载大小
int payloadSize = 640 * 480 * 3; // 例如,存储一个 RGB 视频帧

// 分配并初始化 AVPacket
int ret = avcodec.av_new_packet(avPacket, payloadSize);

// 检查是否成功分配
if (ret < 0) {
    System.err.println("Failed to allocate packet: " + ret);
    return;
}

av_packet_alloc

作用:分配 AVPacket 类,不会初始化数据缓冲区和大小,需要调用 av_packet_free() 来释放。

av_packet_unref

作用:将 AVPacket 的数据清除。将其数据设置为默认值。

参数:需要清除的数据包。

av_packet_free

作用:释放 Avpacket 类,首先会检查是否有引用,有,则调用 av_packet_unref 释放数据,完成后,将当前类设置为 NULL。

avcodec_find_encoder

用于查找和获取一个编码器

参数

  • id:AVCodecID 枚举值,表示要查找的编码器的编码格式。

  • AV_CODEC_ID_H264 表示 H.264 编码器

  • AV_CODEC_ID_MP3 表示 MP3 编码器

返回值

  • 成功: 返回一个指向 AVCodec 结构体的指针
  • 失败: 如果没有找到指定 ID 的编码器,则返回 NULL。
// 打开编码器
AVCodec avCodec = avcodec.avcodec_find_encoder(avcodec.AV_CODEC_ID_AAC);

avcodec_find_encoder

根据指定名称查找编码器

AVCodec libfdkAac = avcodec.avcodec_find_encoder_by_name("libfdk_aac");

avcodec_alloc_context3

用于分配和初始化一个 AVCodecContext 结构体。这个结构体用于编解码操作,包含了编解码器的所有必要信息和设置。

参数一: 为编解码器分配内存并初始化默认值。

返回值

  • 成功: 返回一个填充了默认值的 AVCodecContext 实例。如果 codec 不为 NULL,则结构体会初始化为指定编解码器的默认值
  • 失败: 返回 NULL

avutil 类

av_frame_alloc

用于分配并初始化一个 AVFrame 类,可以用来存储编解码后的视频和音频

AVFrame 结构在 FFmpeg 中用于存储解码后的音频或视频帧。

public static native AVFrame av_frame_alloc();

av_image_get_buffer_size

作用:获取图像大小

  • 参数一:图像的像素格式
  • 参数二:以像素为单位的图像宽度
  • 参数三:以像素为单位的图像高度
  • 参数四:字节对齐,通常设置为 1 表示不对齐。
public static native int av_image_get_buffer_size(int pix_fmt, int width, int height, int align);

av_malloc

作用:用于动态分配内存空间,其对齐方式适合所有内存访问(包括矢量,如果 CPU 上可用)。

参数:要分配的内存块的大小(以字节为单位)

返回值:如果分配成功,返回一个指向分配内存空间的指针;如果分配失败或者 size 为 0,则返回 NULL。

public static native Pointer av_malloc(long size);

av_image_fill_arrays

作用:用于填充图像数据的指针数组和行大小数组。这个方法主要用于为 AVFrame 结构体的数据平面分配内存和填充数据信息。

  • 参数一:要填写的数据平面指针
  • 参数二:要填充数据平面的行大小
  • 参数三:包含实际图像数据的缓冲区,可以为null
  • 参数四:图像的像素格式
  • 参数五:像素为单位的图像宽度
  • 参数六:像素为单位的图像高度
  • 参数七:行大小在内存中的对齐方式(默认1,没有额外对齐方式)

返回值:

av_frame_get_buffer

用于为音频或视频数据分配新缓冲区的函数,在调用此函数之前,需要在 AVFrame 上设置一些字段。

调用此函数后,AVFrame 结构体的 data 和 buf 数组将会填充数据。

参数

  • frame:传递一个 AVFrame 对象。在调用此函数之前,需要设置一些必要的字段(如格式、宽度、高度或样本数和声道布局)
  • align:对齐方式,通常设置 0

返回值

  • 0:成功。
  • 负值,错误

常量

AV_PIX_FMT_YUV420P

用于表示颜色数据。它将图像分成三个独立的平面:一个用于亮度信息(Y),两个用于色度信息(U 和 V)。这种格式每 4 个亮度像素共享一个色度像素,从而节省存储空间,同时保留较高的图像质量。它广泛应用于视频压缩和处理,因为这种格式在数据量和图像质量之间达到了良好的平衡。

AVCOL_RANGE_JPEG

用于指示视频帧的色彩范围是全范围(full range)的。具体来说,它意味着亮度(Y)的值可以从 0 到 255,而色度(U 和 V)的值也可以从 0 到 255。这个设置常用于 JPEG 图像和某些视频编码格式,因为它可以提供更丰富的颜色细节和更高的图像质量。使用全范围色彩的图像在亮度和色彩过渡上会更加平滑和自然。

AVMEDIA_TYPE_VIDEO

表示媒体流的类型是视频

AV_CHANNEL_LAYOUT_STEREO

表示立体声的通道布局。有两个音频通道:左声道和右声道。

获取对应的通道数

int nbChannels = avutil.AV_CHANNEL_LAYOUT_STEREO.nb_channels();

AV_SAMPLE_FMT_S32

用于表示音频样本格式的常量,指的是 32 位有符号整数格式的音频样本。

avdevice 类

用于初始化 libavdevice 组件并注册所有输入和输出设备。在 FFmpeg 中,libavdevice 是一个处理输入和输出设备的库,它允许你访问和控制各种设备,如摄像头、麦克风等。

// 注册所有的设备
avdevice.avdevice_register_all();

swresample 类

swr_alloc_set_opts2

用于设置音频重采样上下文 (SwrContext) 的参数,重采样是指将音频从一种采样率、通道布局或样本格式转换为另一种采样率、通道布局或样本格式。

  • 参数一:ps: 指向现有的 SwrContext,如果没有则为 NULL。成功后,ps 将被设置为已分配的上下文。
  • 参数二:输出的通道布局,值为 avutil的通道布局常量
  • 参数三:输出的样本格式,值为 avutil的样本格式常量
  • 参数四:输出采样率
  • 参数五:输入通道布局
  • 参数六:输入样本格式
  • 参数七:日志级别,一般为0
  • 参数八:父日志上下文,可以为null

返回值

  • 成功时返回 0。
  • 出错时返回负值的错误代码,并释放 SwrContext,ps 设置为 NULL。

swr_alloc

用于分配一个 SwrContext 对象,并不会自动设置其参数。只是一个结构体指针。

// 分配重采样上下文
SwrContext swrContext = swresample.swr_alloc();;
if (swrContext == null) {
    log.error("无法分配 SwrContext");
    return;
}

// 注意释放资源
swresample.swr_free(swrContext);

swr_init

swr_init 是再设置完 SwrContext 的参数之后调用的。

用于初始化已经配置好的 SwrContext 对象,初始化之后,SwrContext 就可以用来执行音频重采样任务了。

// TODO: 这里可能有个疑问,为什么还要再次进行初始化,原因是会检查设置的参数是
//  否有问题,根据配置去分配资源,
int ret = swresample.swr_init(swrContext);
if (ret < 0) {
    log.error("无法初始化 SwrContext");
    return;
}

av_samples_alloc_array_and_samples

作用:

  1. 分配一个音频数据指针数组,用于存储每个声道的音频数据指针。
  2. 为每个声道分配一个样本缓冲区,用于存储实际的音频样本数据。
  3. 填充数据指针和行大小

参数:

  • 参数一
    • 这是一个指向指针的指针,表示音频数据指针数组。
    • 该函数会为音频数据指针数组分配内存空间,并将其地址存储在这个参数中。
  • 参数二
    • 指向一个整数,用于存储每个声道的行大小(即每个声道所需的缓冲区大小)。
    • 行大小是指每个声道的缓冲区需要多少字节。
  • 参数三
    • 音频的声道数。例如,单声道是1,立体声是2。
  • 参数四
    • 每个声道的样本数。
  • 参数五
    • 音频样本格式,定义了音频样本的数据格式。
  • 参数六
    • 缓冲区对齐方式。通常设置为0或1以使用默认对齐

那么参数四是怎么计算的呢。

这个是根据缓冲区大小计算的,缓冲区大小是获取的每个包的大小。

假设我的缓冲区是4096,样本格式是AV_SAMPLE_FMT_S32,对应4个字节,通道数2

计算 = 4096 / (4 * 2) = 512

PointerPointer<BytePointer> dts_data = new PointerPointer<>(1);
IntPointer dtsLineSizePtr = new IntPointer(1);
// 创建并分配一个浮点型音频数据的目标缓冲区,用于存储音频样本数据。
avutil.av_samples_alloc_array_and_samples(
        dts_data, // 输出值
        dtsLineSizePtr, // 存储每行音频数据的大小(字节数)
        2, // 通道数,例如立体声有两个通道
        512, // 采样数,每个通道的采样数
        avutil.AV_SAMPLE_FMT_FLT, // 采样格式,浮点型
        0 // 对齐,使用默认对齐方式
);
// 结果--获取存储每行音频数据的大小(字节数)
int dtsLineSize = dtsLineSizePtr.get();

// 注意需要释放
avutil.av_freep(dts_data);

swr_convert

用于将音频数据从一种格式转换成另一种格式,需要为输入和输出的数据准备缓冲区。输入缓冲区存放原始数据,输出缓冲区用来存放转换后的数据。

函数参数解释

  • s:已经设置好的转换上下文(SwrContext)。它包含了转换需要的各种设置,比如输入和输出的格式、通道布局、采样率等。
  • out:这是一个指向输出缓冲区的指针。可以把它想象成一个准备好存放转换后数据的容器。out 指针的数组表示各个音频通道的输出缓冲区。如果音频是立体声(两个通道),需要两个缓冲区的指针。
  • out_count:这是为每个通道的输出缓冲区分配的空间大小(以样本为单位)。就是说,告诉函数希望每个通道能容纳多少样本数据。
  • in:这是一个指向输入缓冲区的指针。可以把它想象成存放原始音频数据的容器。in 指针的数组表示各个音频通道的输入缓冲区。
  • in_count:这是每个通道输入缓冲区中包含的样本数量。就是说,告诉函数你有多少个输入样本需要转换。

主要作用是将源数据 src_data 进行重采样,然后将重采样后的数据存放到目标缓冲区 dst_data 中

// 使用 SwrContext 进行音频重采样
swresample.swr_convert(
        swrContext, // 重采样上下文
        dts_data, // 目标数据
        nbSamples, // 目标采样数
        src_data, // 原数据
        nbSamples // 原采样数
);

AVFormatContext 类

用于封装和管理多媒体文件的格式信息

获取流的数量 nb_streams()

// 初始化多媒体文件类
AVFormatContext avFormatContext = avformat.avformat_alloc_context();
//----- 省略打开视频和检索流
// 获取流的数量
int i = avFormatContext.nb_streams();

oformat

作用:获取和设置输出容器格式

传递参数表示设置输出容器格式,空参表示获取当前输出容器格式

// 分配一个 AVFormatContext
AVFormatContext formatContext = avformat.avformat_alloc_context();

// 使用 av_guess_format 来猜测输出格式
String outputFilename = "output.mp4";
AVOutputFormat outputFormat = avformat.av_guess_format(null, outputFilename, null);

// 检查 outputFormat 是否为 null
if (outputFormat == null) {
    System.err.println("无法猜测输出格式");
    return;
}

// 设置输出格式
formatContext.oformat(outputFormat);

// 获取并打印当前的输出格式
AVOutputFormat currentFormat = formatContext.oformat();
System.out.println("输出格式: " + currentFormat.name().getString());

pb

用于获取和设置 AVIOContext。AVIOContext 是一个表示输入/输出上下文的数据结构。

public native AVFormatContext pb(AVIOContext setter);

案例:AVFormatContext 的基本使用

public static void main(String[] args) {

    // 视频路径
    String videoSrc = "C:\\Users\\31094\\Desktop\\c3.mp4";

    // 分配并初始化一个空的多媒体文件处理类
    AVFormatContext avFormatContext = avformat.avformat_alloc_context();
    // 静态类,用于初始化AVFormatContext
    int result = avformat.avformat_open_input(avFormatContext, videoSrc, null, null);
    if (result < 0) {
        throw new RuntimeException("无法打开输入文件");
    }
    // 查找输入流的信息
    int i = avformat.avformat_find_stream_info(avFormatContext, (AVDictionary) null);
    if (i < 0) {
        throw new RuntimeException("找不到流信息。");
    }
    // 打印所有流的信息
    for (int j = 0; j < avFormatContext.nb_streams(); j++) {
        AVStream stream = avFormatContext.streams(j);
        AVCodecParameters codecpar = stream.codecpar();
        System.out.println("Stream " + j + ": codec_type=" + codecpar.codec_type() +
                ", width=" + codecpar.width() +
                ", height=" + codecpar.height());
    }
    // 读取数据包
    AVPacket packet = new AVPacket();
    int K = avformat.av_read_frame(avFormatContext, packet);
    if (K < 0) {
        throw new RuntimeException("无法读取帧。");
    }

    System.out.println("Packet size: " + packet.size() + ", pts: " + packet.pts() + ", dts: " + packet.dts());

    // 刷新缓存区
    int C = avformat.avformat_flush(avFormatContext);
    if (C < 0) {
        throw new RuntimeException("无法刷新格式上下文。");
    }

    // 关闭输入文件和释放 AVFormatContext 对象
    avformat.avformat_close_input(avFormatContext);
    avformat.avformat_free_context(avFormatContext);
}

AVStream 流对象

获取流对象

用于获取指定索引 i 处的 AVStream 对象。

AVStream 包含了关于流的详细信息,例如编解码器参数、时间基准、帧率等。

for (i = 0; i < pFormatCtx.nb_streams(); i++) {
    // 获取流的结构体
    AVStream streams = pFormatCtx.streams(i);
}

AVCodecParameters 编码参数

获取当前音视频编码参数

这些参数包含了编解码器类型、视频宽度和高度、音频采样率、声道布局等重要的信息

// 获取流的结构体
AVStream streams = pFormatCtx.streams(i);
// 获取音视频编解码参数
AVCodecParameters codecpar = streams.codecpar();

获取编解码器类型(视频、音频和字幕)

codec_type

// 是否等于视频流
codecpar.codec_type() == avutil.AVMEDIA_TYPE_VIDEO

// avutil.AVMEDIA_TYPE_AUDIO 音频流
// avutil.AVMEDIA_TYPE_SUBTITLE 字幕流

AVCodecContext 类

分配并初始化一个编解码器的上下文

codec_id

获取唯一ID

常见的 AVCodecID 值 一些常见的 AVCodecID 值包括:

  • AV_CODEC_ID_H264:H.264 视频编解码器
  • AV_CODEC_ID_HEVC:HEVC (H.265) 视频编解码器
  • AV_CODEC_ID_AAC:AAC 音频编解码器
  • AV_CODEC_ID_MP3:MP3 音频编解码器

width()

获取视频帧的宽度 像素单位

height()

获取视频帧的高度 像素单位

pix_fmt

用于表示编解码器的像素格式(pixel format)。

// 创建一个 AVCodecContext 对象,假设是视频编码器的上下文
AVCodecContext codecContext = new AVCodecContext(null);
// 设置像素格式为 AV_PIX_FMT_YUV420P
codecContext.pix_fmt(avcodec.AV_PIX_FMT_YUV420P);
// 获取当前的像素格式
int pixFmt = codecContext.pix_fmt();

time_base

作用:用于表示时间基准,所有的帧时间戳(PTS 和 DTS)都以 time_base 为单位进行表示。这个字段对于正确计算和解释媒体流中的时间戳至关重要

比如,如果 time_base 的值是 1/25,这意味着每个时间单位代表,也就是说,每秒钟有 25 个时间单位。

用途:

编码: 在编码时,用户必须设置 time_base。它通常被设置为 1/frame_rate,其中 frame_rate 是视频流的帧率。例如,如果帧率是 30 帧/秒,则 time_base 应设置为 1/30。这样,时间戳的增量将与帧率一致。

解码: 对于解码器,time_base 字段通常是未使用的。解码器主要关注时间戳和数据的解码,而时间基准由编码器提供并用于正确解析帧时间戳。

AVFrame 类

用于表示解码后的视频帧。它包含了大量与视频帧或音频帧相关的信息和数据。

width()

获取当前视频帧的宽度(像素单位)

height()

获取当前视频帧的高度(像素单位)

format(int setter)

用于获取或设置视频帧或音频帧的格式。

data()

用于获取帧数据的指针数组。

AVFrame frame = ...; // 假设已经创建和填充了 AVFrame 对象
// 获取帧数据的指针数组
PointerPointer data = frame.data();
// 访问不同平面的数据
Pointer plane0 = data.get(0); // 第一个平面的数据指针
Pointer plane1 = data.get(1); // 第二个平面的数据指针

linesize()

用于获取帧数据中每个平面(plane)的行大小(line size)。这个方法返回一个整数数组,每个元素对应一个平面的行大小。

音视频帧通常由多个平面组成,每个平面的行大小可能不同,特别是在图像格式为分离的 YUV 或 RGB 平面时。

AVFrame frame = ...; // 假设已经创建和填充了 AVFrame 对象
// 获取每个平面的行大小数组
int[] linesizes = frame.linesize();
// 访问不同平面的行大小
int linesize0 = linesizes[0]; // 第一个平面的行大小
int linesize1 = linesizes[1]; // 第二个平面的行大小

AVPacket 类

AVPacket 是用来存储压缩后的音视频数据的包。它通常是由解复用器(也就是负责把文件分解成音频和视频流的模块)生成的,然后传递给解码器(负责解压缩数据的模块)进行处理。或者,它是从编码器(负责压缩数据的模块)生成的,之后传递给复用器(负责把音频和视频流合并成一个文件的模块)。在处理视频的时候,AVPacket 通常包含一个压缩后的视频帧。

在处理音频的时候,AVPacket 可能包含多个压缩后的音频帧。

方法

stream_index

在多媒体文件(如 MP4、MKV、AVI 等)中,通常包含多个流,每个流都有一个唯一的索引。

  • 空参:表示获取流索引
  • 有参数:表示设置流索引

data

具体数据

size

data所指向的缓冲器大小

AVIOContext 类

用于管理输入/输出(I/O)操作。它是 FFmpeg 用来读写文件或其他 I/O 资源的抽象层。

AVOutputFormat 类

用于描述输出媒体文件的格式。它在创建和写入媒体文件时扮演着关键角色。

video_codec

用于获取和设置默认的视频编解码器。

AVOutputFormat oformat = avFormatContext.oformat();
int videoCodec = oformat.video_codec();

AVRational 类

因此,AVRational 现在表示的时间基准是 1/25 秒,这通常用于设置视频的时间基准,例如帧率为 25 帧每秒。

// 设置 AVRational 的分子部分为 1
avRational.num(1);
// 设置 AVRational 的分母部分为 25
avRational.den(25);

BytePointer 类

主要用于在 Java 中操作 C/C++ 的字节指针。

可以使用 put() 和 get() 方法来读写字节数据。

public BytePointer(Pointer p) {
    super(p);
}
// 获取字节数据
BytePointer pointer = pkt.data();
// 通过指定大小创建数组
byte[] bytes = new byte[pkt.size()];
// 将数据写入到字节数组
pointer.get(bytes);

swscale 类


常量

SWS_BICUBIC

双三次插值算法

在图像缩放时,双三次插值算法通过在原始像素周围的16个像素进行加权平均来生成新像素的值,从而保持图像的平滑性和细节。

图像质量

相较于简单的图像缩放算法,如最近邻插值(Nearest-neighbor interpolation)或双线性插值(Bilinear interpolation),双三次插值通常能够产生更加平滑且质量更高的缩放图像,尤其在缩小图像尺寸或者进行高质量图像处理时效果更为显著。

使用场景

SWS_BICUBIC 适用于需要保留图像细节和纹理的情况,例如视频编辑、图像处理、数字图像放大等领域。它能够有效地减少因缩放而引入的图像锯齿和失真现象,提升图像视觉感知的质量。


方法

sws_getContext

分配并返回 SwsContext,用于创建和配置图像转换上下文(scaling context),也就是用来进行图像缩放、格式转换和颜色空间转换的工具。

  • 参数一:源图像的宽度
  • 参数二:源图像的高度
  • 参数三:源图像的格式
  • 参数四:目标图像的宽度
  • 参数五:目标图像的高度
  • 参数六:目标图像的格式
  • 参数七:指定重新缩放的算法和选项
  • 参数八:用于调整定标器的额外参数
public static native SwsContext sws_getContext
            (int srcW, int srcH, int srcFormat, int dstW, int dstH, int dstFormat, int flags, 
             SwsFilter srcFilter, SwsFilter dstFilter, DoublePointer param);

sws_scale

作用:用于图像的缩放操作、图像格式转换和尺寸调整。这个函数将源图像的一个切片(部分)按指定的尺寸缩放,并将结果写入到目标图像中。

  • 参数一:这是一个缩放上下文,之前通过 sws_getContext() 函数创建。它包含了缩放操作所需的各种信息和设置,比如输入和输出的像素格式、目标尺寸等。
  • 参数二:这是一个指向源图像平面数据的指针数组。每个元素都是一个指向图像平面数据的指针。对于多通道的图像(如YUV格式),每个通道的数据都需要一个指针。
  • 参数三:这是一个数组,其中包含每个平面的步幅(即每行数据的字节数)。步幅用于计算从一行到下一行的偏移量。
  • 参数四: 这是源图像中要处理的切片的起始行号。切片是源图像中的一部分,包含若干行的连续像素数据。
  • 参数五:这是源图像切片的高度,即切片中包含的行数。
  • 参数六:这是一个指向目标图像平面数据的指针数组。类似于 srcSlice,每个元素都是一个指向目标图像平面数据的指针。
  • 参数七:这是一个数组,其中包含每个平面的步幅(即每行数据的字节数)。目标图像的步幅用于计算从一行到下一行的偏移量。

返回值:函数的返回值是输出切片的高度。如果成功,返回的高度应该等于 参数五,如果失败,则返回一个负数表示错误码。

public static void processImage(AVFrame srcFrame, AVFrame dstFrame) {
    // 创建缩放上下文
    SwsContext swsCtx = swscale.sws_getContext(
            srcFrame.width(), srcFrame.height(), srcFrame.format(), // 源图像的尺寸和格式
            dstFrame.width(), dstFrame.height(), dstFrame.format(), // 目标图像的尺寸和格式
            swscale.SWS_BICUBIC, // 缩放算法
            null, null, null // 这些通常为 null,使用默认值
    );

    if (swsCtx == null) {
        System.err.println("Error creating SwsContext");
        return;
    }

    // 执行缩放和格式转换
    int result = swscale.sws_scale(swsCtx,
            srcFrame.data(),     // 源图像的数据指针
            srcFrame.linesize(), // 源图像的步幅
            0,                  // 源图像的起始行
            srcFrame.height(),   // 源图像的高度
            dstFrame.data(),     // 目标图像的数据指针
            dstFrame.linesize()  // 目标图像的步幅
    );

    if (result < 0) {
        System.err.println("Error scaling image: " + result);
    } else {
        System.out.println("Image scaled successfully. Output height: " + result);
    }

    // 释放资源
    swscale.sws_freeContext(swsCtx);
}

Pointer 类

作用是将内存块从源位置拷贝到目标位置

参数解释

  • dst: 目标内存位置的指针。数据将被拷贝到这个位置
  • src: 源内存位置的指针。数据从这个位置拷贝
  • size: 要拷贝的字节数。它指定了从 src 到 dst 复制多少字节的数据

memcpy 方法用于在内存中进行原始数据拷贝。在 JNI 中,这个方法通常被用来实现 Java 和 C/C++ 代码之间的数据传输。它直接操作内存,因此速度非常快,但也需要小心处理,确保内存访问安全,防止越界和数据损坏

// 将 pkt 数据存放到 src_data 中
Pointer dst = src_data.get();
BytePointer pointerData = pkt.data();
int size = pkt.size();
// 进行内存拷贝,按字节拷贝的
Pointer.memcpy(dst, pointerData, size);