| /* |
| * Copyright 2021 The Chromium OS Authors. All rights reserved. |
| * Use of this source code is governed by a BSD-style license that can be |
| * found in the LICENSE file. |
| */ |
| |
| // As per https://www.kernel.org/doc/html/v5.4/media/uapi/v4l/dev-decoder.html |
| #include <ctype.h> |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <getopt.h> |
| #include <limits.h> |
| #include <linux/videodev2.h> |
| #include <openssl/md5.h> |
| #include <stdbool.h> |
| #include <stdint.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <sys/ioctl.h> |
| #include <sys/mman.h> |
| #include <unistd.h> |
| |
| #include "bitstreams/bitstream_helper.h" |
| #include "bs_drm.h" |
| #include "v4l2_macros.h" |
| |
| static const char* kDecodeDevice = "/dev/video-dec0"; |
| static const int kInputbufferMaxSize = 4 * 1024 * 1024; |
| static const int kRequestBufferCount = 8; |
| static const uint32_t kInvalidFrameRate = 0; |
| // |kMaxRetryCount = 2^24| takes around 20 seconds to exhaust all retries on |
| // Trogdor when a decode stalls. |
| static const int kMaxRetryCount = 1 << 24; |
| |
| static const char* kImageProcessorDevice = "/dev/image-proc0"; |
| static const int kInvalidBufferIndex = -1; |
| |
| static uint32_t kPreferredUncompressedFourCCs[] = {V4L2_PIX_FMT_NV12, |
| V4L2_PIX_FMT_MT21C}; |
| |
| static int log_run_level = DEFAULT_LOG_LEVEL; |
| |
| struct mmap_buffers { |
| void* start[VIDEO_MAX_PLANES]; |
| size_t length[VIDEO_MAX_PLANES]; |
| struct gbm_bo* bo; |
| }; |
| |
| struct queue { |
| int v4lfd; |
| enum v4l2_buf_type type; |
| uint32_t fourcc; |
| struct mmap_buffers* buffers; |
| // |display_area| describes the bounds of the displayable image relative to |
| // the image buffer. |left| and |top| are the offset from the top left corner |
| // in pixels. |width| and |height| describe the displayable image's width and |
| // height in pixels. |
| struct v4l2_rect display_area; |
| // |coded_width| x |coded_height|: |
| // The size of the encoded frame. |
| // Usually has an alignment of 16, 32 depending on codec. |
| uint32_t coded_width; |
| uint32_t coded_height; |
| // Contains the number of bytes in every row of video (line_stride) in each |
| // plane of an MMAP buffer. Copied from the V4L2 multi-planar format |
| // structure. |
| uint32_t bytes_per_line[VIDEO_MAX_PLANES]; |
| uint32_t cnt; |
| uint32_t num_planes; |
| enum v4l2_memory memory; // V4L2_MEMORY_(MMAP|DMABUF) etc. |
| uint32_t displayed_frames; // Not valid for OUTPUT queues. |
| // Used to track the state of OUTPUT and CAPTURE queues. The CAPTURE queue |
| // stops streaming when it dequeues a buffer with V4L2_BUF_FLAG_LAST set. |
| // The OUTPUT queue should stop streaming when there are no remaining |
| // compressed frames to enqueue to it. |
| bool is_streaming; |
| }; |
| |
| struct md5_hash { |
| uint8_t bytes[16]; |
| }; |
| |
| struct data_buffer { |
| uint8_t* data; |
| size_t size; |
| }; |
| |
| bool is_vp9(uint32_t fourcc) { |
| // VP9 FourCC should match VP9X where 'X' is the version number. This check |
| // ignores the last character, so VP90, VP91, etc. match. |
| return (fourcc & 0xff) == 'V' && |
| (fourcc >> 8 & 0xff) == 'P' && |
| (fourcc >> 16 & 0xff) == '9'; |
| } |
| |
| char* fourcc_to_string(uint32_t fourcc, char* fourcc_string) { |
| sprintf(fourcc_string, "%c%c%c%c", fourcc & 0xff, fourcc >> 8 & 0xff, |
| fourcc >> 16 & 0xff, fourcc >> 24 & 0xff); |
| // Return the pointer for conveniency. |
| return fourcc_string; |
| } |
| |
| // Verifies that |v4lfd| can be queried for capabilities, and prints them. If |
| // the ioctl() fails, a FATAL is issued and terminates the program. |
| void query_driver(int v4lfd) { |
| struct v4l2_capability cap = {}; |
| const int ret = ioctl(v4lfd, VIDIOC_QUERYCAP, &cap); |
| if (ret != 0) |
| LOG_FATAL("VIDIOC_QUERYCAP failed: %s.", strerror(errno)); |
| |
| LOG_INFO("Driver=\"%s\" bus_info=\"%s\" card=\"%s\" fd=0x%x", cap.driver, |
| cap.bus_info, cap.card, v4lfd); |
| } |
| |
| bool is_fourcc_supported(int v4lfd, enum v4l2_buf_type type, uint32_t fourcc) { |
| struct v4l2_fmtdesc fmtdesc = { .type = type }; |
| while (ioctl(v4lfd, VIDIOC_ENUM_FMT, &fmtdesc) == 0) { |
| if (fourcc == fmtdesc.pixelformat) |
| return true; |
| fmtdesc.index++; |
| } |
| return false; |
| } |
| |
| // Verifies that |fd| supports |compressed_format| and, if specified, |
| // |uncompressed_format|. If |uncompressed_format| is not specified, i.e. it's |
| // equal to V4L2_PIX_FMT_INVALID, then kPreferredUncompressedFourCCs are tried |
| // instead and the first supported one returned in |uncompressed_format|. If any |
| // command fails, the function returns false. |
| bool capabilities(int fd, |
| const char* fd_name, |
| uint32_t compressed_format, |
| uint32_t* uncompressed_format) { |
| if (!is_fourcc_supported(fd, V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE, |
| compressed_format)) { |
| char fourcc_str[FOURCC_SIZE + 1]; |
| LOG_ERROR("%s is not supported for OUTPUT, try running `v4l2-ctl " |
| "--list-formats-out-ext -d %s` for more info", |
| fourcc_to_string(compressed_format, fourcc_str), fd_name); |
| return false; |
| } |
| |
| if (*uncompressed_format != V4L2_PIX_FMT_INVALID) { |
| if (!is_fourcc_supported(fd, V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE, |
| *uncompressed_format)) { |
| char fourcc_str[FOURCC_SIZE + 1]; |
| LOG_ERROR("%s is not supported for CAPTURE, try running `v4l2-ctl " |
| "--list-formats-ext -d %s` for more info", |
| fourcc_to_string(*uncompressed_format, fourcc_str), fd_name); |
| return false; |
| } |
| } else { |
| const size_t num_fourccs = |
| sizeof(kPreferredUncompressedFourCCs) / sizeof(uint32_t); |
| for (size_t i = 0; i < num_fourccs; ++i) { |
| if (is_fourcc_supported(fd, V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE, |
| kPreferredUncompressedFourCCs[i])) { |
| *uncompressed_format = kPreferredUncompressedFourCCs[i]; |
| return true; |
| } |
| } |
| return false; |
| } |
| return true; |
| } |
| |
| // The V4L2 device may emit events outside of the buffer data path. For event |
| // types, see https://www.kernel.org/doc/html/v5.4/media/uapi/v4l/vidioc-dqevent.html#id2 |
| int subscribe_to_event(int v4lfd, uint32_t event_type, uint32_t id) { |
| struct v4l2_event_subscription sub = {.type = event_type, |
| .id = id, |
| .flags = 0}; |
| |
| const int ret = ioctl(v4lfd, VIDIOC_SUBSCRIBE_EVENT, &sub); |
| if (ret != 0) |
| LOG_ERROR("VIDIOC_SUBSCRIBE_EVENT failed: %s.", strerror(errno)); |
| |
| return ret; |
| } |
| |
| // Dequeues an event from the device. Only subscribed events can be dequeued. |
| // If there are no events, the return value is non-zero. |
| int dequeue_event(struct queue* queue, uint32_t* type) { |
| // v4l2_event structure will be completely filled out by the driver. |
| struct v4l2_event event; |
| |
| int ret = ioctl(queue->v4lfd, VIDIOC_DQEVENT, &event); |
| if (!ret && type) |
| *type = event.type; |
| |
| return ret; |
| } |
| |
| bool apply_selection_to_queue(struct queue* queue, |
| struct v4l2_rect display_area) { |
| struct v4l2_selection selection = { |
| // |type| has a note in the header:"(do not use *_MPLANE types)". |
| .type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE, |
| .target = V4L2_SEL_TGT_CROP, |
| .flags = 0}; |
| memcpy(&selection.r, &display_area, sizeof(struct v4l2_rect)); |
| if (ioctl(queue->v4lfd, VIDIOC_S_SELECTION, &selection) != 0) { |
| LOG_ERROR("VIDIOC_S_SELECTION failed: %s.", strerror(errno)); |
| return false; |
| } |
| assert(selection.r.left == 0); |
| assert(selection.r.top == 0); |
| LOG_INFO("Queue selection %dx%d", selection.r.width, selection.r.height); |
| return true; |
| } |
| |
| int request_mmap_buffers(struct queue* queue, |
| struct v4l2_requestbuffers* reqbuf) { |
| const int v4lfd = queue->v4lfd; |
| const uint32_t buffer_alloc = reqbuf->count * sizeof(struct mmap_buffers); |
| struct mmap_buffers* buffers = (struct mmap_buffers*)malloc(buffer_alloc); |
| assert(buffers); |
| memset(buffers, 0, buffer_alloc); |
| queue->buffers = buffers; |
| queue->cnt = reqbuf->count; |
| |
| int ret; |
| for (uint32_t i = 0; i < reqbuf->count; ++i) { |
| struct v4l2_buffer buffer; |
| struct v4l2_plane planes[VIDEO_MAX_PLANES]; |
| memset(&buffer, 0, sizeof(buffer)); |
| buffer.type = reqbuf->type; |
| buffer.memory = queue->memory; |
| buffer.index = i; |
| buffer.length = queue->num_planes; |
| buffer.m.planes = planes; |
| ret = ioctl(v4lfd, VIDIOC_QUERYBUF, &buffer); |
| if (ret != 0) { |
| LOG_ERROR("VIDIOC_QUERYBUF failed: %d.", ret); |
| break; |
| } |
| |
| for (uint32_t j = 0; j < queue->num_planes; ++j) { |
| buffers[i].length[j] = buffer.m.planes[j].length; |
| buffers[i].start[j] = |
| mmap(NULL, buffer.m.planes[j].length, PROT_READ | PROT_WRITE, |
| MAP_SHARED, v4lfd, buffer.m.planes[j].m.mem_offset); |
| if (MAP_FAILED == buffers[i].start[j]) { |
| LOG_ERROR("Failed to mmap buffer of length(%d) and offset(0x%x).", |
| buffer.m.planes[j].length, buffer.m.planes[j].m.mem_offset); |
| ret = -1; |
| } |
| } |
| } |
| |
| return ret; |
| } |
| |
| int request_dmabuf_buffers(struct gbm_device* gbm, |
| struct queue* CAPTURE_queue, |
| uint64_t modifier, const int count) { |
| int ret = 0; |
| const uint32_t buffer_alloc = count * sizeof(struct mmap_buffers); |
| struct mmap_buffers* buffers = (struct mmap_buffers*)malloc(buffer_alloc); |
| assert(buffers); |
| memset(buffers, 0, buffer_alloc); |
| CAPTURE_queue->buffers = buffers; |
| CAPTURE_queue->cnt = count; |
| |
| uint32_t gbm_format; |
| if (CAPTURE_queue->fourcc == V4L2_PIX_FMT_NV12 || |
| CAPTURE_queue->fourcc == V4L2_PIX_FMT_MT21C) { |
| gbm_format = GBM_FORMAT_NV12; |
| } else if (CAPTURE_queue->fourcc == V4L2_PIX_FMT_P010) { |
| gbm_format = GBM_FORMAT_P010; |
| } else { |
| char fourcc_str[FOURCC_SIZE + 1]; |
| LOG_ERROR("%s format not supported for the CAPTURE_queue", |
| fourcc_to_string(CAPTURE_queue->fourcc, fourcc_str)); |
| return -1; |
| } |
| |
| for (uint32_t i = 0; i < CAPTURE_queue->cnt; ++i) { |
| const uint32_t width = CAPTURE_queue->coded_width; |
| const uint32_t height = CAPTURE_queue->coded_height; |
| |
| struct gbm_bo* bo = gbm_bo_create_with_modifiers( |
| gbm, width, height, gbm_format, &modifier, /*count=*/1); |
| assert(bo); |
| CAPTURE_queue->buffers[i].bo = bo; |
| } |
| |
| return ret; |
| } |
| |
| // NV12 to I420 conversion. |
| // This function converts the NV12 |buffer_in| into an I420 buffers stored in a |
| // struct data_buffer. Ownership of the return structure's |.data| member is |
| // transferred to the caller. It must be freed elsewhere. |
| // |buffer_in| is padded, whereas the return buffer is tightly packed. |
| // Example: |display_area.left| = 0, |display_area.top| = 0, |
| // |display_area.width| = 8, |display_area.height| = 2, |buffer_width| = 10. |
| // |
| // NV12 I420 |
| // YYYYYYYY00 YYYYYYYY |
| // YYYYYYYY00 YYYYYYYY |
| // UVUVUVUV00 UUUUVVVV |
| // |
| // HW pads 0s for |buffer_width - display_area.left - display_area.width| |
| // bytes after each row on Trogdor. But other platforms might leave the padding |
| // uninitialized, and in yet others accessing it might causes a crash of some |
| // sort (access violation). |
| struct data_buffer nv12_to_i420(struct v4l2_rect display_area, |
| uint32_t buffer_width, |
| uint32_t buffer_height, |
| uint8_t* buffer_in) { |
| |
| const size_t y_plane_size = display_area.width * display_area.height; |
| |
| // Both NV12 and I420 are 4:2:0, so the U and V planes are downsampled by a |
| // factor of two in width and height respective to the Y plane. |
| // When an image dimension is odd, round the downsampled dimension up. |
| const size_t u_plane_width = (display_area.width + 1) / 2; |
| const size_t u_plane_height = (display_area.height + 1) / 2; |
| const size_t u_plane_size = u_plane_width * u_plane_height; |
| |
| struct data_buffer buffer_out = {.data = NULL, |
| .size = y_plane_size + 2 * u_plane_size}; |
| buffer_out.data = malloc(sizeof(uint8_t) * buffer_out.size); |
| assert(buffer_out.data); |
| memset(buffer_out.data, 0, sizeof(uint8_t) * buffer_out.size); |
| |
| // Copies luma data from |buffer_in| one row at a time |
| // to avoid touching the padding. |
| for (int row = 0; row < display_area.height; ++row) { |
| memcpy(buffer_out.data + row * display_area.width, |
| buffer_in + display_area.left + |
| (display_area.top + row) * buffer_width, |
| display_area.width); |
| } |
| |
| uint8_t* u_plane_out = &buffer_out.data[y_plane_size]; |
| uint8_t* v_plane_out = u_plane_out + u_plane_size; |
| const size_t uv_plane_offset = buffer_width * buffer_height; |
| |
| for (int row = 0; row < u_plane_height; ++row) { |
| for (int column = 0; column < u_plane_width; ++column) { |
| *(u_plane_out + row * u_plane_width + column) = |
| buffer_in[uv_plane_offset + (display_area.top + row) * buffer_width + |
| 2 * (display_area.left + column)]; |
| |
| *(v_plane_out + row * u_plane_width + column) = |
| buffer_in[uv_plane_offset + (display_area.top + row) * buffer_width + |
| 2 * (display_area.left + column) + 1]; |
| } |
| } |
| |
| return buffer_out; |
| } |
| |
| void compute_and_print_md5hash(const void *data, |
| size_t len, |
| uint32_t frame_index) { |
| struct md5_hash hash; |
| MD5_CTX ctx; |
| |
| MD5_Init(&ctx); |
| MD5_Update(&ctx, data, len); |
| MD5_Final(hash.bytes, &ctx); |
| |
| // This printout follows the ways of Tast PlatformDecoding. Don't change ! |
| LOG_INFO("frame # %d - ", frame_index); |
| for (int n = 0; n < 16; ++n) |
| printf("%02x", hash.bytes[n]); |
| printf("\n"); |
| } |
| |
| // Handles cropping a frame to the displayable area. The returned struct |
| // data_buffer member |data| must be freed by the caller. |
| struct data_buffer map_buffers_and_convert_to_i420(struct queue* CAPTURE_queue, |
| uint32_t queue_index) { |
| assert(CAPTURE_queue->memory == V4L2_MEMORY_DMABUF || |
| CAPTURE_queue->memory == V4L2_MEMORY_MMAP); |
| // TODO: only handles 8 bit pixels |
| assert(CAPTURE_queue->fourcc == V4L2_PIX_FMT_NV12); |
| |
| // Maps the frame buffer into |buffer_nv12| and get the buffer's stride and |
| // size metadata for cropping and deinterleaving the image. |
| uint32_t buffer_bytes_per_line = 0; |
| uint32_t buffer_height = 0; |
| size_t buffer_size = 0; |
| uint8_t* buffer_nv12 = NULL; |
| int bo_fd = 0; |
| |
| if (CAPTURE_queue->memory == V4L2_MEMORY_DMABUF) { |
| struct gbm_bo* bo = CAPTURE_queue->buffers[queue_index].bo; |
| bo_fd = gbm_bo_get_fd(bo); |
| assert(bo_fd > 0); |
| buffer_size = lseek(bo_fd, 0, SEEK_END); |
| lseek(bo_fd, 0, SEEK_SET); |
| |
| assert(gbm_bo_get_stride_for_plane(bo, 0) == |
| gbm_bo_get_stride_for_plane(bo, 1)); |
| |
| buffer_bytes_per_line = gbm_bo_get_stride_for_plane(bo, 0); |
| buffer_height = gbm_bo_get_height(bo); |
| |
| buffer_nv12 = mmap(0, buffer_size, PROT_READ, MAP_SHARED, bo_fd, 0); |
| } else { |
| assert(CAPTURE_queue->memory == V4L2_MEMORY_MMAP); |
| // TODO(nhebert) doesn't support multi-planar MMAP buffers |
| assert(CAPTURE_queue->num_planes == 1); |
| |
| buffer_bytes_per_line = CAPTURE_queue->bytes_per_line[0]; |
| buffer_height = CAPTURE_queue->coded_height; |
| |
| buffer_size = CAPTURE_queue->buffers[queue_index].length[0]; |
| buffer_nv12 = CAPTURE_queue->buffers[queue_index].start[0]; |
| } |
| |
| assert(buffer_bytes_per_line * buffer_height * 3 / 2 <= buffer_size); |
| |
| // Libvpx golden md5 hashes are calculated in I420 format. |
| // Deinterleave |buffer_nv12| and apply the display area. |
| struct data_buffer buffer_i420 = nv12_to_i420(CAPTURE_queue->display_area, |
| buffer_bytes_per_line, |
| buffer_height, |
| buffer_nv12); |
| |
| if (CAPTURE_queue->memory == V4L2_MEMORY_DMABUF) { |
| munmap(buffer_nv12, buffer_size); |
| close(bo_fd); |
| } |
| |
| return buffer_i420; |
| } |
| |
| // Maps |CAPTURE_queue|'s buffer at |queue_index| and calculates its MD5 sum. |
| void map_buffers_and_calculate_md5hash(struct queue* CAPTURE_queue, |
| uint32_t queue_index) { |
| |
| struct data_buffer buffer_i420 = |
| map_buffers_and_convert_to_i420(CAPTURE_queue, queue_index); |
| assert(buffer_i420.data); |
| |
| compute_and_print_md5hash(buffer_i420.data, buffer_i420.size, |
| CAPTURE_queue->displayed_frames); |
| |
| free(buffer_i420.data); |
| } |
| |
| // This is the input queue that will take compressed data. |
| // 4.5.1.5 |
| int setup_OUTPUT(struct queue* OUTPUT_queue, |
| const uint32_t* optional_width, |
| const uint32_t* optional_height) { |
| int ret = 0; |
| |
| // 1. Sets the coded format on OUTPUT via VIDIOC_S_FMT(). |
| if (!ret) { |
| struct v4l2_format fmt; |
| memset(&fmt, 0, sizeof(fmt)); |
| |
| fmt.type = OUTPUT_queue->type; |
| if (optional_width) |
| fmt.fmt.pix_mp.width = *optional_width; |
| if (optional_height) |
| fmt.fmt.pix_mp.height = *optional_height; |
| fmt.fmt.pix_mp.pixelformat = OUTPUT_queue->fourcc; |
| if (OUTPUT_queue->num_planes == 1) |
| fmt.fmt.pix_mp.plane_fmt[0].sizeimage = kInputbufferMaxSize; |
| fmt.fmt.pix_mp.num_planes = OUTPUT_queue->num_planes; |
| |
| int ret = ioctl(OUTPUT_queue->v4lfd, VIDIOC_S_FMT, &fmt); |
| if (ret != 0) |
| LOG_ERROR("OUTPUT queue: VIDIOC_S_FMT failed: %s.", strerror(errno)); |
| } |
| |
| // 2. Allocates source (bytestream) buffers via VIDIOC_REQBUFS() on OUTPUT. |
| if (!ret) { |
| struct v4l2_requestbuffers reqbuf = {.count = kRequestBufferCount, |
| .type = OUTPUT_queue->type, |
| .memory = OUTPUT_queue->memory}; |
| |
| ret = ioctl(OUTPUT_queue->v4lfd, VIDIOC_REQBUFS, &reqbuf); |
| if (ret != 0) |
| LOG_ERROR("OUTPUT queue: VIDIOC_REQBUFS failed: %s.", strerror(errno)); |
| |
| char fourcc[FOURCC_SIZE + 1]; |
| LOG_INFO("OUTPUT queue: %d buffers requested, %d buffers for compressed " |
| "data (%s) returned.", kRequestBufferCount, reqbuf.count, |
| fourcc_to_string(OUTPUT_queue->fourcc, fourcc)); |
| |
| if (OUTPUT_queue->memory == V4L2_MEMORY_MMAP) |
| ret = request_mmap_buffers(OUTPUT_queue, &reqbuf); |
| } |
| |
| // 3. Starts streaming on the OUTPUT queue via VIDIOC_STREAMON(). |
| if (!ret) { |
| ret = ioctl(OUTPUT_queue->v4lfd, VIDIOC_STREAMON, &OUTPUT_queue->type); |
| if (ret != 0) |
| LOG_ERROR("OUTPUT queue: VIDIOC_STREAMON failed: %s.", strerror(errno)); |
| else |
| OUTPUT_queue->is_streaming = true; |
| } |
| |
| return ret; |
| } |
| |
| // Reads parsed compressed frame data from |file_buf| and submits it to |
| // |OUTPUT_queue|. |
| int submit_compressed_data(struct queue* OUTPUT_queue, uint32_t queue_index) { |
| struct mmap_buffers* buffers = OUTPUT_queue->buffers; |
| const size_t buf_size = fill_compressed_buffer( |
| buffers[queue_index].start[0], buffers[queue_index].length[0]); |
| |
| if (!buf_size) |
| return 0; |
| |
| struct v4l2_buffer v4l2_buffer; |
| struct v4l2_plane planes[VIDEO_MAX_PLANES]; |
| |
| memset(&v4l2_buffer, 0, sizeof(v4l2_buffer)); |
| v4l2_buffer.index = queue_index; |
| v4l2_buffer.type = OUTPUT_queue->type; |
| v4l2_buffer.memory = OUTPUT_queue->memory; |
| v4l2_buffer.length = 1; |
| v4l2_buffer.m.planes = planes; |
| v4l2_buffer.m.planes[0].length = buffers[queue_index].length[0]; |
| v4l2_buffer.m.planes[0].bytesused = buf_size; |
| v4l2_buffer.m.planes[0].data_offset = 0; |
| int ret = ioctl(OUTPUT_queue->v4lfd, VIDIOC_QBUF, &v4l2_buffer); |
| if (ret != 0) { |
| LOG_ERROR("OUTPUT queue: VIDIOC_QBUF failed: %s.", strerror(errno)); |
| return -1; |
| } |
| |
| return 0; |
| } |
| |
| int prime_OUTPUT(struct queue* OUTPUT_queue) { |
| int ret = 0; |
| |
| for (uint32_t i = 0; i < OUTPUT_queue->cnt; ++i) { |
| ret = submit_compressed_data(OUTPUT_queue, i); |
| if (ret || is_end_of_stream()) |
| break; |
| } |
| return ret; |
| } |
| |
| void cleanup_queue(struct queue* queue) { |
| if (queue->cnt) { |
| struct mmap_buffers* buffers = queue->buffers; |
| |
| for (uint32_t i = 0; i < queue->cnt; ++i) { |
| for (uint32_t j = 0; j < queue->num_planes; ++j) { |
| if (buffers[i].length[j]) |
| munmap(buffers[i].start[j], buffers[i].length[j]); |
| } |
| if (buffers[i].bo) |
| gbm_bo_destroy(buffers[i].bo); |
| } |
| |
| free(queue->buffers); |
| queue->cnt = 0; |
| } |
| } |
| |
| int queue_buffer_CAPTURE(struct queue* queue, uint32_t index) { |
| struct v4l2_buffer v4l2_buffer; |
| struct v4l2_plane planes[VIDEO_MAX_PLANES]; |
| memset(&v4l2_buffer, 0, sizeof v4l2_buffer); |
| memset(&planes, 0, sizeof planes); |
| |
| v4l2_buffer.type = queue->type; |
| v4l2_buffer.memory = queue->memory; |
| v4l2_buffer.index = index; |
| v4l2_buffer.m.planes = planes; |
| v4l2_buffer.length = queue->num_planes; |
| |
| struct gbm_bo* bo = queue->buffers[index].bo; |
| for (uint32_t i = 0; i < queue->num_planes; ++i) { |
| if (queue->memory == V4L2_MEMORY_DMABUF) { |
| v4l2_buffer.m.planes[i].m.fd = gbm_bo_get_fd_for_plane(bo, i); |
| assert(v4l2_buffer.m.planes[i].m.fd); |
| } else if (queue->memory == V4L2_MEMORY_MMAP) { |
| struct mmap_buffers* buffers = queue->buffers; |
| |
| v4l2_buffer.m.planes[i].length = buffers[index].length[i]; |
| v4l2_buffer.m.planes[i].bytesused = buffers[index].length[i]; |
| v4l2_buffer.m.planes[i].data_offset = 0; |
| } |
| } |
| |
| int ret = ioctl(queue->v4lfd, VIDIOC_QBUF, &v4l2_buffer); |
| if (ret != 0) |
| LOG_ERROR("CAPTURE queue: VIDIOC_QBUF failed: %s.", strerror(errno)); |
| |
| if (queue->memory == V4L2_MEMORY_DMABUF) { |
| for (uint32_t i = 0; i < queue->num_planes; ++i) { |
| int ret_close = close(v4l2_buffer.m.planes[i].m.fd); |
| if (ret_close != 0) { |
| LOG_ERROR("close failed with v4l2_buffer.m.planes[%d].m.fd: %s.", i, |
| strerror(errno)); |
| ret = ret_close; |
| } |
| } |
| } |
| |
| return ret; |
| } |
| |
| // This is the output queue that will produce uncompressed frames, see 4.5.1.6 |
| // Capture Setup |
| // https://www.kernel.org/doc/html/v5.4/media/uapi/v4l/dev-decoder.html#capture-setup |
| int setup_CAPTURE(struct gbm_device* gbm, |
| struct queue* CAPTURE_queue, |
| uint64_t modifier, |
| uint32_t coded_frame_rate, |
| bool use_CAPTURE_queue_dimensions) { |
| int ret = 0; |
| |
| // In stateful decoders, we insert some encoded chunks in the OUTPUT queue and |
| // then query (via VIDIOC_G_FMT) the associated CAPTURE queue to learn e.g. |
| // the decoded dimensions; this follows the process detailed in [1,2]. For the |
| // Image Processor (MDP) case, we don't want/need to queue frames first, |
| // because we know the dimensions from the decoding proper. So we simply set |
| // the format in the |CAPTURE_queue| first. |
| |
| // [1] https://www.kernel.org/doc/html/v5.4/media/uapi/v4l/dev-decoder.html#initialization |
| // [2] https://www.kernel.org/doc/html/v5.4/media/uapi/v4l/dev-decoder.html#capture-setup |
| if (use_CAPTURE_queue_dimensions) { |
| struct v4l2_format fmt; |
| memset(&fmt, 0, sizeof(fmt)); |
| fmt.type = CAPTURE_queue->type; |
| fmt.fmt.pix_mp.pixelformat = CAPTURE_queue->fourcc; |
| fmt.fmt.pix_mp.width = CAPTURE_queue->display_area.width; |
| fmt.fmt.pix_mp.height = CAPTURE_queue->display_area.height; |
| |
| ret = ioctl(CAPTURE_queue->v4lfd, VIDIOC_S_FMT, &fmt); |
| if (ret != 0) |
| LOG_ERROR("CAPTURE queue: VIDIOC_S_FMT failed: %s.", strerror(errno)); |
| } |
| |
| // 1. "Call VIDIOC_G_FMT() on the CAPTURE queue to get format for the |
| // destination buffers parsed/decoded from the bytestream."" |
| if (!ret) { |
| struct v4l2_format fmt; |
| memset(&fmt, 0, sizeof(fmt)); |
| |
| fmt.type = CAPTURE_queue->type; |
| |
| ret = ioctl(CAPTURE_queue->v4lfd, VIDIOC_G_FMT, &fmt); |
| if (ret != 0) |
| LOG_ERROR("CAPTURE queue: VIDIOC_G_FMT failed: %s.", strerror(errno)); |
| |
| // |CAPTURE_queue| might be configured for Q128, in that case we simply |
| // ignore it; we'll force |CAPTURE_queue->fourcc| further below. |
| assert(CAPTURE_queue->fourcc == fmt.fmt.pix_mp.pixelformat || |
| fmt.fmt.pix_mp.pixelformat == V4L2_PIX_FMT_NV12_UBWC); |
| |
| // Oftentimes the driver will give us back the appropriately aligned sizes. |
| // Pick them up here. |
| if (CAPTURE_queue->coded_width != fmt.fmt.pix_mp.width || |
| CAPTURE_queue->coded_height != fmt.fmt.pix_mp.height) { |
| LOG_DEBUG("CAPTURE queue provided adjusted coded dimensions: %dx%d -->" |
| "%dx%d",CAPTURE_queue->coded_width, CAPTURE_queue->coded_height, |
| fmt.fmt.pix_mp.width , fmt.fmt.pix_mp.height); |
| } |
| CAPTURE_queue->coded_width = fmt.fmt.pix_mp.width; |
| CAPTURE_queue->coded_height = fmt.fmt.pix_mp.height; |
| |
| if (CAPTURE_queue->num_planes != fmt.fmt.pix_mp.num_planes) { |
| LOG_DEBUG("CAPTURE queue provided adjusted num planes: %d --> %d", |
| CAPTURE_queue->num_planes, fmt.fmt.pix_mp.num_planes); |
| } |
| CAPTURE_queue->num_planes = fmt.fmt.pix_mp.num_planes; |
| |
| for (int i = 0; i < CAPTURE_queue->num_planes; ++i) { |
| CAPTURE_queue->bytes_per_line[i] = |
| fmt.fmt.pix_mp.plane_fmt[i].bytesperline; |
| } |
| |
| char fourcc[FOURCC_SIZE + 1]; |
| LOG_INFO("CAPTURE queue: width=%d, height=%d, format=%s, num_planes=%d", |
| CAPTURE_queue->coded_width, CAPTURE_queue->coded_height, |
| fourcc_to_string(CAPTURE_queue->fourcc, fourcc), |
| CAPTURE_queue->num_planes); |
| } |
| |
| // 2. Optional. Acquire the visible resolution via VIDIOC_G_SELECTION. |
| if (!ret) { |
| struct v4l2_selection selection; |
| memset(&selection, 0, sizeof(selection)); |
| selection.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; |
| selection.target = V4L2_SEL_TGT_COMPOSE; |
| |
| ret = ioctl(CAPTURE_queue->v4lfd, VIDIOC_G_SELECTION, &selection); |
| if (ret != 0) |
| LOG_ERROR("VIDIOC_G_SELECTION failed: %s.", strerror(errno)); |
| |
| if (CAPTURE_queue->display_area.left != selection.r.left || |
| CAPTURE_queue->display_area.top != selection.r.top || |
| CAPTURE_queue->display_area.width != selection.r.width || |
| CAPTURE_queue->display_area.height != selection.r.height) { |
| LOG_DEBUG("CAPTURE queue visible area changed from %dx%d+%d+%d to " |
| "%dx%d+%d+%d", |
| CAPTURE_queue->display_area.width, CAPTURE_queue->display_area.height, |
| CAPTURE_queue->display_area.left, CAPTURE_queue->display_area.top, |
| selection.r.width , selection.r.height, |
| selection.r.left , selection.r.top); |
| } |
| CAPTURE_queue->display_area = selection.r; |
| |
| LOG_INFO("DisplayLeft=%d, DisplayTop=%d, DisplayWidth=%d, DisplayHeight=%d", |
| selection.r.left, selection.r.top, |
| selection.r.width, selection.r.height); |
| } |
| |
| |
| // 4. "Optional. Set the CAPTURE format via VIDIOC_S_FMT() on the CAPTURE |
| // queue. The client may choose a different format than selected/suggested by |
| // the decoder in VIDIOC_G_FMT()." |
| if (!ret) { |
| struct v4l2_format fmt; |
| memset(&fmt, 0, sizeof(fmt)); |
| fmt.type = CAPTURE_queue->type; |
| fmt.fmt.pix_mp.pixelformat = CAPTURE_queue->fourcc; |
| |
| fmt.fmt.pix_mp.width = CAPTURE_queue->coded_width; |
| fmt.fmt.pix_mp.height = CAPTURE_queue->coded_height; |
| |
| ret = ioctl(CAPTURE_queue->v4lfd, VIDIOC_S_FMT, &fmt); |
| if (ret != 0) |
| LOG_ERROR("CAPTURE queue: VIDIOC_S_FMT failed: %s.", strerror(errno)); |
| } |
| |
| // 5. (Optional) Sets the coded frame interval on the CAPTURE queue via |
| // VIDIOC_S_PARM. Support for this feature is signaled by the |
| // V4L2_FMT_FLAG_ENC_CAP_FRAME_INTERVAL format flag. |
| if (!ret && coded_frame_rate != kInvalidFrameRate) { |
| struct v4l2_streamparm parms; |
| memset(&parms, 0, sizeof(parms)); |
| parms.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; |
| |
| // TODO(b/202758590): Handle non-integral coded_frame_rate |
| parms.parm.output.timeperframe.numerator = 1; |
| parms.parm.output.timeperframe.denominator = coded_frame_rate; |
| |
| LOG_INFO("Time per frame set to %d/%d seconds", |
| parms.parm.output.timeperframe.numerator, |
| parms.parm.output.timeperframe.denominator); |
| |
| ret = ioctl(CAPTURE_queue->v4lfd, VIDIOC_S_PARM, &parms); |
| if (ret != 0) |
| LOG_ERROR("CAPTURE queue: VIDIOC_S_PARM failed: %s.", strerror(errno)); |
| } |
| |
| // 10. Allocates CAPTURE buffers via VIDIOC_REQBUFS() on the CAPTURE queue. |
| if (!ret) { |
| struct v4l2_requestbuffers reqbuf; |
| memset(&reqbuf, 0, sizeof(reqbuf)); |
| reqbuf.count = kRequestBufferCount; |
| reqbuf.type = CAPTURE_queue->type; |
| reqbuf.memory = CAPTURE_queue->memory; |
| |
| ret = ioctl(CAPTURE_queue->v4lfd, VIDIOC_REQBUFS, &reqbuf); |
| if (ret != 0) |
| LOG_ERROR("CAPTURE queue: VIDIOC_REQBUFS failed: %s.", strerror(errno)); |
| |
| char fourcc[FOURCC_SIZE + 1]; |
| LOG_INFO("CAPTURE queue: %d buffers requested, %d buffers for decoded data " |
| " (%s) returned.", kRequestBufferCount, reqbuf.count, |
| fourcc_to_string(CAPTURE_queue->fourcc, fourcc)); |
| |
| if (CAPTURE_queue->memory == V4L2_MEMORY_DMABUF) |
| ret = request_dmabuf_buffers(gbm, CAPTURE_queue, modifier, reqbuf.count); |
| else if (CAPTURE_queue->memory == V4L2_MEMORY_MMAP) |
| ret = request_mmap_buffers(CAPTURE_queue, &reqbuf); |
| else |
| ret = -1; |
| |
| if (!ret) { |
| for (uint32_t i = 0; i < reqbuf.count; ++i) { |
| ret = queue_buffer_CAPTURE(CAPTURE_queue, i); |
| if (ret != 0) |
| break; |
| } |
| } |
| } |
| |
| // 11. Calls VIDIOC_STREAMON() on the CAPTURE queue to start decoding frames. |
| if (!ret) { |
| ret = ioctl(CAPTURE_queue->v4lfd, VIDIOC_STREAMON, &CAPTURE_queue->type); |
| if (ret != 0) |
| LOG_ERROR("VIDIOC_STREAMON failed: %s.", strerror(errno)); |
| else |
| CAPTURE_queue->is_streaming = true; |
| } |
| |
| return ret; |
| } |
| |
| // Writes a single frame to disk. The file path is based on the input file name |
| // and the frame characteristics. For example: |
| // Input file: /path/file.ivf |
| // Frame number: 12 |
| // Image width: 800 |
| // Image height: 450 |
| // This results in a file written to: /path/file_frame_0012_800x450_I420.yuv |
| void write_frame_to_disk(const char* output_file_prefix, |
| struct queue* CAPTURE_queue, |
| uint32_t queue_index) { |
| // TODO(b/204566257) doesn't support writing to a single file |
| #define FILE_NAME_FORMAT "%s_frame_%04d_%dx%d_I420.yuv" |
| // Update FILE_NAME_FORMAT if this assert is triggered. |
| assert(CAPTURE_queue->displayed_frames <= 9999); |
| const int sz = snprintf(NULL, 0, FILE_NAME_FORMAT, output_file_prefix, |
| CAPTURE_queue->displayed_frames, |
| CAPTURE_queue->display_area.width, |
| CAPTURE_queue->display_area.height); |
| char file_name[sz + 1]; // note +1 for terminating null byte |
| snprintf(file_name, sizeof file_name, FILE_NAME_FORMAT, output_file_prefix, |
| CAPTURE_queue->displayed_frames, CAPTURE_queue->display_area.width, |
| CAPTURE_queue->display_area.height); |
| #undef FILE_NAME_FORMAT |
| |
| FILE* fp = fopen(file_name, "wb"); |
| |
| if (!fp) |
| LOG_ERROR("Unable to open output yuv file: %s.", file_name); |
| |
| if (CAPTURE_queue->fourcc == V4L2_PIX_FMT_YUV420M) { |
| // Technically this path could also support V4L2_PIX_FMT_YVU420 and other |
| // I420 variations, but it's not properly tested/commonly utilized. |
| const size_t num_coded_pixels = CAPTURE_queue->coded_width * |
| CAPTURE_queue->coded_height; |
| uint8_t* y_buffer = CAPTURE_queue->buffers[queue_index].start[0]; |
| fwrite(y_buffer, num_coded_pixels, 1, fp); |
| |
| for (uint32_t i = 1; i < CAPTURE_queue->num_planes; ++i) { |
| const size_t uv_plane_size = ((CAPTURE_queue->coded_width + 1) / 2) * |
| ((CAPTURE_queue->coded_height + 1) / 2); |
| uint8_t* chroma_buffer = CAPTURE_queue->buffers[queue_index].start[i]; |
| fwrite(chroma_buffer, uv_plane_size, 1, fp); |
| } |
| } else { |
| // TODO(b/204566257) doesn't support writing full decoded buffers |
| struct data_buffer buffer_i420 = |
| map_buffers_and_convert_to_i420(CAPTURE_queue, queue_index); |
| |
| fwrite(buffer_i420.data, buffer_i420.size, 1, fp); |
| } |
| |
| fclose(fp); |
| } |
| |
| int dequeue_buffer(struct queue* queue, |
| uint32_t* index, |
| uint32_t* bytesused, |
| uint32_t* flags) { |
| struct v4l2_buffer v4l2_buffer; |
| struct v4l2_plane planes[VIDEO_MAX_PLANES] = {0}; |
| memset(&v4l2_buffer, 0, sizeof(v4l2_buffer)); |
| v4l2_buffer.type = queue->type; |
| v4l2_buffer.memory = queue->memory; |
| // "Applications call the VIDIOC_DQBUF ioctl to dequeue [...]. They just |
| // set the type, memory and reserved fields of a struct v4l2_buffer as |
| // above, when VIDIOC_DQBUF is called with a pointer to this structure the |
| // driver fills the remaining fields or returns an error code." |
| // https://www.kernel.org/doc/html/v4.19/media/uapi/v4l/vidioc-qbuf.html |
| // Mediatek 8173 however ,needs the |length| field filed in. |
| v4l2_buffer.length = queue->num_planes; |
| |
| v4l2_buffer.m.planes = planes; |
| v4l2_buffer.m.planes[0].bytesused = 0; |
| int ret = ioctl(queue->v4lfd, VIDIOC_DQBUF, &v4l2_buffer); |
| |
| if (index) |
| *index = v4l2_buffer.index; |
| if (bytesused) |
| *bytesused = v4l2_buffer.m.planes[0].bytesused; |
| if (flags) |
| *flags = v4l2_buffer.flags; |
| |
| return ret; |
| } |
| |
| // 4.5.1.10. Drain |
| // https://www.kernel.org/doc/html/v5.4/media/uapi/v4l/dev-decoder.html#drain |
| // initiate_drain should be called when the source ends or when the requested |
| // number of frames has been met. |
| int initiate_drain(struct queue* OUTPUT_queue) { |
| // 1. Begin the drain sequence by issuing VIDIOC_DECODER_CMD(). |
| struct v4l2_decoder_cmd cmd; |
| memset(&cmd, 0, sizeof(cmd)); |
| cmd.cmd = V4L2_DEC_CMD_STOP; |
| |
| // V4L2_DEC_CMD_STOP may not be supported, but we haven't run into |
| // a driver that doesn't support V4L2_DEC_CMD_STOP cmd. |
| int ret = ioctl(OUTPUT_queue->v4lfd, VIDIOC_DECODER_CMD, &cmd); |
| if (!ret) |
| OUTPUT_queue->is_streaming = false; |
| |
| // 2. The decode loop needs to proceed normally as long as the CAPTURE |
| // queue has buffers that can be dequeued. |
| // This step occurs in the main |decode| loop. |
| return ret; |
| } |
| |
| int finish_drain(struct queue* OUTPUT_queue, struct queue* CAPTURE_queue) { |
| // Steps 1 and 2 in the drain sequence have already occurred when this is |
| // called. |
| int ret = 0; |
| |
| // 3. Reset by issuing VIDIOC_STREAMOFF |
| int ret_streamoff = |
| ioctl(OUTPUT_queue->v4lfd, VIDIOC_STREAMOFF, &OUTPUT_queue->type); |
| if (ret_streamoff != 0) { |
| LOG_ERROR("VIDIOC_STREAMOFF failed on OUTPUT: %s", strerror(errno)); |
| ret = ret_streamoff; |
| } |
| |
| ret_streamoff = |
| ioctl(CAPTURE_queue->v4lfd, VIDIOC_STREAMOFF, &CAPTURE_queue->type); |
| if (ret_streamoff != 0) { |
| LOG_ERROR("VIDIOC_STREAMOFF failed on CAPTURE: %s", strerror(errno)); |
| ret = ret_streamoff; |
| } |
| |
| return ret; |
| } |
| |
| typedef bool (*process_opaque_frame_function)(struct queue*, |
| uint32_t, |
| struct queue*, |
| struct queue*, |
| int*); |
| |
| |
| // This function gets a single opaque frame from |CAPTURE_queue| and enqueues it |
| // in the |mdp_OUTPUT_queue| to be processed by the MDP/Image Processor; it then |
| // waits for it to produce a single "clear" frame in |mdp_CAPTURE_queue|. |
| bool process_opaque_frame(struct queue* CAPTURE_queue, |
| uint32_t CAPTURE_queue_index, |
| struct queue* mdp_OUTPUT_queue, |
| struct queue* mdp_CAPTURE_queue, |
| int* mdp_CAPTURE_queue_dequeued_buffer_index) |
| { |
| assert(CAPTURE_queue->memory == V4L2_MEMORY_MMAP); |
| assert(CAPTURE_queue->fourcc == mdp_OUTPUT_queue->fourcc); |
| assert(mdp_OUTPUT_queue->memory == V4L2_MEMORY_DMABUF); |
| |
| // Insert the opaque buffer from the |CAPTURE_queue| into the Image Processor |
| // via the |mdp_OUTPUT_queue|. |CAPTURE_queue| and |mdp_OUTPUT_queue| are |
| // supposed to be of type V4L2_MEMORY_MMAP and V4L2_MEMORY_DMABUF, resp. To |
| // avoid a copy, we extract the DmaBufs under |CAPTURE_queue| using |
| // VIDIOC_EXPBUF and "import" them into |mdp_OUTPUT_queue|. |
| { |
| struct v4l2_buffer v4l2_buffer; |
| memset(&v4l2_buffer, 0, sizeof(v4l2_buffer)); |
| |
| // Re-use the CAPTURE_queue index. This is for simplicity in this file: |
| // every time we dequeue an opaque frame from |CAPTURE_queue| we enqueue it |
| // in |mdp_OUTPUT_queue|. |
| v4l2_buffer.index = CAPTURE_queue_index; |
| v4l2_buffer.type = mdp_OUTPUT_queue->type; |
| v4l2_buffer.bytesused = 0; // "unused (set to 0) for multiplanar buffers" |
| v4l2_buffer.flags = V4L2_BUF_FLAG_QUEUED | V4L2_BUF_FLAG_TIMESTAMP_COPY; |
| v4l2_buffer.memory = mdp_OUTPUT_queue->memory; |
| // |length|:"number of elements in the planes array for multi-plane buffers" |
| v4l2_buffer.length = mdp_OUTPUT_queue->num_planes; |
| // Note: although later specs mandated that |v4l2_buffer.timestamp| had to |
| // be filled in, it doesn't seem necessary in older kernels. |
| struct v4l2_plane planes[VIDEO_MAX_PLANES] = { 0 }; |
| v4l2_buffer.m.planes = planes; |
| |
| for (int i = 0; i < mdp_OUTPUT_queue->num_planes; ++i) { |
| // Export the DmaBuf out of the |CAPTURE_queue| |queue_index| and use it |
| // for |v4l2_buffer|. |
| struct v4l2_exportbuffer expbuf; |
| memset(&expbuf, 0, sizeof(expbuf)); |
| expbuf.type = CAPTURE_queue->type; |
| expbuf.index = CAPTURE_queue_index; |
| expbuf.plane = i; |
| const int ret = ioctl(CAPTURE_queue->v4lfd, VIDIOC_EXPBUF, &expbuf); |
| if (ret != 0) { |
| LOG_ERROR("Image Processor: mdp_OUTPUT_queue VIDIOC_EXPBUF failed: %s.", |
| strerror(errno)); |
| return false; |
| } |
| |
| v4l2_buffer.m.planes[i].m.fd = expbuf.fd; |
| |
| v4l2_buffer.m.planes[i].length = |
| CAPTURE_queue->buffers[CAPTURE_queue_index].length[i]; |
| v4l2_buffer.m.planes[i].bytesused = |
| CAPTURE_queue->buffers[CAPTURE_queue_index].length[i]; |
| assert(v4l2_buffer.m.planes[i].data_offset == 0); |
| } |
| |
| const int ret = ioctl(mdp_OUTPUT_queue->v4lfd, VIDIOC_QBUF, &v4l2_buffer); |
| |
| // VIDIOC_EXPBUF opens file descriptors that need to be closed. |
| for (int i = 0; i < mdp_OUTPUT_queue->num_planes; ++i) { |
| close(v4l2_buffer.m.planes[i].m.fd); |
| } |
| |
| if (ret != 0) { |
| LOG_ERROR("Image Processor: mdp_OUTPUT_queue VIDIOC_QBUF failed: %s.", |
| strerror(errno)); |
| return false; |
| } else { |
| LOG_DEBUG("Image Processor: enqueued frame in mdp_OUTPUT_queue with index" |
| " %d.", CAPTURE_queue_index); |
| } |
| } |
| |
| // Try to dequeue a "clear" buffer from |mdp_CAPTURE_queue|. We try as long as |
| // VIDIOC_DQBUF returns EAGAIN, indicating that the device is busy (hopefully |
| // converting our opaque frames). If a buffer is dequeued, we reenqueue it |
| // immediately (note that it won't be reused until a couple of calls later |
| // since the indexes are used in round-robin fashion). |
| { |
| struct v4l2_buffer v4l2_buffer; |
| memset(&v4l2_buffer, 0, sizeof(v4l2_buffer)); |
| v4l2_buffer.type = mdp_CAPTURE_queue->type; |
| v4l2_buffer.memory = mdp_CAPTURE_queue->memory; |
| struct v4l2_plane planes[VIDEO_MAX_PLANES] = { 0 }; |
| v4l2_buffer.m.planes = planes; |
| |
| v4l2_buffer.length = mdp_CAPTURE_queue->num_planes; |
| |
| int num_tries = kMaxRetryCount; |
| bool keep_trying = false; |
| do { |
| const int ret = |
| ioctl(mdp_CAPTURE_queue->v4lfd, VIDIOC_DQBUF, &v4l2_buffer); |
| if (ret != 0 && errno != EAGAIN) { |
| LOG_ERROR("Image Processor: mdp_CAPTURE_queue VIDIOC_DQBUF failed."); |
| return false; |
| } |
| keep_trying = (ret != 0 && errno == EAGAIN); |
| num_tries--; |
| } while(keep_trying && num_tries != 0); |
| |
| if (num_tries == 0) { |
| LOG_ERROR("Image Processor: timed out waiting for a clear frame."); |
| return false; |
| } |
| |
| LOG_DEBUG("Image Processor: dequeued frame from mdp_CAPTURE_queue with " |
| "index %d.", v4l2_buffer.index); |
| *mdp_CAPTURE_queue_dequeued_buffer_index = v4l2_buffer.index; |
| |
| const int ret = |
| ioctl(mdp_CAPTURE_queue->v4lfd, VIDIOC_QBUF, &v4l2_buffer); |
| if (ret != 0) { |
| LOG_ERROR("Image Processor: mdp_CAPTURE_queue VIDIOC_QBUF failed: %s.", |
| strerror(errno)); |
| return false; |
| } else { |
| LOG_DEBUG("Image Processor: (re)enqueued frame in mdp_CAPTURE_queue " |
| "with index %d.", v4l2_buffer.index); |
| } |
| } |
| |
| // If we're here, we have dequeued a "clear" buffer from |mdp_CAPTURE_queue|; |
| // dequeue a buffer from |mdp_OUTPUT_queue|: the MDP has finished processing |
| // one and we can reuse that slot in future function calls. |
| { |
| uint32_t index = 0; |
| const int ret = dequeue_buffer(mdp_OUTPUT_queue, &index, /*bytesused=*/NULL, |
| /*flags=*/NULL); |
| if (ret != 0) { |
| LOG_ERROR("Image Processor: mdp_OUTPUT_queue VIDIOC_DQBUF failed: %s.", |
| strerror(errno)); |
| return false; |
| } else { |
| LOG_DEBUG("Image Processor: (re)dequeued frame in mdp_OUTPUT_queue with " |
| "index %d.", index); |
| } |
| } |
| |
| return true; |
| } |
| |
| // From 4.5.1.9. Dynamic Resolution Change |
| // This is called after a source change event is caught, dequeued, |
| // and all pending buffers in the |CAPTURE_queue| are drained. |
| int handle_dynamic_resolution_change(struct gbm_device* gbm, |
| struct queue* CAPTURE_queue, |
| uint64_t modifier) { |
| // Calls VIDIOC_STREAMOFF to stop the CAPTURE queue stream. |
| int ret = ioctl(CAPTURE_queue->v4lfd, VIDIOC_STREAMOFF, &CAPTURE_queue->type); |
| if (ret != 0) |
| LOG_ERROR("VIDIOC_STREAMOFF failed: %s.", strerror(errno)); |
| |
| // Deallocates |CAPTURE_queue| buffers via VIDIOC_REQBUFS. Calling |
| // VIDIOC_REQBUFS with |count == 0| tells the device to deallocate the |
| // existing buffers. |
| if (!ret) { |
| cleanup_queue(CAPTURE_queue); |
| |
| struct v4l2_requestbuffers reqbuf = {.count = 0, |
| .type = CAPTURE_queue->type, |
| .memory = CAPTURE_queue->memory}; |
| |
| ret = ioctl(CAPTURE_queue->v4lfd, VIDIOC_REQBUFS, &reqbuf); |
| if (ret != 0) { |
| LOG_ERROR("CAPTURE queue: VIDIOC_REQBUFS failed while deallocating " |
| "buffers: %s.", strerror(errno)); |
| } |
| } |
| |
| // setup_CAPTURE will call VIDIOC_STREAMON as part of the capture setup |
| // sequence, which starts capture queue streaming. |
| if (!ret) { |
| ret = setup_CAPTURE(gbm, CAPTURE_queue, modifier, kInvalidFrameRate, |
| /*use_CAPTURE_queue_dimensions=*/false); |
| } |
| |
| return ret; |
| } |
| |
| int decode(struct gbm_device* gbm, |
| struct queue* CAPTURE_queue, |
| struct queue* OUTPUT_queue, |
| uint64_t modifier, |
| const char* output_file_prefix, |
| uint32_t frames_to_decode, |
| bool print_md5hash, |
| process_opaque_frame_function process_opaque_frame_fp, |
| struct queue* mdp_OUTPUT_queue, |
| struct queue* mdp_CAPTURE_queue) { |
| int ret = 0; |
| |
| // If no buffers have been dequeued for more than |kMaxRetryCount| retries, we |
| // should exit the program. Something is wrong in the decoder or with how we |
| // are controlling it. |
| int num_tries = kMaxRetryCount; |
| |
| // TODO(nhebert) See if it is possible to have two events dequeue before |
| // the first is handled. Assume "no" for now. |
| bool pending_dynamic_resolution_change = false; |
| |
| while (ret == 0 && CAPTURE_queue->is_streaming && num_tries != 0) { |
| { |
| uint32_t event_type = 0; |
| if (!dequeue_event(CAPTURE_queue, &event_type) && |
| event_type == V4L2_EVENT_SOURCE_CHANGE) { |
| LOG_INFO("CAPTURE queue experienced a source change."); |
| pending_dynamic_resolution_change = true; |
| } |
| |
| uint32_t index = 0; |
| uint32_t flags = 0; |
| uint32_t bytesused = 0; |
| const int ret_dequeue = dequeue_buffer(CAPTURE_queue, &index, &bytesused, |
| &flags); |
| if (ret_dequeue != 0) { |
| if (errno != EAGAIN) { |
| LOG_ERROR("VIDIOC_DQBUF failed for CAPTURE queue: %s.", |
| strerror(errno)); |
| ret = ret_dequeue; |
| break; |
| } |
| num_tries--; |
| continue; |
| } |
| // Successfully dequeued a buffer. Reset the |num_tries| counter. |
| num_tries = kMaxRetryCount; |
| |
| const bool is_flag_error_set = (flags & V4L2_BUF_FLAG_ERROR) != 0; |
| const bool is_flag_last_set = (flags & V4L2_BUF_FLAG_LAST) != 0; |
| if (is_flag_last_set) |
| CAPTURE_queue->is_streaming = false; |
| |
| // Don't use a buffer flagged with V4L2_BUF_FLAG_ERROR regardless of |
| // |bytesused| or with V4L2_BUF_FLAG_LAST and |bytesused| == 0 |
| const bool ignore_buffer = is_flag_error_set || |
| (is_flag_last_set && (bytesused == 0)); |
| if (!ignore_buffer && |
| CAPTURE_queue->displayed_frames < frames_to_decode) { |
| CAPTURE_queue->displayed_frames++; |
| |
| if (process_opaque_frame_fp) { |
| int mdp_CAPTURE_queue_buffer_index = kInvalidBufferIndex; |
| assert(V4L2_MEMORY_MMAP == CAPTURE_queue->memory); |
| if (!process_opaque_frame_fp(CAPTURE_queue, |
| index, |
| mdp_OUTPUT_queue, |
| mdp_CAPTURE_queue, |
| &mdp_CAPTURE_queue_buffer_index)){ |
| LOG_ERROR("Image Processor failed."); |
| return -1; |
| } |
| mdp_CAPTURE_queue->displayed_frames++; |
| if (output_file_prefix && |
| mdp_CAPTURE_queue_buffer_index != kInvalidBufferIndex) { |
| write_frame_to_disk(output_file_prefix, mdp_CAPTURE_queue, |
| mdp_CAPTURE_queue_buffer_index); |
| } |
| |
| if (print_md5hash && |
| mdp_CAPTURE_queue_buffer_index != kInvalidBufferIndex) { |
| compute_and_print_md5hash( |
| mdp_CAPTURE_queue->buffers[mdp_CAPTURE_queue_buffer_index] |
| .start[0], |
| mdp_CAPTURE_queue->buffers[mdp_CAPTURE_queue_buffer_index] |
| .length[0], |
| CAPTURE_queue->displayed_frames); |
| } |
| } else { |
| if (output_file_prefix) |
| write_frame_to_disk(output_file_prefix, CAPTURE_queue, index); |
| |
| if (print_md5hash) |
| map_buffers_and_calculate_md5hash(CAPTURE_queue, index); |
| } |
| } else { |
| LOG_DEBUG("Buffer set %s%s%s.", |
| is_flag_error_set ? "V4L2_BUF_FLAG_ERROR" : "", |
| (is_flag_error_set && is_flag_last_set) ? " and " : "", |
| is_flag_last_set ? "V4L2_BUF_FLAG_LAST" : ""); |
| } |
| |
| // When the device has decoded |frames_to_decode| displayable frames, |
| // start the drain sequence. |
| if (!ret && OUTPUT_queue->is_streaming && |
| CAPTURE_queue->displayed_frames >= frames_to_decode) { |
| ret = initiate_drain(OUTPUT_queue); |
| } |
| |
| // Done with buffer, queue it back up unless we got V4L2_BUF_FLAG_LAST |
| if (!ret && !is_flag_last_set) |
| ret = queue_buffer_CAPTURE(CAPTURE_queue, index); |
| |
| if (!ret && pending_dynamic_resolution_change && is_flag_last_set) { |
| LOG_DEBUG("Handling dynamic resolution change."); |
| pending_dynamic_resolution_change = false; |
| ret = handle_dynamic_resolution_change(gbm, CAPTURE_queue, modifier); |
| } |
| } |
| |
| // A frame was recieved on the CAPTURE queue, that means there should |
| // now be a free OUTPUT buffer. |
| if (!ret) { |
| uint32_t index = 0; |
| const int ret_dequeue = dequeue_buffer(OUTPUT_queue, &index, NULL, |
| NULL); |
| if (ret_dequeue != 0) { |
| if (errno != EAGAIN) { |
| LOG_ERROR("VIDIOC_DQBUF failed for OUTPUT queue: %s.", |
| strerror(errno)); |
| ret = ret_dequeue; |
| break; |
| } |
| continue; |
| } |
| |
| if (OUTPUT_queue->is_streaming) { |
| ret = submit_compressed_data(OUTPUT_queue, index); |
| |
| // If there are no remaining frames, we should stop the OUTPUT queue. |
| // Doing so, lets the decoder know it should process the remaining |
| // buffers in the output queue, then dequeue a buffer with |
| // V4L2_BUF_FLAG_LAST set. After that happens, the decode loop will |
| // stop. |
| if (!ret && is_end_of_stream()) |
| ret = initiate_drain(OUTPUT_queue); |
| } |
| } |
| } |
| |
| if (num_tries == 0) { |
| LOG_FATAL("Decoder appeared to stall after decoding %d frames.", |
| CAPTURE_queue->displayed_frames); |
| } |
| |
| finish_drain(OUTPUT_queue, CAPTURE_queue); |
| |
| LOG_INFO("%d displayable frames decoded.", CAPTURE_queue->displayed_frames); |
| |
| return ret; |
| } |
| |
| static void print_help(const char* argv0) { |
| printf("usage: %s [OPTIONS]\n", argv0); |
| printf(" -f, --file ivf file to decode\n"); |
| printf(" -w, --write write visible frames in I420 format\n"); |
| printf(" -m, --max max number of visible frames to decode\n"); |
| printf(" -a, --mmap use mmap instead of dmabuf\n"); |
| printf(" -c, --capture_fmt fourcc of the CAPTURE queue, i.e. the " |
| "decoded video format\n"); |
| printf(" -l, --log_level specifies log level, 0:debug 1:info 2:error " |
| "3:fatal (default: %d)\n", DEFAULT_LOG_LEVEL); |
| printf(" -r, --frame_rate (optional) specify a frame rate (Hz)\n"); |
| printf(" -d, --md5 compute md5 hash for each decoded visible frame " |
| "in I420 format\n"); |
| } |
| |
| static const struct option longopts[] = { |
| {"file", required_argument, NULL, 'f'}, |
| {"write", no_argument, NULL, 'w'}, |
| {"max", required_argument, NULL, 'm'}, |
| {"mmap", no_argument, NULL, 'a'}, |
| {"capture_fmt", required_argument, NULL, 'c'}, |
| {"log_level", required_argument, NULL, 'l'}, |
| {"md5", no_argument, NULL, 'd'}, |
| {"frame_rate", required_argument, NULL, 'r'}, |
| {0, 0, 0, 0}, |
| }; |
| |
| int main(int argc, char* argv[]) { |
| int c; |
| char* file_name = NULL; |
| bool write_out = false; |
| bool print_md5hash = false; |
| uint32_t frames_to_decode = UINT_MAX; |
| uint32_t frame_rate = kInvalidFrameRate; |
| uint64_t modifier = DRM_FORMAT_MOD_LINEAR; |
| uint32_t uncompressed_fourcc = V4L2_PIX_FMT_INVALID; |
| enum v4l2_memory CAPTURE_memory = V4L2_MEMORY_DMABUF; |
| |
| while ((c = getopt_long(argc, argv, "f:m:wac:l:dr:", longopts, NULL)) != -1) { |
| switch (c) { |
| case 'f': |
| file_name = strdup(optarg); |
| break; |
| case 'm': |
| frames_to_decode = atoi(optarg); |
| break; |
| case 'w': |
| write_out = true; |
| break; |
| case 'a': |
| CAPTURE_memory = V4L2_MEMORY_MMAP; |
| break; |
| case 'c': |
| if (strlen(optarg) == 4) { |
| uncompressed_fourcc = |
| v4l2_fourcc(toupper(optarg[0]), toupper(optarg[1]), |
| toupper(optarg[2]), toupper(optarg[3])); |
| |
| char fourcc[FOURCC_SIZE + 1]; |
| LOG_INFO("User-provided CAPTURE format: %s", |
| fourcc_to_string(uncompressed_fourcc, fourcc)); |
| |
| if (uncompressed_fourcc == V4L2_PIX_FMT_NV12_UBWC) { |
| modifier = DRM_FORMAT_MOD_QCOM_COMPRESSED; |
| LOG_INFO("CAPTURE format is compressed, setting modifier."); |
| } |
| } |
| break; |
| case 'l': { |
| const int specified_log_run_level = atoi(optarg); |
| if (specified_log_run_level >= kLoggingLevelMax) { |
| LOG_INFO("Undefined log level %d, using default log level instead.", |
| specified_log_run_level); |
| } else { |
| log_run_level = specified_log_run_level; |
| } |
| break; |
| } |
| case 'd': |
| print_md5hash = true; |
| break; |
| case 'r': |
| frame_rate = atoi(optarg); |
| if (frame_rate == kInvalidFrameRate) |
| { |
| LOG_FATAL("Invalid frame rate provided - %s. --frame_rate requires " |
| "positive integer less than 2^32.", optarg); |
| } |
| break; |
| default: |
| break; |
| } |
| } |
| |
| LOG_INFO("Simple v4l2 decode."); |
| |
| if (frames_to_decode != UINT_MAX) |
| LOG_INFO("Only decoding a max of %d frames.", frames_to_decode); |
| |
| if (!file_name) { |
| print_help(argv[0]); |
| exit(1); |
| } |
| |
| int drm_device_fd = bs_drm_open_main_display(); |
| if (drm_device_fd < 0) { |
| LOG_FATAL("Failed to open card for display."); |
| } |
| |
| struct gbm_device* gbm = gbm_create_device(drm_device_fd); |
| if (!gbm) { |
| close(drm_device_fd); |
| LOG_FATAL("Failed to create gbm device."); |
| } |
| |
| init_bitstream(file_name); |
| |
| char* output_file_prefix = NULL; |
| if (write_out) { |
| const size_t len_prefix = strrchr(file_name, '.') - file_name; |
| output_file_prefix = strndup(file_name, len_prefix); |
| } |
| |
| int v4lfd = open(kDecodeDevice, O_RDWR | O_NONBLOCK | O_CLOEXEC); |
| if (v4lfd < 0) |
| LOG_FATAL("Unable to open device file: %s.", kDecodeDevice); |
| query_driver(v4lfd); |
| |
| if (!capabilities(v4lfd, kDecodeDevice, get_fourcc(), &uncompressed_fourcc)) |
| LOG_FATAL("Not enough capabilities present for decoding."); |
| assert(uncompressed_fourcc != V4L2_PIX_FMT_INVALID); |
| |
| struct queue OUTPUT_queue = {.v4lfd = v4lfd, |
| .type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE, |
| .fourcc = get_fourcc(), |
| .num_planes = 1, |
| .memory = V4L2_MEMORY_MMAP, |
| .displayed_frames = 0, |
| .is_streaming = false}; |
| int ret = setup_OUTPUT(&OUTPUT_queue, /*optional_width=*/NULL, |
| /*optional_height=*/NULL); |
| |
| if (!ret) |
| ret = prime_OUTPUT(&OUTPUT_queue); |
| |
| struct queue CAPTURE_queue = {.v4lfd = v4lfd, |
| .type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE, |
| .fourcc = uncompressed_fourcc, |
| .num_planes = 1, |
| .memory = CAPTURE_memory, |
| .displayed_frames = 0, |
| .is_streaming = false}; |
| if (!ret) { |
| ret = setup_CAPTURE(gbm, &CAPTURE_queue, modifier, frame_rate, |
| /*use_CAPTURE_queue_dimensions=*/false); |
| } |
| |
| process_opaque_frame_function process_opaque_frame_fp = NULL; |
| int mdp_fd = -1; |
| struct queue* mdp_OUTPUT_queue = NULL; |
| struct queue* mdp_CAPTURE_queue = NULL; |
| if (!ret && uncompressed_fourcc == V4L2_PIX_FMT_MT21C) { |
| char fourcc[FOURCC_SIZE + 1]; |
| fourcc_to_string(uncompressed_fourcc, fourcc); |
| LOG_INFO("Image Processor: needed, %s is an opaque format.", fourcc); |
| // MT21C is a specific opaque MediaTek format and seems, sadly, |
| // undocumented, forcing the use of a specific ImageProcessor, called by |
| // MediaTek Media Data Path (MDP) to produce a "clear" format. This format |
| // was controversial: |
| // https://groups.google.com/g/linux.kernel/c/4nijzmKfdak/m/pWUN7HQBBAAJ |
| mdp_fd = open(kImageProcessorDevice, O_RDWR | O_NONBLOCK | O_CLOEXEC); |
| if (mdp_fd < 0) { |
| LOG_FATAL("Image Processor: Unable to open dev file: %s.", |
| kImageProcessorDevice); |
| } |
| query_driver(mdp_fd); |
| |
| // MDP claims to support (from v4l2-ctl): |
| // [0]: 'NM12' (Y/CbCr 4:2:0 (N-C)) |
| // [1]: 'YM12' (Planar YUV 4:2:0 (N-C)) |
| // [2]: 'YV12' (Planar YVU 4:2:0) |
| // Chrome uses V4L2_PIX_FMT_YVU420 (YV12). |
| uint32_t readable_fourcc = V4L2_PIX_FMT_YUV420M ; |
| const uint32_t readable_fourcc_num_planes = 3; |
| if (!capabilities(mdp_fd, kImageProcessorDevice, uncompressed_fourcc, |
| &readable_fourcc)) { |
| LOG_FATAL("Image processor: Expected formats not supported."); |
| } |
| assert(readable_fourcc == V4L2_PIX_FMT_YUV420M); |
| |
| char mdp_fourcc[FOURCC_SIZE + 1]; |
| fourcc_to_string(readable_fourcc, mdp_fourcc); |
| LOG_INFO("Image Processor: OUTPUT format %s --> CAPTURE format: %s", |
| fourcc, mdp_fourcc); |
| |
| // |mdp_OUTPUT_queue| takes decoded, opaque frames. |
| mdp_OUTPUT_queue = malloc(sizeof(struct queue)); |
| mdp_OUTPUT_queue->v4lfd = mdp_fd; |
| mdp_OUTPUT_queue->type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; |
| mdp_OUTPUT_queue->fourcc = uncompressed_fourcc; |
| mdp_OUTPUT_queue->num_planes = 2; |
| mdp_OUTPUT_queue->memory = V4L2_MEMORY_DMABUF; |
| mdp_OUTPUT_queue->displayed_frames = 0; |
| mdp_OUTPUT_queue->is_streaming = false; |
| int ret = setup_OUTPUT(mdp_OUTPUT_queue, &CAPTURE_queue.coded_width, |
| &CAPTURE_queue.coded_height); |
| if (ret < 0) |
| LOG_FATAL("Image Processor: Unable to configure OUTPUT_queue."); |
| |
| if (!apply_selection_to_queue(mdp_OUTPUT_queue, CAPTURE_queue.display_area)) |
| LOG_FATAL("Image Processor: Unable to set SELECTION for OUTPUT_queue."); |
| |
| // |mdp_CAPTURE_queue| has "clear" and fully mappable and readable frames. |
| mdp_CAPTURE_queue = malloc(sizeof(struct queue)); |
| mdp_CAPTURE_queue->v4lfd = mdp_fd; |
| mdp_CAPTURE_queue->type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; |
| mdp_CAPTURE_queue->fourcc = readable_fourcc; |
| mdp_CAPTURE_queue->num_planes = readable_fourcc_num_planes; |
| mdp_CAPTURE_queue->memory = V4L2_MEMORY_MMAP; |
| mdp_CAPTURE_queue->display_area = CAPTURE_queue.display_area; |
| mdp_CAPTURE_queue->displayed_frames = 0; |
| mdp_CAPTURE_queue->is_streaming = false; |
| ret = setup_CAPTURE(gbm, mdp_CAPTURE_queue, DRM_FORMAT_MOD_NONE, |
| /*frame_rate=*/0, /*use_CAPTURE_queue_dimensions=*/ true); |
| if (ret < 0) |
| LOG_FATAL("Image Processor: Unable to configure CAPTURE_queue."); |
| |
| process_opaque_frame_fp = &process_opaque_frame; |
| } |
| |
| if (subscribe_to_event(v4lfd, V4L2_EVENT_SOURCE_CHANGE, 0 /* id */)) |
| LOG_FATAL("Unable to subscribe to source change event."); |
| |
| if (!ret) { |
| ret = decode(gbm, &CAPTURE_queue, &OUTPUT_queue, modifier, |
| output_file_prefix, frames_to_decode, print_md5hash, |
| process_opaque_frame_fp, mdp_OUTPUT_queue, mdp_CAPTURE_queue); |
| } |
| |
| if (output_file_prefix) |
| free(output_file_prefix); |
| if(mdp_OUTPUT_queue) |
| free(mdp_OUTPUT_queue); |
| if (mdp_CAPTURE_queue) |
| free(mdp_CAPTURE_queue); |
| if (mdp_fd != -1) |
| close(mdp_fd); |
| |
| cleanup_queue(&OUTPUT_queue); |
| cleanup_queue(&CAPTURE_queue); |
| close(v4lfd); |
| close(drm_device_fd); |
| free(file_name); |
| cleanup_bitstream(); |
| |
| return ret; |
| } |