【再深入FFMPEG】libavdevice的作用和android_camera的使用

1,235 阅读15分钟

【再深入FFMPEG】过滤器Filter执行流程

再深入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获取数据输入,这样做:

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

image.png

如果AVInputFormat参数为NULL的话,这里会根据filename的内容生成不同的AVInputFormat,比如:如果我们传递的filename是一个本地文件路径,那么这里就会生成一个读取本地文件的AVInputFormat,用来将数据内容封装成AVPacket。

如果不为NULL的话,也就是ffmpeg不会自动的去生成AVInputFormat,则会根据我们指定的AVInputFormat解析规则去解析数据。

如果传入的AVInputFormat的flags有AVFMT_NOFILR标记,则filename参数可以为NULL,否则会报错。

同时还会设置AVInputFormat的默认属性。

默认属性的设置 就AVInputFormat而言。他的avclass有一个option属性,会通过av_opt_set_defaults这个方法将option中的属性值会根据offset值和default_val值设置到AVFormatContext->priv_data中,AVFormatContext->priv_data的值的大小是AVInputFormat->priv_data_size,在android_camera中,AVFormatContext->priv_data就是AndroidCameraCtx*

android_camera

android_camera是一个AVInputFormat,用来将安卓摄像头中的数据转成AVPACKET

企业微信截图_16998555167848.png

当我们调用avformat_open_input时,就会调用到AVInputFormat的read_header函数

image.png

当我们通过av_read_frame读取数据时,就会调用到AVInputFormat的read_paket函数

image.png

通过read_paket函数获取到一个AVPACKET,功能由具体的AVInputFormat实现,这里以android_camera举例:

image.png

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 函数的基本用法:

int av_thread_message_queue_recv(AVThreadMessageQueue *mq, AVThreadMessage *msg, int flags);
  • mq 是指向线程消息队列的指针,用于指定从哪个消息队列中接收消息。
  • msg 是一个指向 AVThreadMessage 结构的指针,用于存储接收到的消息内容。
  • flags 是一些控制接收行为的标志位。

在调用 av_thread_message_queue_recv 函数后,如果消息队列中有消息可用,它将把消息的内容复制到 msg 指向的结构中,并返回一个非负值表示成功接收消息。如果消息队列为空,它可能会根据标志位的设置来等待一段时间,或者立即返回一个指示队列为空的值。

android_camera加入数据到avpacket队列

image.png

对于从摄像头获取数据,需要设置监听器,当摄像头有新数据时就会回调该监听器

而这个监听器是由AImageReader_setImageListener设置的

media_status_t AImageReader_setImageListener(  
    AImageReader* reader, AImageReader_ImageListener* listener) __INTRODUCED_IN(24);

AImageReader

源码位置:frameworks/av/media/ndk/NdkImageReader.cpp

edit

首先需要先获取一个AImageReader

image.png

通过AImageReader_new可以获取到一个Reader,第3个参数是获取的图像数据格式:关于支持的格式在NdkImage.h中,并且AImageReader_new的宽高参数就是所要求的图像分辨率大小

但是他并不会对格式进行转换

这个地方有点没看懂,意思应该是默认只支持YUV格式(yuv格式包括YV12),这里的format应该是我们期望获取到的相机的原始数据格式是什么样的,如果不支持则返回错误信息。如果都属于YUV格式,则将AImageReader中的mHalFormat属性重新设置为获取到的图像的Formate格式(这个问题先保留)

image.png

前两个参数是图像的默认宽高,如果相机的原始图像数据是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

image.png

typedef void (*AImageReader_ImageCallback)(void* context, AImageReader* reader);

通过reader获取Image

media_status_t AImageReader_acquireLatestImage(AImageReader* reader, /*out*/AImage** image)

Image转Packet

需要通过以下方法设置packet的一些属性:

  1. pts:通过AImage_getTimestamp获取到时间戳
  2. 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 函数接收两个参数:

  1. const AImage* image - 指向要查询的AImage对象的指针。
  2. int32_t planeIdx - 要查询的图像平面的索引。

