From 537578108634112924717fe0dea220aed94aae14 Mon Sep 17 00:00:00 2001 From: Devaev Maxim Date: Fri, 1 Jan 2021 14:42:46 +0300 Subject: [PATCH] h264 encoder --- src/h264/encoder.c | 390 +++++++++++++++++++++++++++++++++++++++++++++ src/h264/encoder.h | 70 ++++++++ src/h264/unjpeg.c | 7 +- 3 files changed, 465 insertions(+), 2 deletions(-) create mode 100644 src/h264/encoder.c create mode 100644 src/h264/encoder.h diff --git a/src/h264/encoder.c b/src/h264/encoder.c new file mode 100644 index 0000000..f008231 --- /dev/null +++ b/src/h264/encoder.c @@ -0,0 +1,390 @@ +/***************************************************************************** +# # +# uStreamer - Lightweight and fast MJPG-HTTP streamer. # +# # +# Copyright (C) 2018 Maxim Devaev # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +*****************************************************************************/ + + +#include "encoder.h" + + +static int _h264_encoder_configure(h264_encoder_s *encoder, const frame_s *frame); +static void _h264_encoder_cleanup(h264_encoder_s *encoder); +static int _h264_encoder_compress_raw(h264_encoder_s *encoder, const frame_s *src, frame_s *dest, bool force_key); + +static void _mmal_callback(MMAL_WRAPPER_T *wrapper); +static const char *_mmal_error_to_string(MMAL_STATUS_T error); + + +#define RUN(_next) encoder->run->_next + +#define LOG_ERROR_MMAL(_error, _msg, ...) { \ + LOG_ERROR(_msg ": %s", ##__VA_ARGS__, _mmal_error_to_string(_error)); \ + } + + +h264_encoder_s *h264_encoder_init(void) { + h264_encoder_runtime_s *run; + h264_encoder_s *encoder; + + A_CALLOC(run, 1); + run->tmp = frame_init("h264_tmp"); + + A_CALLOC(encoder, 1); + encoder->gop = 60; + encoder->bps = 5000 * 1000; // Kbps * 1000 + encoder->fps = 30; + encoder->run = run; + return encoder; +} + +void h264_encoder_destroy(h264_encoder_s *encoder) { + _h264_encoder_cleanup(encoder); + if (RUN(i_handler_sem)) { + vcos_semaphore_delete(&RUN(handler_sem)); + } + if (RUN(i_bcm_host)) { + bcm_host_deinit(); + } + frame_destroy(RUN(tmp)); + free(encoder); +} + +int h264_encoder_compress(h264_encoder_s *encoder, const frame_s *src, frame_s *dest, bool force_key) { + assert(src->used > 0); + assert(src->width > 0); + assert(src->height > 0); + assert(src->format > 0); + + if (!RUN(i_bcm_host)) { + LOG_INFO("Initializing BCM ..."); + bcm_host_init(); + RUN(i_bcm_host) = true; + } + + if (!RUN(i_handler_sem)) { + LOG_INFO("Creating VCOS semaphore ..."); + if (vcos_semaphore_create(&RUN(handler_sem), "h264_handler_sem", 0) != VCOS_SUCCESS) { + LOG_PERROR("Can't create VCOS semaphore"); + return -1; + } + RUN(i_handler_sem) = true; + } + + if (src->format == V4L2_PIX_FMT_MJPEG || src->format == V4L2_PIX_FMT_JPEG) { + LOG_DEBUG("Input frame format is JPEG; decoding ..."); + if (unjpeg(src, RUN(tmp)) < 0) { + return -1; + } + src = RUN(tmp); + } + + if (RUN(width) != src->width || RUN(height) != src->height || RUN(format) != src->format) { + if (_h264_encoder_configure(encoder, src) < 0) { + return -1; + } + force_key = true; + } + + if (_h264_encoder_compress_raw(encoder, src, dest, force_key) < 0) { + _h264_encoder_cleanup(encoder); + return -1; + } + return 0; +} + +static int _h264_encoder_configure(h264_encoder_s *encoder, const frame_s *frame) { + MMAL_STATUS_T error; + +# define PREPARE_PORT(_id) { \ + RUN(_id##_port) = RUN(wrapper->_id[0]); \ + if (RUN(_id##_port->is_enabled)) { \ + if ((error = mmal_wrapper_port_disable(RUN(_id##_port))) != MMAL_SUCCESS) { \ + LOG_ERROR_MMAL(error, "Can't disable MMAL %s port while configuring", #_id); \ + goto error; \ + } \ + } \ + } + +# define COMMIT_PORT(_id) { \ + if ((error = mmal_port_format_commit(RUN(_id##_port))) != MMAL_SUCCESS) { \ + LOG_ERROR_MMAL(error, "Can't commit MMAL %s port", #_id); \ + goto error; \ + } \ + } + +# define SET_PORT_PARAM(_id, _type, _key, _value) { \ + if ((error = mmal_port_parameter_set_##_type(RUN(_id##_port), _key, _value)) != MMAL_SUCCESS) { \ + LOG_ERROR_MMAL(error, "Can't set %s for the %s port", #_key, #_id); \ + goto error; \ + } \ + } + +# define ENABLE_PORT(_id) { \ + if ((error = mmal_wrapper_port_enable(RUN(_id##_port), MMAL_WRAPPER_FLAG_PAYLOAD_ALLOCATE)) != MMAL_SUCCESS) { \ + LOG_ERROR_MMAL(error, "Can't enable MMAL %s port", #_id); \ + goto error; \ + } \ + } + + _h264_encoder_cleanup(encoder); + + LOG_INFO("Applying H264 configuration ..."); + + if ((error = mmal_wrapper_create(&RUN(wrapper), MMAL_COMPONENT_DEFAULT_VIDEO_ENCODER)) != MMAL_SUCCESS) { + LOG_ERROR_MMAL(error, "Can't create MMAL wrapper"); + goto error; + } + RUN(wrapper->status) = MMAL_SUCCESS; + + { + PREPARE_PORT(input); + +# define IFMT(_next) RUN(input_port->format->_next) + IFMT(type) = MMAL_ES_TYPE_VIDEO; + if (frame->format == V4L2_PIX_FMT_UYVY) { + LOG_INFO("Using pixelformat: UYVY"); + IFMT(encoding) = MMAL_ENCODING_UYVY; + } else if (frame->format == V4L2_PIX_FMT_RGB24) { + LOG_INFO("Using pixelformat: RGB24"); + IFMT(encoding) = MMAL_ENCODING_RGB24; + } else { + char fourcc_buf[8]; + LOG_ERROR("Unsupported pixelformat (fourcc): %s", fourcc_to_string(frame->format, fourcc_buf, 8); + goto error; + } + LOG_INFO("Using resolution: %ux%u", frame->width, frame->height); + IFMT(es->video.width) = align_size(frame->width, 32); + IFMT(es->video.height) = align_size(frame->height, 16); + IFMT(es->video.crop.x) = 0; + IFMT(es->video.crop.y) = 0; + IFMT(es->video.crop.width) = frame->width; + IFMT(es->video.crop.height) = frame->height; + IFMT(flags) = MMAL_ES_FORMAT_FLAG_FRAMED; + RUN(input_port->buffer_size) = 1000 * 1000; + RUN(input_port->buffer_num) = RUN(input_port->buffer_num_recommended) * 4; +# undef IFMT + + COMMIT_PORT(input); + SET_PORT_PARAM(input, boolean, MMAL_PARAMETER_ZERO_COPY, MMAL_FALSE); + } + + { + PREPARE_PORT(output); + +# define OFMT(_next) RUN(output_port->format->_next) + OFMT(type) = MMAL_ES_TYPE_VIDEO; + OFMT(encoding) = MMAL_ENCODING_H264; + OFMT(encoding_variant) = MMAL_ENCODING_VARIANT_H264_DEFAULT; + OFMT(bitrate) = encoder->bps; + OFMT(es->video.frame_rate.num) = encoder->fps; + OFMT(es->video.frame_rate.den) = 1; + RUN(output_port->buffer_size) = RUN(output_port->buffer_size_recommended) * 4; + RUN(output_port->buffer_num) = RUN(output_port->buffer_num_recommended); +# undef OFMT + + COMMIT_PORT(output); + { + MMAL_PARAMETER_VIDEO_PROFILE_T profile; + MEMSET_ZERO(profile); + profile.hdr.id = MMAL_PARAMETER_PROFILE; + profile.hdr.size = sizeof(profile); + // http://blog.mediacoderhq.com/h264-profiles-and-levels + profile.profile[0].profile = MMAL_VIDEO_PROFILE_H264_CONSTRAINED_BASELINE; + profile.profile[0].level = MMAL_VIDEO_LEVEL_H264_4; // Supports 1080p + if ((error = mmal_port_parameter_set(RUN(output_port), &profile.hdr)) != MMAL_SUCCESS) { + LOG_ERROR_MMAL(error, "Can't set MMAL_PARAMETER_PROFILE for the output port"); + goto error; + } + } + + SET_PORT_PARAM(output, boolean, MMAL_PARAMETER_ZERO_COPY, MMAL_TRUE); + SET_PORT_PARAM(output, uint32, MMAL_PARAMETER_INTRAPERIOD, encoder->gop); + SET_PORT_PARAM(output, uint32, MMAL_PARAMETER_NALUNITFORMAT, MMAL_VIDEO_NALUNITFORMAT_STARTCODES); + SET_PORT_PARAM(output, boolean, MMAL_PARAMETER_MINIMISE_FRAGMENTATION, MMAL_TRUE); + SET_PORT_PARAM(output, uint32, MMAL_PARAMETER_MB_ROWS_PER_SLICE, 0); + SET_PORT_PARAM(output, boolean, MMAL_PARAMETER_VIDEO_IMMUTABLE_INPUT, MMAL_FALSE); + SET_PORT_PARAM(output, boolean, MMAL_PARAMETER_VIDEO_DROPPABLE_PFRAMES, MMAL_FALSE); + SET_PORT_PARAM(output, uint32, MMAL_PARAMETER_VIDEO_BIT_RATE, encoder->bps); + SET_PORT_PARAM(output, uint32, MMAL_PARAMETER_VIDEO_ENCODE_PEAK_RATE, encoder->bps); + SET_PORT_PARAM(output, uint32, MMAL_PARAMETER_VIDEO_ENCODE_MIN_QUANT, 16); + SET_PORT_PARAM(output, uint32, MMAL_PARAMETER_VIDEO_ENCODE_MAX_QUANT, 34); + SET_PORT_PARAM(output, uint32, MMAL_PARAMETER_VIDEO_ENCODE_FRAME_LIMIT_BITS, 1000000); + SET_PORT_PARAM(output, uint32, MMAL_PARAMETER_VIDEO_ENCODE_H264_AU_DELIMITERS, MMAL_FALSE); + } + + RUN(wrapper->user_data) = (void *)encoder; + RUN(wrapper->callback) = _mmal_callback; + + ENABLE_PORT(input); + ENABLE_PORT(output); + + RUN(width) = frame->width; + RUN(height) = frame->height; + RUN(format) = frame->format; + + return 0; + + error: + _h264_encoder_cleanup(encoder); + return -1; + +# undef ENABLE_PORT +# undef SET_PORT_PARAM +# undef COMMIT_PORT +# undef PREPARE_PORT +} + +static void _h264_encoder_cleanup(h264_encoder_s *encoder) { + MMAL_STATUS_T error; + +# define DISABLE_PORT(_id) { \ + if (RUN(_id##_port)) { \ + if ((error = mmal_wrapper_port_disable(RUN(_id##_port))) != MMAL_SUCCESS) { \ + LOG_ERROR_MMAL(error, "Can't disable MMAL %s port", #_id); \ + } \ + RUN(_id##_port) = NULL; \ + } \ + } + + DISABLE_PORT(input); + DISABLE_PORT(output); + +# undef DISABLE_PORT + + if (RUN(wrapper)) { + if ((error = mmal_wrapper_destroy(RUN(wrapper))) != MMAL_SUCCESS) { + LOG_ERROR_MMAL(error, "Can't destroy MMAL encoder"); + } + RUN(wrapper) = NULL; + } + + RUN(width) = 0; + RUN(height) = 0; + RUN(format) = 0; +} + +static int _h264_encoder_compress_raw(h264_encoder_s *encoder, const frame_s *src, frame_s *dest, bool force_key) { + MMAL_STATUS_T error; + MMAL_BUFFER_HEADER_T *out = NULL; + MMAL_BUFFER_HEADER_T *in = NULL; + bool eos = false; + bool sent = false; + + assert(src->used > 0); + assert(src->width == encoder->width); + assert(src->height == encoder->height); + assert(src->format == encoder->format); + + LOG_DEBUG("Compressing new H264 frame; force_key=%d ...", force_key); + + frame_copy_meta(src, dest); + dest->format = V4L2_PIX_FMT_H264; + dest->encode_begin_ts = get_now_monotonic(); + dest->used = 0; + + if (force_key) { + if ((error = mmal_port_parameter_set_boolean( + RUN(output_port), + MMAL_PARAMETER_VIDEO_REQUEST_I_FRAME, + MMAL_TRUE + )) != MMAL_SUCCESS) { + LOG_ERROR_MMAL(error, "Can't request keyframe"); + return -1; + } + } + + while (!eos) { + out = NULL; + while (mmal_wrapper_buffer_get_empty(RUN(output_port), &out, 0) == MMAL_SUCCESS) { + if ((error = mmal_port_send_buffer(RUN(output_port), out)) != MMAL_SUCCESS) { + LOG_ERROR_MMAL(error, "Can't send MMAL output buffer"); + return -1; + } + } + + in = NULL; + if (!sent && mmal_wrapper_buffer_get_empty(RUN(input_port), &in, 0) == MMAL_SUCCESS) { + in->data = src->data; + in->length = src->used; + in->offset = 0; + in->flags = MMAL_BUFFER_HEADER_FLAG_EOS; + if ((error = mmal_port_send_buffer(RUN(input_port), in)) != MMAL_SUCCESS) { + LOG_ERROR_MMAL(error, "Can't send MMAL input buffer"); + return -1; + } + sent = true; + } + + error = mmal_wrapper_buffer_get_full(RUN(output_port), &out, 0); + if (error == MMAL_EAGAIN) { + vcos_semaphore_wait(&RUN(handler_sem)); + continue; + } else if (error != MMAL_SUCCESS) { + LOG_ERROR_MMAL(error, "Can't get MMAL output buffer"); + return -1; + } + + frame_append_data(dest, out->data, out->length); + + eos = out->flags & MMAL_BUFFER_HEADER_FLAG_EOS; + mmal_buffer_header_release(out); + } + + if ((error = mmal_port_flush(RUN(output_port))) != MMAL_SUCCESS) { + LOG_ERROR_MMAL(error, "Can't flush MMAL output buffer; ignored"); + } + + dest->encode_end_ts = get_now_monotonic(); + LOG_VERBOSE("Compressed new H264 frame: force_key=%d, size=%zu, time=%0.3Lf", + force_key, dest->used, dest->encode_end_ts - dest->encode_begin_ts); + return 0; +} + +static void _mmal_callback(MMAL_WRAPPER_T *wrapper) { + vcos_semaphore_post(&((h264_encoder_s *)(wrapper->user_data))->run->handler_sem); +} + +static const char *_mmal_error_to_string(MMAL_STATUS_T error) { + // http://www.jvcref.com/files/PI/documentation/html/group___mmal_types.html +# define CASE_ERROR(_name, _msg) case MMAL_##_name: return "MMAL_" #_name " [" _msg "]" + switch (error) { + case MMAL_SUCCESS: return "MMAL_SUCCESS"; + CASE_ERROR(ENOMEM, "Out of memory"); + CASE_ERROR(ENOSPC, "Out of resources"); + CASE_ERROR(EINVAL, "Invalid argument"); + CASE_ERROR(ENOSYS, "Function not implemented"); + CASE_ERROR(ENOENT, "No such file or directory"); + CASE_ERROR(ENXIO, "No such device or address"); + CASE_ERROR(EIO, "IO error"); + CASE_ERROR(ESPIPE, "Illegal seek"); + CASE_ERROR(ECORRUPT, "Data is corrupt"); + CASE_ERROR(ENOTREADY, "Component is not ready"); + CASE_ERROR(ECONFIG, "Component is not configured"); + CASE_ERROR(EISCONN, "Port is already connected"); + CASE_ERROR(ENOTCONN, "Port is disconnected"); + CASE_ERROR(EAGAIN, "Resource temporarily unavailable"); + CASE_ERROR(EFAULT, "Bad address"); + case MMAL_STATUS_MAX: break; // Makes cpplint happy + } + return "Unknown error"; +# undef CASE_ERROR +} + +#undef LOG_ERROR_MMAL +#undef RUN diff --git a/src/h264/encoder.h b/src/h264/encoder.h new file mode 100644 index 0000000..28472d5 --- /dev/null +++ b/src/h264/encoder.h @@ -0,0 +1,70 @@ +/***************************************************************************** +# # +# uStreamer - Lightweight and fast MJPG-HTTP streamer. # +# # +# Copyright (C) 2018 Maxim Devaev # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +*****************************************************************************/ + + +#pragma once + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include "../common/tools.h" +#include "../common/logging.h" +#include "../common/frame.h" + +#include "unjpeg.h" + + +typedef struct { + MMAL_WRAPPER_T *wrapper; + MMAL_PORT_T *input_port; + MMAL_PORT_T *output_port; + VCOS_SEMAPHORE_T handler_sem; + bool i_bcm_host; + bool i_handler_sem; + + unsigned width; + unsigned height; + unsigned format; + frame_s *tmp; +} h264_encoder_runtime_s; + +typedef struct { + unsigned gop; // Interval between keyframes + unsigned bps; // Bit-per-sec + unsigned fps; + h264_encoder_runtime_s *run; +} h264_encoder_s; + + +h264_encoder_s *h264_encoder_init(void); +void h264_encoder_destroy(h264_encoder_s *encoder); + +int h264_encoder_compress(h264_encoder_s *encoder, const frame_s *src, frame_s *dest, bool force_key); diff --git a/src/h264/unjpeg.c b/src/h264/unjpeg.c index 917701b..b975071 100644 --- a/src/h264/unjpeg.c +++ b/src/h264/unjpeg.c @@ -39,6 +39,11 @@ int unjpeg(const frame_s *src, frame_s *dest) { unsigned row_stride; volatile int retval = 0; + frame_realloc_data(dest, ((src->width * src->height) << 1) * 2); + frame_copy_meta(src, dest); + dest->format = V4L2_PIX_FMT_RGB24; + dest->used = 0; + jpeg_create_decompress(&jpeg); // https://stackoverflow.com/questions/19857766/error-handling-in-libjpeg @@ -64,10 +69,8 @@ int unjpeg(const frame_s *src, frame_s *dest) { jpeg_finish_decompress(&jpeg); - frame_copy_meta(src, dest); dest->width = jpeg.output_width; dest->height = jpeg.output_height; - dest->format = V4L2_PIX_FMT_RGB24; done: jpeg_destroy_decompress(&jpeg);