blob: 7110778296b08cd6679d851fc1435ac9287b3e6a [file] [log] [blame]
/*
* 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 <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
#include "bs_drm.h"
#define FOURCC_SIZE 4
enum logging_levels {
kLoggingInfo = 0,
kLoggingError,
kLoggingFatal,
kLoggingLevelMax
};
#define DEFAULT_LOG_LEVEL kLoggingInfo
#define LOG(level, stream, fmt, ...) \
do { \
if (level >= log_run_level) { \
fprintf(stream, fmt, ##__VA_ARGS__); \
fprintf(stream, "\n"); \
} \
} while (0)
#define LOG_INFO(fmt, ...) LOG(kLoggingInfo, stdout, fmt, ##__VA_ARGS__)
#define LOG_ERROR(fmt, ...) LOG(kLoggingError, stderr, fmt, ##__VA_ARGS__)
#define LOG_FATAL(fmt, ...) \
do { \
LOG(kLoggingFatal, stderr, fmt, ##__VA_ARGS__); \
exit(EXIT_FAILURE); \
} while (0)
static const char* kDecodeDevice = "/dev/video-dec0";
static const int kInputbufferMaxSize = 4 * 1024 * 1024;
static const int kRequestBufferCount = 8;
static const int kTimestampMarkerForShowFrames = 1;
static const uint32_t kIVFHeaderSignature = v4l2_fourcc('D', 'K', 'I', 'F');
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_width| x |display_height|:
// The size of the image on the screen.
uint32_t display_width;
uint32_t display_height;
// |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;
uint32_t cnt;
uint32_t num_planes;
uint32_t memory;
uint32_t processed_frames;
uint32_t displayed_frames;
// Set to true when a frame is recieved with V4L2_BUF_FLAG_LAST set. This
// happens when a decoder returns the last frame from its stream. Any
// subsequent attempts to dequeue will fail with EPIPE.
bool buf_last_flag_has_been_set;
};
struct ivf_file_header {
uint32_t signature;
uint16_t version;
uint16_t header_length;
uint32_t fourcc;
uint16_t width;
uint16_t height;
uint32_t denominator;
uint32_t numerator;
uint32_t frame_cnt;
uint32_t unused;
} __attribute__((packed));
struct ivf_frame_header {
uint32_t size;
uint64_t timestamp;
} __attribute__((packed));
struct compressed_file {
FILE* fp;
struct ivf_file_header header;
};
struct md5_hash {
uint8_t bytes[16];
};
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';
}
void 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);
}
struct compressed_file open_file(const char* file_name) {
struct compressed_file file = {0};
FILE* fp = fopen(file_name, "rb");
if (fp) {
if (fread(&file.header, sizeof(struct ivf_file_header), 1, fp) != 1) {
fclose(fp);
LOG_ERROR("Unable to read ivf file header.");
}
if (file.header.signature != kIVFHeaderSignature) {
fclose(fp);
LOG_ERROR("Incorrect header signature : 0x%0x != 0x%0x",
file.header.signature, kIVFHeaderSignature);
}
file.fp = fp;
char fourcc[FOURCC_SIZE + 1];
fourcc_to_string(file.header.fourcc, fourcc);
LOG_INFO("OUTPUT format: %s", fourcc);
LOG_INFO("Ivf file header: %d x %d", file.header.width, file.header.height);
// |width| and |height| should be even numbers.
// This is because nv12 to i420 conversion (needed for md5hash computation)
// assumes |width| and |height| to be even numbers.
assert((file.header.width % 2) == 0);
assert((file.header.height % 2) == 0);
LOG_INFO("Ivf file header: frame_cnt = %d", file.header.frame_cnt);
} else {
LOG_ERROR("Unable to open file: %s.", file_name);
}
return file;
}
int query_format(int v4lfd, enum v4l2_buf_type type, uint32_t fourcc) {
struct v4l2_fmtdesc fmtdesc;
memset(&fmtdesc, 0, sizeof(fmtdesc));
fmtdesc.type = type;
while (ioctl(v4lfd, VIDIOC_ENUM_FMT, &fmtdesc) == 0) {
if (fourcc == 0) {
char fourcc_str[FOURCC_SIZE + 1];
fourcc_to_string(fmtdesc.pixelformat, fourcc_str);
LOG_INFO("%s", fourcc_str);
} else if (fourcc == fmtdesc.pixelformat)
return 1;
fmtdesc.index++;
}
return 0;
}
int capabilities(int v4lfd,
uint32_t compressed_format,
uint32_t uncompressed_format) {
struct v4l2_capability cap;
memset(&cap, 0, sizeof(cap));
int ret = ioctl(v4lfd, VIDIOC_QUERYCAP, &cap);
if (ret != 0)
LOG_ERROR("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);
if (!query_format(v4lfd, V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE,
compressed_format)) {
LOG_ERROR("Supported compressed formats:");
query_format(v4lfd, V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE, 0);
ret = 1;
}
if (!query_format(v4lfd, V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE,
uncompressed_format)) {
LOG_ERROR("Supported uncompressed formats:");
query_format(v4lfd, V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE, 0);
ret = 1;
}
return ret;
}
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;
}
// NV12 to I420 conversion.
// This function converts the NV12 |buffer_in| into an I420 |buffer_out|.
// |buffer_in| is padded, whereas |buffer_out| is tightly packed.
// Example: |display_width| = 8, |display_height| = 2, |buffer_width| = 10.
//
// NV12 I420
// YYYYYYYY00 YYYYYYYY
// YYYYYYYY00 YYYYYYYY
// UVUVUVUV00 UUUUVVVV
//
// HW pads 0s for |buffer_width - display_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).
void nv12_to_i420(uint32_t display_width,
uint32_t display_height,
uint32_t buffer_width,
uint32_t buffer_height,
uint8_t* buffer_in,
uint8_t* buffer_out) {
// Copies luma data from |buffer_in| one row at a time
// to avoid touching the padding.
for (int row = 0; row < display_height; ++row)
memcpy(buffer_out + row * display_width, buffer_in + row * buffer_width,
display_width);
const size_t y_plane_size = display_width * display_height;
const size_t u_plane_size = y_plane_size / 4;
uint8_t* u_plane_out = &buffer_out[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 < display_height / 2; ++row) {
for (int column = 0; column < display_width / 2; ++column) {
*(u_plane_out + row * display_width / 2 + column) =
buffer_in[uv_plane_offset + row * buffer_width + 2 * column];
*(v_plane_out + row * display_width / 2 + column) =
buffer_in[uv_plane_offset + row * buffer_width + 2 * column + 1];
}
}
}
// Computes md5 hash for each frame.
struct md5_hash compute_md5hash(struct queue* CAPTURE_queue,
uint32_t queue_index) {
// TODO: needs to handle V4L2_MEMORY_MMAP case
assert(CAPTURE_queue->memory == V4L2_MEMORY_DMABUF);
struct gbm_bo* bo = CAPTURE_queue->buffers[queue_index].bo;
int bo_fd = gbm_bo_get_fd(bo);
size_t 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));
uint8_t* buffer_nv12 = mmap(0, buffer_size, PROT_READ, MAP_SHARED, bo_fd, 0);
// Libvpx golden md5 hashes are calculated in I420 format.
// Uses |buffer_i420| to deinterleave |buffer_nv12| for this purpose.
const uint32_t display_width = CAPTURE_queue->display_width;
const uint32_t display_height = CAPTURE_queue->display_height;
// buffer w x h :
// The size of the buffer that the frame will be stored in.
// Usually the driver has limitations on alignment depending on HW.
const uint32_t buffer_width = gbm_bo_get_stride_for_plane(bo, 0);
const uint32_t buffer_height = gbm_bo_get_height(bo);
assert(buffer_width * buffer_height <= buffer_size);
const uint32_t buffer_i420_size_in_bytes =
(display_width * display_height * 3) / 2;
uint8_t* buffer_i420 = malloc(sizeof(size_t) * buffer_i420_size_in_bytes);
assert(buffer_i420);
memset(buffer_i420, 0, sizeof(size_t) * buffer_i420_size_in_bytes);
nv12_to_i420(display_width, display_height, buffer_width, buffer_height,
buffer_nv12, buffer_i420);
struct md5_hash hash;
MD5_CTX ctx;
MD5_Init(&ctx);
MD5_Update(&ctx, buffer_i420, buffer_i420_size_in_bytes);
MD5_Final(hash.bytes, &ctx);
free(buffer_i420);
munmap(buffer_nv12, buffer_size);
return hash;
}
// This is the input queue that will take compressed data.
// 4.5.1.5
int setup_OUTPUT(struct queue* OUTPUT_queue) {
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;
fmt.fmt.pix_mp.pixelformat = OUTPUT_queue->fourcc;
fmt.fmt.pix_mp.plane_fmt[0].sizeimage = kInputbufferMaxSize;
fmt.fmt.pix_mp.num_planes = 1;
int ret = ioctl(OUTPUT_queue->v4lfd, VIDIOC_S_FMT, &fmt);
if (ret != 0)
LOG_ERROR("VIDIOC_S_FMT failed: %s.", strerror(errno));
}
// 2. Allocates source (bytestream) buffers via VIDIOC_REQBUFS() on OUTPUT.
if (!ret) {
struct v4l2_requestbuffers reqbuf;
memset(&reqbuf, 0, sizeof(reqbuf));
reqbuf.count = kRequestBufferCount;
reqbuf.type = OUTPUT_queue->type;
reqbuf.memory = OUTPUT_queue->memory;
ret = ioctl(OUTPUT_queue->v4lfd, VIDIOC_REQBUFS, &reqbuf);
if (ret != 0)
LOG_ERROR("VIDIOC_REQBUFS failed: %s.", strerror(errno));
LOG_INFO("%d buffers requested, %d buffers for compressed data returned.",
kRequestBufferCount, reqbuf.count);
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("VIDIOC_STREAMON failed: %s.", strerror(errno));
}
return ret;
}
int submit_compressed_frame(struct compressed_file* file,
struct queue* OUTPUT_queue,
uint32_t queue_index,
uint32_t frames_to_decode) {
const uint32_t num = file->header.numerator;
const uint32_t den = file->header.denominator;
if (OUTPUT_queue->displayed_frames >= frames_to_decode) {
LOG_INFO(
"Already prepared requested number of frames |frames_to_decode|"
"from OUTPUT_queue.");
return 0;
}
struct ivf_frame_header frame_header = {0};
if (fread(&frame_header, sizeof(struct ivf_frame_header), 1, file->fp) != 1) {
if (!feof(file->fp)) {
LOG_ERROR("Unable to read ivf frame header.");
return -1;
} else {
LOG_INFO("Unable to read ivf frame header, reached the end of ivf file.");
return 0;
}
}
struct mmap_buffers* buffers = OUTPUT_queue->buffers;
if (fread(buffers[queue_index].start[0], sizeof(uint8_t), frame_header.size,
file->fp) != frame_header.size) {
LOG_ERROR("Unable to read ivf frame data.");
return -1;
}
OUTPUT_queue->processed_frames++;
if (is_vp9(file->header.fourcc)) {
// VP9 Bitstream and Decoding Process Specification
// Sec 6.2 "Uncompressed header syntax", pp.28.
// https://storage.googleapis.com/downloads.webmproject.org/docs/vp9/vp9-bitstream-specification-v0.6-20160331-draft.pdf
//
// |show_frame| is the second bit (counting from the least
// significant one) of the VP9 frame header's first byte.
const bool show_frame =
(*(uint32_t*)buffers[queue_index].start[0] & (1 << 1));
// Assumption here is that an non-displayed frame is always followed by a
// displayed frame. Please reference super-frame for more details.
// http://downloads.webmproject.org/docs/vp9/vp9-bitstream_superframe-and-uncompressed-header_v1.0.pdf
if (!show_frame)
OUTPUT_queue->processed_frames++;
}
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;
// Intentionally starting timestamp |kTimestampMarkerForShowFrames| at 1
// so that we can detect non-displayed frames with simpler logic.
v4l2_buffer.timestamp.tv_sec = kTimestampMarkerForShowFrames;
v4l2_buffer.timestamp.tv_usec = ((frame_header.timestamp * den) / num) * 100;
v4l2_buffer.m.planes = planes;
v4l2_buffer.m.planes[0].length = buffers[queue_index].length[0];
v4l2_buffer.m.planes[0].bytesused = frame_header.size;
v4l2_buffer.m.planes[0].data_offset = 0;
int ret = ioctl(OUTPUT_queue->v4lfd, VIDIOC_QBUF, &v4l2_buffer);
if (ret != 0) {
LOG_ERROR("VIDIOC_QBUF failed: %s.", strerror(errno));
return -1;
}
// Counts processed displayed frames in OUTPUT_queue. Non-displayed frames
// are not counted as non-displayed frame is a part of a super-frame.
OUTPUT_queue->displayed_frames++;
return 0;
}
int prime_OUTPUT(struct compressed_file* file,
struct queue* OUTPUT_queue,
uint32_t frames_to_decode) {
int ret = 0;
for (uint32_t i = 0; i < OUTPUT_queue->cnt; ++i) {
ret = submit_compressed_frame(file, OUTPUT_queue, i, frames_to_decode);
if (ret)
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("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.
// 4.5.1.6
int setup_CAPTURE(struct gbm_device* gbm,
struct queue* CAPTURE_queue,
uint64_t modifier) {
int ret = 0;
// 1. Calls 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;
int ret = ioctl(CAPTURE_queue->v4lfd, VIDIOC_G_FMT, &fmt);
if (ret != 0)
LOG_ERROR("VIDIOC_G_FMT failed: %s.", strerror(errno));
CAPTURE_queue->coded_width = fmt.fmt.pix_mp.width;
CAPTURE_queue->coded_height = fmt.fmt.pix_mp.height;
CAPTURE_queue->num_planes = fmt.fmt.pix_mp.num_planes;
LOG_INFO("CAPTURE: %d x %d", fmt.fmt.pix_mp.width, fmt.fmt.pix_mp.height);
LOG_INFO("num_planes = %d", fmt.fmt.pix_mp.num_planes);
}
// 4. Optional. Sets 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("VIDIOC_S_FMT 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("VIDIOC_REQBUFS failed: %s.", strerror(errno));
LOG_INFO("%d buffers requested, %d buffers for decoded data returned.",
kRequestBufferCount, reqbuf.count);
if (CAPTURE_queue->memory == V4L2_MEMORY_DMABUF) {
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);
CAPTURE_queue->buffers = buffers;
CAPTURE_queue->cnt = reqbuf.count;
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_NV12, &modifier, 1);
CAPTURE_queue->buffers[i].bo = bo;
if (bo) {
ret = queue_buffer_CAPTURE(CAPTURE_queue, i);
if (ret != 0)
break;
} else {
LOG_ERROR("Could not allocate a bo %d x %d.", width, height);
ret = -1;
break;
}
}
} else if (CAPTURE_queue->memory == V4L2_MEMORY_MMAP) {
ret = request_mmap_buffers(CAPTURE_queue, &reqbuf);
if (!ret) {
for (uint32_t i = 0; i < reqbuf.count; ++i) {
ret = queue_buffer_CAPTURE(CAPTURE_queue, i);
if (ret != 0)
break;
}
}
} else {
ret = -1;
}
}
// 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));
}
return ret;
}
void write_file_to_disk(FILE* fp,
struct queue* CAPTURE_queue,
uint32_t queue_index,
uint32_t frame_index,
bool show_frame) {
if (V4L2_MEMORY_DMABUF == CAPTURE_queue->memory) {
struct gbm_bo* bo = CAPTURE_queue->buffers[queue_index].bo;
int bo_fd = gbm_bo_get_fd(bo);
assert(bo_fd);
assert(gbm_bo_get_stride_for_plane(bo, 0) ==
gbm_bo_get_stride_for_plane(bo, 1));
const uint32_t buffer_width = gbm_bo_get_stride_for_plane(bo, 0);
const uint32_t buffer_height = gbm_bo_get_height(bo);
const uint32_t buffer_size = (buffer_width * buffer_height * 3) / 2;
uint8_t* buffer_nv12 =
mmap(0, buffer_size, PROT_READ, MAP_SHARED, bo_fd, 0);
fwrite(buffer_nv12, buffer_size, 1, fp);
munmap(buffer_nv12, buffer_size);
close(bo_fd);
} else {
if (CAPTURE_queue->num_planes == 1) {
size_t buffer_size =
(3 * CAPTURE_queue->coded_width * CAPTURE_queue->coded_height) >> 1;
uint8_t* buffer = CAPTURE_queue->buffers[queue_index].start[0];
fwrite(buffer, buffer_size, 1, fp);
} else {
for (uint32_t i = 0; i < CAPTURE_queue->num_planes; ++i) {
size_t buffer_size =
(CAPTURE_queue->coded_width * CAPTURE_queue->coded_height) >> i;
uint8_t* buffer = CAPTURE_queue->buffers[queue_index].start[i];
fwrite(buffer, buffer_size, 1, fp);
}
}
}
}
int dequeue_buffer(struct queue* queue,
uint32_t* index,
uint32_t* bytesused,
uint32_t* flags,
bool* show_frame) {
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.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;
// For displayed frames, timestamp values will be increasing for each
// frame starting from 1 (which can be configured differently for the
// first frame). For non-displayed frames, timestamp values will be 0.
// Thus, this info can be used to distinguish non-displayed frames
// from displayed frames.
if (show_frame) {
*show_frame =
!(v4l2_buffer.timestamp.tv_sec < kTimestampMarkerForShowFrames);
}
if (v4l2_buffer.flags & V4L2_BUF_FLAG_LAST) {
queue->buf_last_flag_has_been_set = true;
}
return ret;
}
// 4.5.1.10. Drain
// https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/dev-decoder.html#drain
int drain(struct queue* OUTPUT_queue, struct queue* CAPTURE_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);
assert(ret == 0);
// 2. Dequeue buffers
// The way the decode loop is set up, there shouldn't be any buffers
// left to dequeue. Furthermore, if the CAPTURE_queue has already set
// V4L2_BUF_FLAG_LAST, then there is no need to dequeue.
if (!CAPTURE_queue->buf_last_flag_has_been_set ) {
uint32_t bytesused = 0;
uint32_t flags = 0;
const int max_drain_delay = 2048;
// Checks to make sure the queue is empty.
// It takes time to execute V4L2_DEC_CMD_STOP cmd.
// From experiments, iterating |kMaxDrainDelay| times
// was good enough to get V4L2_BUF_FLAG_LAST flag.
int counter;
for (counter = max_drain_delay; counter > 0; --counter) {
ret = dequeue_buffer(CAPTURE_queue, NULL, &bytesused, &flags, NULL);
if (ret == 0) {
break;
} else {
if (errno != EAGAIN) {
LOG_ERROR("VIDIOC_DQBUF failed: %s", strerror(errno));
break;
}
}
}
if (counter == 0) {
LOG_ERROR(
"V4L2_DEC_CMD_STOP cmd execution was not completed within %d "
"iterations.\n",
max_drain_delay);
ret = 1;
}
if (!CAPTURE_queue->buf_last_flag_has_been_set && (bytesused != 0)) {
LOG_ERROR("WARNING: CAPTURE queue did not clean up.\n");
ret = 1;
}
}
// 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;
}
int decode(struct compressed_file* file,
struct queue* CAPTURE_queue,
struct queue* OUTPUT_queue,
uint64_t modifier,
FILE* output_file,
uint32_t frames_to_decode,
bool print_md5hash) {
int ret = 0;
if (!ret) {
while (CAPTURE_queue->processed_frames < OUTPUT_queue->processed_frames) {
{
uint32_t index = 0;
uint32_t flags = 0;
bool show_frame;
const int ret_dequeue = dequeue_buffer(CAPTURE_queue, &index, NULL,
&flags, &show_frame);
if (ret_dequeue != 0) {
if (errno != EAGAIN) {
LOG_ERROR("VIDIOC_DQBUF failed for CAPTURE queue: %s.",
strerror(errno));
ret = ret_dequeue;
break;
}
continue;
}
if (show_frame) {
CAPTURE_queue->displayed_frames++;
if (output_file) {
write_file_to_disk(output_file, CAPTURE_queue, index,
CAPTURE_queue->processed_frames, show_frame);
}
}
CAPTURE_queue->processed_frames++;
if (print_md5hash && show_frame) {
LOG_INFO("frame # %d - ", CAPTURE_queue->displayed_frames);
struct md5_hash hash = compute_md5hash(CAPTURE_queue, index);
for (int n = 0; n < 16; ++n)
printf("%02x", hash.bytes[n]);
printf("\n");
}
// Break out of the decode loop if V4L2_BUF_FLAG_LAST is set.
if (CAPTURE_queue->buf_last_flag_has_been_set) {
LOG_INFO("CAPTURE_queue produced last buffer.");
break;
}
// Done with buffer, queue it back up.
ret = queue_buffer_CAPTURE(CAPTURE_queue, index);
}
// A frame was recieved on the CAPTURE queue, that means there should
// now be a free OUTPUT buffer.
{
uint32_t index = 0;
const int ret_dequeue = dequeue_buffer(OUTPUT_queue, &index, NULL,
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;
}
submit_compressed_frame(file, OUTPUT_queue, index, frames_to_decode);
}
}
drain(OUTPUT_queue, CAPTURE_queue);
LOG_INFO("%d frames decoded, %d displayable, %d non-visible.",
CAPTURE_queue->processed_frames, CAPTURE_queue->displayed_frames,
CAPTURE_queue->processed_frames - 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 out decompressed frames to a file\n");
printf(" -m, --max max number of visible frames to decode\n");
printf(" -b, --buffer use mmap instead of dmabuf\n");
printf(" -o, --output_fmt fourcc of output format\n");
printf(" -l, --log_level specifies log level, 0:info 1:error 2:fatal \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'},
{"buffer", no_argument, NULL, 'b'},
{"output_fmt", no_argument, NULL, 'o'},
{"log_level", required_argument, NULL, 'l'},
{"md5", no_argument, NULL, 'd'},
{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;
uint64_t modifier = DRM_FORMAT_MOD_LINEAR;
uint32_t uncompressed_fourcc = v4l2_fourcc('N', 'V', '1', '2');
uint32_t CAPTURE_memory = V4L2_MEMORY_DMABUF;
while ((c = getopt_long(argc, argv, "wbdm:f:o:l:", 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 'b':
CAPTURE_memory = V4L2_MEMORY_MMAP;
break;
case 'o':
if (strlen(optarg) == 4) {
uncompressed_fourcc =
v4l2_fourcc(toupper(optarg[0]), toupper(optarg[1]),
toupper(optarg[2]), toupper(optarg[3]));
if (uncompressed_fourcc == v4l2_fourcc('Q', '1', '2', '8'))
modifier = DRM_FORMAT_MOD_QCOM_COMPRESSED;
}
break;
case 'l': {
const uint32_t 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;
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);
char fourcc[FOURCC_SIZE + 1];
fourcc_to_string(uncompressed_fourcc, fourcc);
LOG_INFO("CAPTURE format: %s", fourcc);
if (uncompressed_fourcc == v4l2_fourcc('Q', '1', '2', '8'))
LOG_INFO("Compressed format, setting modifier.");
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.");
}
struct compressed_file compressed_file = open_file(file_name);
if (!compressed_file.fp)
LOG_FATAL("Unable to open ivf file: %s.", file_name);
FILE* output_file = NULL;
if (write_out) {
// Replaces |.ivf| with |.yuv| in |file_name|.
strcpy(strrchr(file_name, '.'), ".yuv");
output_file = fopen(file_name, "wb");
if (!output_file) {
LOG_ERROR("Unable to open output yuv file: %s.", file_name);
return 1;
}
}
int v4lfd = open(kDecodeDevice, O_RDWR | O_NONBLOCK | O_CLOEXEC);
if (v4lfd < 0)
LOG_FATAL("Unable to open device file: %s.", kDecodeDevice);
if (capabilities(v4lfd, compressed_file.header.fourcc, uncompressed_fourcc) !=
0)
LOG_FATAL("Capabilities not present for decode.");
struct queue OUTPUT_queue = {.v4lfd = v4lfd,
.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE,
.fourcc = compressed_file.header.fourcc,
.num_planes = 1,
.memory = V4L2_MEMORY_MMAP,
.processed_frames = 0,
.displayed_frames = 0};
int ret = setup_OUTPUT(&OUTPUT_queue);
if (!ret)
ret = prime_OUTPUT(&compressed_file, &OUTPUT_queue, frames_to_decode);
struct queue CAPTURE_queue = {.v4lfd = v4lfd,
.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE,
.fourcc = uncompressed_fourcc,
.num_planes = 1,
.memory = CAPTURE_memory,
.display_width = compressed_file.header.width,
.display_height = compressed_file.header.height,
.processed_frames = 0,
.displayed_frames = 0};
if (!ret)
ret = setup_CAPTURE(gbm, &CAPTURE_queue, modifier);
if (!ret) {
ret = decode(&compressed_file, &CAPTURE_queue, &OUTPUT_queue, modifier,
output_file, frames_to_decode, print_md5hash);
}
if (write_out)
fclose(output_file);
cleanup_queue(&OUTPUT_queue);
cleanup_queue(&CAPTURE_queue);
close(v4lfd);
fclose(compressed_file.fp);
close(drm_device_fd);
free(file_name);
return ret;
}