平面索引(plane index)通常是指YUV这三个平面。在多平面图像格式(如YUV)中,图像的不同分量(亮度和色度分量)被分开存储在不同的平面中。在YUV图像中,有三个平面:

  1. Y平面(亮度平面):包含图像的亮度信息,每个像素都有一个亮度值。在YUV图像中,Y平面的索引通常为0。
  2. U平面(色度平面):包含图像的蓝色色差信息,通常以子采样的方式存储。在YUV图像中,U平面的索引通常为1。
  3. V平面(色度平面):包含图像的红色色差信息,通常以子采样的方式存储。在YUV图像中,V平面的索引通常为2。

由于NV12和NV12的UV平面是合并存储的,所以一般会占两个字节,而一般的YUV420P,分别存储各占一个字节。但是他们的平面数还是3个

AImage_getPlaneData

AImage_getPlaneData函数是Android平台上用于获取图像数据的函数。这个函数可以返回一个指向图像数据的指针,以及这些数据的字节大小。

在YUV格式的图像中,AImage_getPlaneData可以用来获取Y、U、V平面的数据。例如,你可以使用AImage_getPlaneData函数获取Y平面的数据,然后再使用AImage_getPlanePixelStrideAImage_getPlaneRowStride函数来确定如何遍历这些数据。

这个函数的原型如下:

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

获取相机信息 ACameraMetadata_getConstEntry

ACameraMetadata_getConstEntry是一个用于从ACameraMetadata结构中获取特定元数据条目的函数。ACameraMetadata结构包含了与相机设备相关的各种元数据信息,例如相机的可用性、传感器的属性、镜头的属性、图像处理参数等。这些元数据可以帮助开发者了解相机设备的特性和能力,从而调整应用程序以适应不同的设备。

ACameraMetadata_getConstEntry函数的原型如下:

camera_status_t ACameraMetadata_getConstEntry(
    const ACameraMetadata* metadata,
    uint32_t tag,
    ACameraMetadata_const_entry* entry
);

参数说明:

  • metadata:指向ACameraMetadata结构的指针,该结构包含了相机设备的元数据信息。
  • tag:一个uint32_t类型的值,表示要获取的元数据条目的标签。这些标签在camera_metadata_tags.h头文件中定义,例如ANDROID_SENSOR_INFO_PHYSICAL_SIZEANDROID_LENS_INFO_AVAILABLE_APERTURES等。
  • entry:指向一个ACameraMetadata_const_entry结构的指针,该结构将被填充为指定的元数据条目。如果函数成功获取了元数据条目,entry将包含条目的数据和相关信息。

函数的返回值是一个camera_status_t类型,表示函数执行的结果。如果函数成功获取了元数据条目,返回值将是ACAMERA_OK。如果发生错误,返回值将是一个表示错误类型的camera_status_t枚举值(ACAMERA_ERROR_*)。

比如我要获取ACAMERA_SCALER_AVAILABLE_STREAM_CONFIGURATIONS的数据,它是一个包含四元组的数组,每个四元组代表一个可用的流配置。每个四元组的元素分别是:

  • 流的格式(例如,HAL_PIXEL_FORMAT_RAW16HAL_PIXEL_FORMAT_BLOB等)
  • 流的宽度
  • 流的高度
  • 输入或输出标志(0表示此配置用于输出,1表示此配置用于输入)

所以,当你从data中取出数据时,你需要按照这个四元组的结构来获取数据。data.i32[i * 4 + 3]data.i32[i * 4 + 0]分别获取的是输入/输出标志和流的格式。

如果你想获取流的宽度和高度,你可以使用data.i32[i * 4 + 1]data.i32[i * 4 + 2]

ACameraCaptureSession_setRepeatingRequest

ACameraCaptureSession_setRepeatingRequest是Android Camera2 API中的一个函数。它的主要作用是设置一个重复的捕获请求,这个请求会不断地向相机发送,使得相机持续地捕获图像,例如视频。

这个函数的原型如下:

camera_status_t ACameraCaptureSession_setRepeatingRequest(
    ACameraCaptureSession* session,
    ACameraCaptureSession_captureCallbacks* callbacks,
    int numRequests,
    const ACaptureRequest** requests,
    /*out*/int* sequenceId);

