| /* | |
| * Copyright 2021 The ChromiumOS Authors | |
| * 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 <libgen.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/stat.h> | |
| #include <sys/types.h> | |
| #include <sys/mman.h> | |
| #include <unistd.h> | |
| #include "bitstreams/bitstream_helper.h" | |
| #include "bs_drm.h" | |
| #include "pixel_formats/mt21_converter.h" | |
| #include "pixel_formats/q08c_converter.h" | |
| #include "v4l2_macros.h" | |
| #ifndef V4L2_PIX_FMT_QC08C | |
| #define V4L2_PIX_FMT_QC08C v4l2_fourcc('Q', '0', '8', 'C') /* Qualcomm 8-bit compressed */ | |
| #endif | |
| #ifndef V4L2_PIX_FMT_QC10C | |
| #define V4L2_PIX_FMT_QC10C v4l2_fourcc('Q', '1', '0', 'C') /* Qualcomm 10-bit compressed */ | |
| #endif | |
| static const char* kDecodeDevice = "/dev/video-dec0"; | |
| static const int kInputbufferMaxSize = 4 * 1024 * 1024; | |
| static const int kRequestBufferCount = 9; | |
| 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 uint32_t kPreferredUncompressedFourCCs[] = { | |
| V4L2_PIX_FMT_NV12, V4L2_PIX_FMT_QC08C, V4L2_PIX_FMT_MT21C, | |
| V4L2_PIX_FMT_P010}; | |
| static int log_run_level = DEFAULT_LOG_LEVEL; | |
| struct per_frame_md5_hash_logs { | |
| bool enabled; | |
| bool output_file_enabled; | |
| FILE* output_fp; | |
| }; | |
| 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]; | |
| // Size of the plane in bytes. Copied from the V4L2 multi-planar format | |
| // structure. | |
| size_t size_image[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 bitstream* bts; | |
| }; | |
| struct md5_hash { | |
| uint8_t bytes[16]; | |
| }; | |
| struct data_buffer { | |
| uint8_t* data; | |
| size_t size; | |
| }; | |
| enum file_output_type { | |
| FILE_NONE = 0, | |
| FILE_SINGLE = 1, | |
| FILE_MULTIPLE = 2 | |
| }; | |
| struct file_output { | |
| FILE* single_fp; | |
| char* file_prefix; | |
| enum file_output_type type; | |
| bool raw; | |
| }; | |
| 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|. | |
| 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_P010 || | |
| *uncompressed_format == V4L2_PIX_FMT_QC10C) { | |
| char fourcc_str[FOURCC_SIZE + 1]; | |
| LOG_INFO("Proceeding with CAPTURE format %s. Unable to test for high bit " | |
| "depth capabilities before bitstream is availible.", | |
| fourcc_to_string(*uncompressed_format, fourcc_str)); | |
| } else 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; | |
| } | |
| } | |
| 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 = GBM_FORMAT_NV12; | |
| if (CAPTURE_queue->fourcc == V4L2_PIX_FMT_P010) | |
| gbm_format = GBM_FORMAT_P010; | |
| 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_left = display_area.left / 2; | |
| const size_t u_plane_top = display_area.top / 2; | |
| 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 + (u_plane_top + row) * buffer_width + | |
| 2 * (u_plane_left + column)]; | |
| *(v_plane_out + row * u_plane_width + column) = | |
| buffer_in[uv_plane_offset + (u_plane_top + row) * buffer_width + | |
| 2 * (u_plane_left + column) + 1]; | |
| } | |
| } | |
| return buffer_out; | |
| } | |
| // P010 to I010 conversion. | |
| // Does the same deinterleaving conversion that |nv12_to_i420| does. | |
| // P010 use the msb of the 16 bits. In order to convert to I010 each | |
| // pixel needs to be downshifted 6 (16 - 10). | |
| struct data_buffer p010_to_i010(struct v4l2_rect display_area, | |
| uint32_t buffer_width_in_pixels, | |
| uint32_t buffer_height, | |
| uint8_t* buffer_in) { | |
| const size_t y_plane_size_in_pixels = display_area.width * | |
| display_area.height * 2; | |
| // Both P010 and I010 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 uv_plane_left = display_area.left / 2; | |
| const size_t uv_plane_top = display_area.top / 2; | |
| const size_t uv_plane_width = (display_area.width + 1) / 2; | |
| const size_t uv_plane_height = (display_area.height + 1) / 2; | |
| const size_t uv_plane_size_in_pixels = uv_plane_width * uv_plane_height * 2; | |
| struct data_buffer buffer_out = {.data = NULL, | |
| .size = y_plane_size_in_pixels + | |
| 2 * uv_plane_size_in_pixels}; | |
| 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); | |
| uint16_t* y_plane_out = (uint16_t*)buffer_out.data; | |
| uint16_t const* y_plane_in = (uint16_t*)buffer_in; | |
| y_plane_in += display_area.left; | |
| y_plane_in += display_area.top * buffer_width_in_pixels >> 1; | |
| for (int y = 0; y < display_area.height; ++y) { | |
| for (int x = 0 ; x < display_area.width; ++x) { | |
| y_plane_out[x] = y_plane_in[x] >> 6; | |
| } | |
| y_plane_out += display_area.width; | |
| y_plane_in += buffer_width_in_pixels >> 1; | |
| } | |
| uint16_t* u_plane_out = (uint16_t*)(buffer_out.data + y_plane_size_in_pixels); | |
| uint16_t* v_plane_out = u_plane_out + (uv_plane_size_in_pixels >> 1); | |
| const size_t uv_plane_offset = buffer_width_in_pixels * buffer_height; | |
| uint16_t const* uv_plane_in = (uint16_t*)(buffer_in + uv_plane_offset); | |
| uv_plane_in += uv_plane_left; | |
| uv_plane_in += uv_plane_top * buffer_width_in_pixels >> 1; | |
| for (int row = 0; row < uv_plane_height; ++row) { | |
| for (int column = 0; column < uv_plane_width; ++column) { | |
| u_plane_out[column] = uv_plane_in[2 * column] >> 6; | |
| v_plane_out[column] = uv_plane_in[2 * column + 1] >> 6; | |
| } | |
| u_plane_out += uv_plane_width; | |
| v_plane_out += uv_plane_width; | |
| uv_plane_in += buffer_width_in_pixels >> 1; | |
| } | |
| return buffer_out; | |
| } | |
| void compute_and_print_md5hash(const void *data, | |
| size_t len, | |
| uint32_t frame_index, | |
| const struct per_frame_md5_hash_logs* | |
| md5_hash_logs) { | |
| 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); | |
| char md5Str[33] = {}; | |
| for (int n = 0; n < 16; ++n) { | |
| sprintf(&md5Str[n * 2], "%02x", hash.bytes[n]); | |
| } | |
| LOG_INFO("MD5 checksum hash: %s", md5Str); | |
| if (md5_hash_logs->output_file_enabled) { | |
| fprintf(md5_hash_logs->output_fp, "%s\n", md5Str); | |
| } | |
| } | |
| // 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); | |
| // Maps the frame buffer into |y_buffer| 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* plane_0_buffer = NULL; | |
| uint8_t* plane_1_buffer = NULL; | |
| int bo_fd = 0; | |
| if (CAPTURE_queue->memory == V4L2_MEMORY_DMABUF) { | |
| struct gbm_bo* bo = CAPTURE_queue->buffers[queue_index].bo; | |
| assert(CAPTURE_queue->fourcc != V4L2_PIX_FMT_MT21C); | |
| 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); | |
| plane_0_buffer = mmap(0, buffer_size, PROT_READ, MAP_SHARED, bo_fd, 0); | |
| } else { | |
| assert(CAPTURE_queue->memory == V4L2_MEMORY_MMAP); | |
| 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]; | |
| plane_0_buffer = CAPTURE_queue->buffers[queue_index].start[0]; | |
| if (CAPTURE_queue->fourcc == V4L2_PIX_FMT_MT21C) { | |
| plane_1_buffer = CAPTURE_queue->buffers[queue_index].start[1]; | |
| buffer_size += CAPTURE_queue->buffers[queue_index].length[1]; | |
| } | |
| } | |
| 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 = {}; | |
| if (CAPTURE_queue->fourcc == V4L2_PIX_FMT_MT21C) { | |
| size_t tmp_buf_size = CAPTURE_queue->coded_width * buffer_height * 3 / 2; | |
| uint8_t* nv12_buffer = (uint8_t*)malloc(tmp_buf_size); | |
| convert_mt21_to_nv12( | |
| nv12_buffer, nv12_buffer + CAPTURE_queue->coded_width * buffer_height, | |
| plane_0_buffer, plane_1_buffer, CAPTURE_queue->coded_width, | |
| CAPTURE_queue->coded_height, | |
| CAPTURE_queue->buffers[queue_index].length[0], | |
| CAPTURE_queue->buffers[queue_index].length[1]); | |
| buffer_i420 = | |
| nv12_to_i420(CAPTURE_queue->display_area, CAPTURE_queue->coded_width, | |
| buffer_height, nv12_buffer); | |
| free(nv12_buffer); | |
| } else if (CAPTURE_queue->fourcc == V4L2_PIX_FMT_QC08C) { | |
| uint8_t* nv12_buffer = (uint8_t*)malloc(buffer_size); | |
| convert_q08c_to_nv12(nv12_buffer, | |
| nv12_buffer + buffer_bytes_per_line * buffer_height, | |
| plane_0_buffer, CAPTURE_queue->coded_width, | |
| CAPTURE_queue->coded_height); | |
| buffer_i420 = | |
| nv12_to_i420(CAPTURE_queue->display_area, buffer_bytes_per_line, | |
| buffer_height, nv12_buffer); | |
| free(nv12_buffer); | |
| } else if (CAPTURE_queue->fourcc == V4L2_PIX_FMT_NV12) { | |
| buffer_i420 = | |
| nv12_to_i420(CAPTURE_queue->display_area, buffer_bytes_per_line, | |
| buffer_height, plane_0_buffer); | |
| } else { | |
| assert(CAPTURE_queue->fourcc == V4L2_PIX_FMT_P010); | |
| buffer_i420 = | |
| p010_to_i010(CAPTURE_queue->display_area, buffer_bytes_per_line, | |
| buffer_height, plane_0_buffer); | |
| } | |
| if (CAPTURE_queue->memory == V4L2_MEMORY_DMABUF) { | |
| assert(plane_1_buffer == NULL); | |
| munmap(plane_0_buffer, 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, | |
| const struct per_frame_md5_hash_logs* | |
| md5_hash_logs) { | |
| 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, md5_hash_logs); | |
| 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], | |
| OUTPUT_queue->bts); | |
| 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(OUTPUT_queue->bts)) | |
| 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) { | |
| int ret = 0; | |
| // 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)); | |
| // By default the CAPTURE format is not known at initialization because | |
| // it could be 8bit or 10bit and that won't be known until the file is | |
| // parsed. The format provided by VIDIOC_G_FMT may not be a preferred | |
| // format, in which case one should be choosen from the preferred list. | |
| if (CAPTURE_queue->fourcc == V4L2_PIX_FMT_INVALID) { | |
| const size_t num_fourccs = | |
| sizeof(kPreferredUncompressedFourCCs) / sizeof(uint32_t); | |
| // Pick from a list of supported formats in order of their preference. | |
| for (size_t i = 0; i < num_fourccs; ++i) { | |
| if (is_fourcc_supported(CAPTURE_queue->v4lfd, | |
| V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE, | |
| kPreferredUncompressedFourCCs[i])) { | |
| CAPTURE_queue->fourcc = kPreferredUncompressedFourCCs[i]; | |
| break; | |
| } | |
| } | |
| } | |
| assert(CAPTURE_queue->fourcc != V4L2_PIX_FMT_INVALID); | |
| if (CAPTURE_queue->fourcc == V4L2_PIX_FMT_QC08C) { | |
| modifier = DRM_FORMAT_MOD_QCOM_COMPRESSED; | |
| LOG_INFO("CAPTURE format is compressed, setting modifier."); | |
| } | |
| // 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); | |
| } | |
| 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; | |
| if (CAPTURE_queue->fourcc == V4L2_PIX_FMT_MT21C) { | |
| // Factor in header when allocating buffers. | |
| fmt.fmt.pix_mp.plane_fmt[0].sizeimage = compute_mt21_min_y_size( | |
| CAPTURE_queue->coded_width, CAPTURE_queue->coded_height); | |
| fmt.fmt.pix_mp.plane_fmt[1].sizeimage = compute_mt21_min_uv_size( | |
| CAPTURE_queue->coded_width, 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)); | |
| // 7.31.4. | |
| // When the application calls the VIDIOC_S_FMT ioctl with a pointer to a | |
| // struct v4l2_format structure the driver checks and adjusts the | |
| // parameters against hardware abilities. Drivers should not return an | |
| // error code unless the type field is invalid, this is a mechanism to | |
| // fathom device capabilities and to approach parameters acceptable for | |
| // both the application and driver. | |
| assert(fmt.fmt.pix_mp.pixelformat == CAPTURE_queue->fourcc); | |
| CAPTURE_queue->num_planes = fmt.fmt.pix_mp.num_planes; | |
| if (CAPTURE_queue->fourcc == V4L2_PIX_FMT_MT21C) { | |
| assert(CAPTURE_queue->num_planes == 2); | |
| } | |
| for (int i = 0; i < CAPTURE_queue->num_planes; ++i) { | |
| CAPTURE_queue->bytes_per_line[i] = | |
| fmt.fmt.pix_mp.plane_fmt[i].bytesperline; | |
| CAPTURE_queue->size_image[i] = | |
| fmt.fmt.pix_mp.plane_fmt[i].sizeimage; | |
| } | |
| } | |
| // 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 struct file_output* file_output, | |
| struct queue* CAPTURE_queue, | |
| uint32_t queue_index) { | |
| FILE* fp = 0; | |
| if (FILE_MULTIPLE == file_output->type) { | |
| #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, | |
| file_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, | |
| file_output->file_prefix, | |
| CAPTURE_queue->displayed_frames, | |
| CAPTURE_queue->display_area.width, | |
| CAPTURE_queue->display_area.height); | |
| #undef FILE_NAME_FORMAT | |
| fp = fopen(file_name, "wb"); | |
| if (!fp) | |
| LOG_ERROR("Unable to open output yuv file: %s.", file_name); | |
| } else if (FILE_SINGLE == file_output->type) { | |
| fp = file_output->single_fp; | |
| } else { | |
| return; | |
| } | |
| if (file_output->raw) { | |
| if (CAPTURE_queue->memory == V4L2_MEMORY_DMABUF) { | |
| struct gbm_bo* bo = CAPTURE_queue->buffers[queue_index].bo; | |
| int bo_fd = gbm_bo_get_fd(bo); | |
| assert(bo_fd > 0); | |
| off_t buffer_size = lseek(bo_fd, 0, SEEK_END); | |
| lseek(bo_fd, 0, SEEK_SET); | |
| void* buffer = mmap(0, buffer_size, PROT_READ, MAP_SHARED, bo_fd, 0); | |
| fwrite(buffer, buffer_size, 1, fp); | |
| munmap(buffer, buffer_size); | |
| close(bo_fd); | |
| } else { | |
| for (uint32_t i = 0; i < CAPTURE_queue->num_planes; ++i) { | |
| fwrite(CAPTURE_queue->buffers[queue_index].start[i], | |
| CAPTURE_queue->size_image[i], 1, fp); | |
| } | |
| } | |
| } else if (CAPTURE_queue->fourcc == V4L2_PIX_FMT_NV12 || | |
| CAPTURE_queue->fourcc == V4L2_PIX_FMT_P010 || | |
| CAPTURE_queue->fourcc == V4L2_PIX_FMT_MT21C || | |
| CAPTURE_queue->fourcc == V4L2_PIX_FMT_QC08C) { | |
| struct data_buffer buffer_i420 = | |
| map_buffers_and_convert_to_i420(CAPTURE_queue, queue_index); | |
| fwrite(buffer_i420.data, buffer_i420.size, 1, fp); | |
| free(buffer_i420.data); | |
| } else { | |
| LOG_ERROR("No conversion to I420 for this file. Unable to write to disk."); | |
| } | |
| if (FILE_MULTIPLE == file_output->type) | |
| 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; | |
| } | |
| // 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); | |
| } | |
| return ret; | |
| } | |
| int decode(struct gbm_device* gbm, | |
| struct queue* CAPTURE_queue, | |
| struct queue* OUTPUT_queue, | |
| uint64_t modifier, | |
| const struct file_output *file_output, | |
| uint32_t frames_to_decode, | |
| const struct per_frame_md5_hash_logs* md5_hash_logs) { | |
| 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--; | |
| } else { | |
| // 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 (FILE_NONE != file_output->type) | |
| write_frame_to_disk(file_output, CAPTURE_queue, index); | |
| if (md5_hash_logs->enabled) | |
| map_buffers_and_calculate_md5hash(CAPTURE_queue, index, | |
| md5_hash_logs); | |
| } 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); | |
| } | |
| } | |
| } | |
| // Check the OUTPUT queue for free buffers and fill accordingly. | |
| 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(OUTPUT_queue->bts)) | |
| 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 output visible frames in I420 format " | |
| "to individual files (one frame per file)\n"); | |
| printf(" -b, --raw output visible frames in original format " | |
| "one frame per file. The entire buffer is written " | |
| "as is without any cropping or conversion.\n"); | |
| printf(" -s, --single file name to output all visible frames in I420 " | |
| "format to\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 and displays in an INFO log. If " | |
| "specified with argument will also write " | |
| "checksums to specified file.\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", optional_argument, NULL, 'd'}, | |
| {"frame_rate", required_argument, NULL, 'r'}, | |
| {"single", required_argument, NULL, 's'}, | |
| {"raw", no_argument, NULL, 'b'}, | |
| {0, 0, 0, 0}, | |
| }; | |
| int main(int argc, char* argv[]) { | |
| int c; | |
| char* file_name = NULL; | |
| 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; | |
| struct file_output file_output = {.raw = false}; | |
| struct per_frame_md5_hash_logs md5_hash_logs = {.enabled = false, | |
| .output_file_enabled = false, | |
| .output_fp = NULL}; | |
| while ((c = getopt_long(argc, argv, "f:m:wbac:l:d::r:s:", longopts, NULL)) | |
| != -1) { | |
| switch (c) { | |
| case 'f': | |
| file_name = strdup(optarg); | |
| break; | |
| case 'm': | |
| frames_to_decode = atoi(optarg); | |
| break; | |
| case 's': | |
| if (file_output.type != FILE_NONE) { | |
| LOG_FATAL("Can only write out single OR multiple files, not both"); | |
| } else { | |
| char* path_str = strdup(optarg); | |
| char* dir_str = dirname(path_str); | |
| mkdir(dir_str, 0777); | |
| free(path_str); | |
| file_output.single_fp = fopen(optarg, "wb"); | |
| file_output.type = FILE_SINGLE; | |
| if (!file_output.single_fp) | |
| LOG_FATAL("Unable to create output yuv file: %s.", optarg); | |
| } | |
| break; | |
| case 'b': | |
| file_output.raw = true; | |
| case 'w': | |
| if (file_output.type != FILE_NONE) { | |
| LOG_FATAL("Can only write out single OR multiple files, not both"); | |
| } else { | |
| file_output.type = FILE_MULTIPLE; | |
| } | |
| 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_QC08C) { | |
| 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': | |
| md5_hash_logs.enabled = true; | |
| if(!optarg) | |
| break; | |
| md5_hash_logs.output_fp = fopen(optarg, "w"); | |
| if(!md5_hash_logs.output_fp) | |
| LOG_FATAL("Unable to create md5 checksum log file."); | |
| md5_hash_logs.output_file_enabled = 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: | |
| LOG_FATAL("Invalid argument syntax"); | |
| 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 = -1; | |
| struct gbm_device* gbm = NULL; | |
| const bool use_gbm = V4L2_MEMORY_DMABUF == CAPTURE_memory; | |
| if (use_gbm) { | |
| drm_device_fd = bs_drm_open_main_display(); | |
| if (drm_device_fd < 0) { | |
| LOG_FATAL("Failed to open card for display."); | |
| } | |
| gbm = gbm_create_device(drm_device_fd); | |
| if (!gbm) { | |
| close(drm_device_fd); | |
| LOG_FATAL("Failed to create gbm device."); | |
| } | |
| } | |
| struct bitstream bts; | |
| init_bitstream(file_name, &bts); | |
| if (FILE_MULTIPLE == file_output.type) { | |
| const size_t len_prefix = strrchr(file_name, '.') - file_name; | |
| file_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, bts.fourcc, &uncompressed_fourcc)) | |
| LOG_FATAL("Not enough capabilities present for decoding."); | |
| struct queue OUTPUT_queue = {.v4lfd = v4lfd, | |
| .type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE, | |
| .fourcc = bts.fourcc, | |
| .num_planes = 1, | |
| .memory = V4L2_MEMORY_MMAP, | |
| .displayed_frames = 0, | |
| .is_streaming = false, | |
| .bts = &bts}; | |
| 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); | |
| } | |
| 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, | |
| &file_output, frames_to_decode, &md5_hash_logs); | |
| } | |
| if (file_output.file_prefix) | |
| free(file_output.file_prefix); | |
| if (file_output.single_fp) | |
| fclose(file_output.single_fp); | |
| cleanup_queue(&OUTPUT_queue); | |
| cleanup_queue(&CAPTURE_queue); | |
| close(v4lfd); | |
| if (use_gbm) | |
| close(drm_device_fd); | |
| free(file_name); | |
| cleanup_bitstream(&bts); | |
| // Closes checksum log file if specified | |
| if (md5_hash_logs.output_file_enabled) | |
| fclose(md5_hash_logs.output_fp); | |
| return ret; | |
| } |