再深入FFMPEG
libavdevice
是什么
libavdevice
是 FFmpeg 中的一个库,它提供了对各种音视频设备 的输入和输出支持。这包括摄像头、麦克风、音频接口、视频接口等。通过 libavdevice
,你可以使用 FFmpeg 来处理来自各种设备的音视频数据。
具体来说,libavdevice
提供了一些设备的输入和输出协议,以便于 FFmpeg 可以直接与这些设备进行交互。比如,它包括了一些常见的设备输入协议,如 v4l2
(Video for Linux 2,Linux 下的视频设备接口)、alsa
(Advanced Linux Sound Architecture,Linux 下的音频设备接口)等。对于输出,它也支持将音视频数据输出到一些特定的设备,比如显示器、音频输出设备等。
比如可以使用 libavdevice
提供的设备输入协议 "android_camera" 来打开安卓设备的摄像头,并获取摄像头捕获的视频帧。
总的来说,libavdevice
提供了一个统一的接口,使得 FFmpeg 可以方便地与各种音视频设备进行交互,这使得它成为一个强大的多媒体处理工具。
获取数据
先通过avformat_open_input
获取数据输入,这样做:
cpp
AVInputFormat *input_format = av_find_input_format("android_camera");
// 打开摄像头设备
avformat_open_input(&format_context, device_name, input_format, NULL)
既然打开一个数据输入如此简单,那么avformat_open_input就必须要了解这个函数内部做了什么?
avformat_open_input
如果AVInputFormat参数为NULL的话,这里会根据filename的内容生成不同的AVInputFormat,比如:如果我们传递的filename是一个本地文件路径,那么这里就会生成一个读取本地文件的AVInputFormat,用来将数据内容封装成AVPacket。
如果不为NULL的话,也就是ffmpeg不会自动的去生成AVInputFormat,则会根据我们指定的AVInputFormat解析规则去解析数据。
如果传入的AVInputFormat的flags有AVFMT_NOFILR标记,则filename参数可以为NULL,否则会报错。
android_camera
android_camera是一个AVInputFormat,用来将安卓摄像头中的数据转成AVPACKET
当我们通过av_read_frame读取数据时,就会调用到AVInputFormat的read_paket函数
通过read_paket函数获取到一个AVPACKET,功能由具体的AVInputFormat实现,这里以android_camera举例:
android_camera会从他的avpacket队列中获取一个值,这里的av_thread_message_queue_recv会根据flags执行等待或者不等待,而android-camera会执行等待,直到队列中由新值加入进来。
对于flags只有两种模式:AV_THREAD_MESSAGE_NONBLOCK(非阻塞模式)和0(默认模式 阻塞模式)
在 FFmpeg 中,
av_thread_message_queue_recv
函数用于从线程消息队列中接收消息。这个函数的作用是从指定的线程消息队列中获取消息,并将消息的内容复制到用户提供的缓冲区中。通常情况下,线程之间需要进行通信,而线程消息队列是一种常见的线程间通信的方式。通过将消息放入队列,一个线程可以向另一个线程发送数据或指令,而接收线程则可以使用
av_thread_message_queue_recv
函数来接收并处理这些消息。以下是
av_thread_message_queue_recv
函数的基本用法:
cppint av_thread_message_queue_recv(AVThreadMessageQueue *mq, AVThreadMessage *msg, int flags);
mq
是指向线程消息队列的指针,用于指定从哪个消息队列中接收消息。msg
是一个指向 AVThreadMessage 结构的指针,用于存储接收到的消息内容。flags
是一些控制接收行为的标志位。在调用
av_thread_message_queue_recv
函数后,如果消息队列中有消息可用,它将把消息的内容复制到msg
指向的结构中,并返回一个非负值表示成功接收消息。如果消息队列为空,它可能会根据标志位的设置来等待一段时间,或者立即返回一个指示队列为空的值。
android_camera加入数据到avpacket队列
对于从摄像头获取数据,需要设置监听器,当摄像头有新数据时就会回调该监听器
而这个监听器是由AImageReader_setImageListener
设置的
cpp
media_status_t AImageReader_setImageListener(
AImageReader* reader, AImageReader_ImageListener* listener) __INTRODUCED_IN(24);
AImageReader
源码位置:frameworks/av/media/ndk/NdkImageReader.cpp
首先需要先获取一个AImageReader
通过AImageReader_new
可以获取到一个Reader,第3个参数是获取的图像数据格式:关于支持的格式在NdkImage.h中
但是他并不会对格式进行转换
这个地方有点没看懂,意思应该是默认只支持YUV格式(yuv格式包括YV12),这里的format应该是我们期望获取到的相机的原始数据格式是什么样的,如果不支持则返回错误信息。如果都属于YUV格式,则将AImageReader中的mHalFormat属性重新设置为获取到的图像的Formate格式(这个问题先保留)
前两个参数是图像的默认宽高,如果相机的原始图像数据是HAL_PIXEL_FORMAT_BLOB格式的话,就会使用该默认的宽高,如果不是这种数据就是使用图像的真实宽高。
在Android的相机API中,
HAL_PIXEL_FORMAT_BLOB
是一个特殊的像素格式,它被用来处理非图像数据或者特殊的图像数据。这种格式的数据通常是不透明的,即应用程序通常不能直接访问或修改这种格式的数据。在实际使用中,
HAL_PIXEL_FORMAT_BLOB
常常被用来处理JPEG压缩的图像数据。当相机设置为这种格式时,相机会直接输出JPEG格式的图像数据,而不是常见的YUV或者RAW格式的图像数据。这样做的好处是,JPEG格式的图像数据体积较小,可以节省存储空间。而且,因为JPEG格式广泛被支持,所以这种格式的图像数据可以直接被大多数的图像查看或处理软件使用。需要注意的是,
HAL_PIXEL_FORMAT_BLOB
格式的数据通常不能直接用来进行图像处理。如果你需要对图像进行处理,你可能需要将这种格式的数据转换为其他格式,例如YUV或者RGB格式。
AImageReader_ImageListener
js
typedef void (*AImageReader_ImageCallback)(void* context, AImageReader* reader);
通过reader获取Image
js
media_status_t AImageReader_acquireLatestImage(AImageReader* reader, /*out*/AImage** image)
Image转Packet
需要通过以下方法设置packet的一些属性:
- pts:通过
AImage_getTimestamp
获取到时间戳 - data: av_image_copy_to_buffer设置图像数据, 其中的参数需要通过:av_image_get_buffer_size、AImage_getPlaneRowStride和AImage_getPlaneData获取
一些函数
AImage_getPlanePixelStride
AImage_getPlanePixelStride
是一个Android NDK(Native Development Kit)中的函数,用于处理Android平台上的图像。该函数的作用是获取给定图像平面的像素跨距(Pixel Stride)。像素跨距表示在图像的连续像素之间的字节间距。
在处理多平面图像(例如YUV格式图像)时,了解像素跨距是很重要的。多平面图像通常将图像的不同分量(如亮度和色度分量)分开存储在不同的平面中。这些平面可能具有不同的像素跨距,即在连续像素之间的字节间距可能不同。
AImage_getPlanePixelStride
函数接收两个参数:
const AImage* image
- 指向要查询的AImage对象的指针。int32_t planeIdx
- 要查询的图像平面的索引。
平面索引(plane index)通常是指YUV这三个平面。在多平面图像格式(如YUV)中,图像的不同分量(亮度和色度分量)被分开存储在不同的平面中。在YUV图像中,有三个平面:
- Y平面(亮度平面):包含图像的亮度信息,每个像素都有一个亮度值。在YUV图像中,Y平面的索引通常为0。
- U平面(色度平面):包含图像的蓝色色差信息,通常以子采样的方式存储。在YUV图像中,U平面的索引通常为1。
- V平面(色度平面):包含图像的红色色差信息,通常以子采样的方式存储。在YUV图像中,V平面的索引通常为2。
由于NV12和NV12的UV平面是合并存储的,所以一般会占两个字节,而一般的YUV420P,分别存储各占一个字节。但是他们的平面数还是3个
AImage_getPlaneData
AImage_getPlaneData
函数是Android平台上用于获取图像数据的函数。这个函数可以返回一个指向图像数据的指针,以及这些数据的字节大小。
在YUV格式的图像中,AImage_getPlaneData
可以用来获取Y、U、V平面的数据。例如,你可以使用AImage_getPlaneData
函数获取Y平面的数据,然后再使用AImage_getPlanePixelStride
和AImage_getPlaneRowStride
函数来确定如何遍历这些数据。
这个函数的原型如下:
cpp
media_status_t AImage_getPlaneData(
const AImage* image,
int32_t planeIdx,
uint8_t** data,
int* dataLength);
其中,image
是要处理的图像,planeIdx
是要获取数据的平面索引(对于YUV 420格式的图像,0表示Y平面,1表示U平面,2表示V平面),data
是一个指向指针的指针,该函数会设置这个指针指向平面的数据,dataLength
是一个指向整数的指针,该函数会设置这个整数为数据的字节大小。
需要注意的是,这个函数并不会复制数据,返回的指针直接指向图像的内存,因此你需要确保在调用AImage_delete
函数删除图像之前不要使用这个指针。
在NV12格式中,Y平面的数据排列在前,然后是V平面,最后是U平面。所以,如果您发现U平面的*data
大于V平面的*data
,那么这很可能意味着图像是YV12格式的。
在NV12格式中,Y平面的数据排列在前,然后是U和V平面的交错排列。也就是说,U和V平面的数据是交替存储的。在这种情况下,U平面的*data
会小于V平面的*data
。
特别
HAL_PIXEL_FORMAT_YCrCb_420_SP
在Android的图像格式中,对应安卓FFMPEG下的AV_PIX_FMT_NV12或者AV_PIX_FMT_NV21,具体区分需要通过AImage_getPlaneData,上面有写。