我们做一个音视频数据无非是
拉流(采集数据)--> 像素数据 / 音频数据 --> 解码 --> 封装数据 --> 协议推流
基础知识概括
图像封装格式:
常见的封装格式有: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分发。 其他服务:
- 还有一些开源的和大型公司的付费流媒体服务,适合不同的应用场景。
知识概念图
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。
- 参数四
- 每个声道的样本数。
- 参数五
- 音频样本格式,定义了音频样本的数据格式。
- 参数六
- 缓冲区对齐方式。通常设置为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);