参数说明:

  • session:一个已经创建的相机捕获会话。
  • callbacks:一个指向ACameraCaptureSession_captureCallbacks结构体的指针,用于设置捕获回调函数。
  • numRequests:重复请求的数量。
  • requests:一个指向ACaptureRequest指针数组的指针,用于设置重复的捕获请求。
  • sequenceId:一个指向int类型的指针,用于返回序列ID。

当这个函数被调用后,相机会根据requests参数中的设置,持续地捕获图像。每次图像捕获完成后,相应的回调函数就会被调用。

这个函数通常用于实现连续的图像捕获,例如视频录制或者预览显示。通过设置不同的ACaptureRequest,你可以控制相机的各种参数,例如曝光时间、ISO、焦距等。

ACameraCaptureSession_capture

ACameraCaptureSession_capture 和 ACameraCaptureSession_setRepeatingRequest 都是用来提交捕获请求的,但它们的使用场景和特点有所不同。

  • ACameraCaptureSession_capture:这个函数用于提交一次性的捕获请求。当这个请求被处理完毕后,相机会停止捕获图像。这个函数通常用于拍摄静态照片。
  • ACameraCaptureSession_setRepeatingRequest:这个函数用于提交重复的捕获请求。当这个请求被设置后,相机会持续地捕获图像,直到另一个请求被提交或者捕获会话被关闭。这个函数通常用于视频录制或者预览显示。

简单来说,ACameraCaptureSession_capture是用于一次性操作,比如拍照,而ACameraCaptureSession_setRepeatingRequest是用于连续操作,比如视频录制或者实时预览。

特别

  1. HAL_PIXEL_FORMAT_YCrCb_420_SP在Android的图像格式中,对应安卓FFMPEG下的AV_PIX_FMT_NV12或者AV_PIX_FMT_NV21,具体区分需要通过AImage_getPlaneData,上面有写。

image.png

  1. CaptureSessionCaptureRequest
  • CaptureSession:捕获会话(Capture Session)是相机设备和为捕获图像配置的输出表面之间的一种连接。它负责管理与相机设备的生命周期,包括开始捕获、处理捕获请求、停止捕获等。一个CaptureSession可以处理多个CaptureRequest。比如:一个手机可能有几个相机,这个session就定义了和哪个相机进行通讯。
  • CaptureRequest:捕获请求(Capture Request)是一个包含了相机设备如何捕获图像的指令集。它定义了一次捕获操作的所有参数,包括曝光、焦距、感光度(ISO)等设置,以及输出表面(用于接收图像数据)。一个CaptureRequest可以被一个或多个CaptureSession处理。

简单来说,CaptureSession是相机设备和输出表面之间的连接,负责管理捕获的生命周期,而CaptureRequest是一次具体的捕获操作,定义了如何捕获图像。

  1. session_output_container

CaptureSessionOutputContainer在Android Camera2 API中是一个非常重要的组件,它用于管理与CameraCaptureSession相关的所有输出Surface

当你创建一个CameraCaptureSession时,你需要提供一个CaptureSessionOutputContainer,它包含了所有你想要相机输出到的Surface。这些Surface可能来自ImageReaderMediaRecorder或其他可以接收相机输出的对象。

例如,你可能会这样使用CaptureSessionOutputContainer

CaptureSessionOutputContainer outputs = new CaptureSessionOutputContainer();
outputs.add(new CaptureSessionOutput(reader.getSurface()));
cameraDevice.createCaptureSession(outputs, new MySessionCallback(), handler);

在这个例子中,我们创建了一个新的CaptureSessionOutputContainer,然后添加了一个来自ImageReaderSurface。然后我们使用这个CaptureSessionOutputContainer来创建一个新的CameraCaptureSession

我们之前虽然创建了一个ImageReder,但我们并没有将它关联到任何一个window,也就是图像源。而这里我们通过ACaptureSessionOutput_create创建一个SessionOutput,并将此SessionOutput关联到这个ImageReader,这个SessionOutput又保存在CaptureSessionOutputContainer中,而相机的输出数据正是会输出到CaptureSessionOutputContainer的所有的SessionOutput中。这样一来ImageReader就可以获取到数据了。

所以,CaptureSessionOutputContainer的作用就是管理和组织所有的输出Surface,并在创建CameraCaptureSession时提供给它。