From bb825de73ba727ba481bbe958f4eb3795bdc06aa Mon Sep 17 00:00:00 2001 From: Sean Reid Date: Sat, 10 Mar 2018 10:57:32 -0500 Subject: [PATCH] GUACAMOLE-465: added dependency to libavformat as first step to supporting other types of codecs and containers in guacenc. migrated existing functionality to use the libavformat library for writing the files. there is not differnce to the user with this patch, but it provides a good base to finish this new feature from later --- configure.ac | 28 ++++++-- src/guacenc/Makefile.am | 14 ++-- src/guacenc/ffmpeg-compat.c | 103 ++++++++++++++++++++++++-- src/guacenc/ffmpeg-compat.h | 72 +++++++++++++++++++ src/guacenc/guacenc.c | 4 ++ src/guacenc/video.c | 139 +++++++++++++++++++++++++----------- src/guacenc/video.h | 19 ++++- 7 files changed, 320 insertions(+), 59 deletions(-) diff --git a/configure.ac b/configure.ac index dba5f715..fd2c93e0 100644 --- a/configure.ac +++ b/configure.ac @@ -202,6 +202,24 @@ fi AM_CONDITIONAL([ENABLE_AVCODEC], [test "x${have_libavcodec}" = "xyes"]) +# +# libavformat +# + +have_libavformat=disabled +AC_ARG_WITH([libavformat], + [AS_HELP_STRING([--with-libavformat], + [use libavformat when encoding video @<:@default=check@:>@])], + []. + [with_libavformat=check]) +if test "x$with_libavformat" != "xno" +then + have_libavformat=yes + PKG_CHECK_MODULES([AVFORMAT], [libavformat],, [have_libavformat=no]); +fi + +AM_CONDITIONAL([ENABLE_AVFORMAT], [test "x${have_libavformat}" = "xyes"]) + # # libavutil # @@ -980,10 +998,11 @@ AC_ARG_ENABLE([guacenc], [], [enable_guacenc=yes]) -AM_CONDITIONAL([ENABLE_GUACENC], [test "x${enable_guacenc}" = "xyes" \ - -a "x${have_libavcodec}" = "xyes" \ - -a "x${have_libavutil}" = "xyes" \ - -a "x${have_libswscale}" = "xyes"]) +AM_CONDITIONAL([ENABLE_GUACENC], [test "x${enable_guacenc}" = "xyes" \ + -a "x${have_libavcodec}" = "xyes" \ + -a "x${have_libavutil}" = "xyes" \ + -a "x${have_libswscale}" = "xyes" \ + -a "x${have_libavformat}" = "xyes"]) # # guaclog @@ -1076,6 +1095,7 @@ $PACKAGE_NAME version $PACKAGE_VERSION freerdp2 ............ ${have_freerdp2} pango ............... ${have_pango} libavcodec .......... ${have_libavcodec} + libavformat.......... ${have_libavformat} libavutil ........... ${have_libavutil} libssh2 ............. ${have_libssh2} libssl .............. ${have_ssl} diff --git a/src/guacenc/Makefile.am b/src/guacenc/Makefile.am index 8debd632..34aeaf6a 100644 --- a/src/guacenc/Makefile.am +++ b/src/guacenc/Makefile.am @@ -90,6 +90,7 @@ endif guacenc_CFLAGS = \ -Werror -Wall \ @AVCODEC_CFLAGS@ \ + @AVFORMAT_CFLAGS@ \ @AVUTIL_CFLAGS@ \ @LIBGUAC_INCLUDE@ \ @SWSCALE_CFLAGS@ @@ -97,12 +98,13 @@ guacenc_CFLAGS = \ guacenc_LDADD = \ @LIBGUAC_LTLIB@ -guacenc_LDFLAGS = \ - @AVCODEC_LIBS@ \ - @AVUTIL_LIBS@ \ - @CAIRO_LIBS@ \ - @JPEG_LIBS@ \ - @SWSCALE_LIBS@ \ +guacenc_LDFLAGS = \ + @AVCODEC_LIBS@ \ + @AVFORMAT_LIBS@ \ + @AVUTIL_LIBS@ \ + @CAIRO_LIBS@ \ + @JPEG_LIBS@ \ + @SWSCALE_LIBS@ \ @WEBP_LIBS@ EXTRA_DIST = \ diff --git a/src/guacenc/ffmpeg-compat.c b/src/guacenc/ffmpeg-compat.c index c1b2c687..6f0e525b 100644 --- a/src/guacenc/ffmpeg-compat.c +++ b/src/guacenc/ffmpeg-compat.c @@ -51,8 +51,41 @@ */ static int guacenc_write_packet(guacenc_video* video, void* data, int size) { - /* Write data, logging any errors */ - if (fwrite(data, 1, size, video->output) == 0) { + int ret; + AVPacket *pkt; + + +#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(54,1,0) + + pkt = malloc(sizeof(AVPacket)); + /* have to create a packet around the encoded data we have */ + av_init_packet(pkt); + + if (video->context->coded_frame->pts != AV_NOPTS_VALUE) { + pkt->pts = av_rescale_q(video->context->coded_frame->pts, + video->context->time_base, + video->output_stream->time_base); + } + if (video->context->coded_frame->key_frame) { + pkt->flags |= AV_PKT_FLAG_KEY; + } + + pkt->data = data; + pkt->size = size; + pkt->stream_index = video->output_stream->index; + ret = av_interleaved_write_frame(video->container_format_context, pkt); + free(pkt); + +#else + /* we know data is already a packet if we're using a newer libavcodec */ + pkt = (AVPacket*) data; + av_packet_rescale_ts(pkt, video->context->time_base, video->output_stream->time_base); + pkt->stream_index = video->output_stream->index; + ret = av_interleaved_write_frame(video->container_format_context, pkt); +#endif + + + if (ret != 0) { guacenc_log(GUAC_LOG_ERROR, "Unable to write frame " "#%" PRId64 ": %s", video->next_pts, strerror(errno)); return -1; @@ -62,8 +95,7 @@ static int guacenc_write_packet(guacenc_video* video, void* data, int size) { guacenc_log(GUAC_LOG_DEBUG, "Frame #%08" PRId64 ": wrote %i bytes", video->next_pts, size); - return 0; - + return ret; } int guacenc_avcodec_encode_video(guacenc_video* video, AVFrame* frame) { @@ -123,7 +155,7 @@ int guacenc_avcodec_encode_video(guacenc_video* video, AVFrame* frame) { /* Write corresponding data to file */ if (got_data) { - guacenc_write_packet(video, packet.data, packet.size); + guacenc_write_packet(video, (void*) &packet, packet.size); av_packet_unref(&packet); } #else @@ -149,7 +181,7 @@ int guacenc_avcodec_encode_video(guacenc_video* video, AVFrame* frame) { got_data = 1; /* Attempt to write data to output file */ - guacenc_write_packet(video, packet.data, packet.size); + guacenc_write_packet(video, (void*) &packet, packet.size); av_packet_unref(&packet); } @@ -165,3 +197,62 @@ int guacenc_avcodec_encode_video(guacenc_video* video, AVFrame* frame) { #endif } +AVCodecContext* guacenc_build_avcodeccontext(AVStream* stream, + AVCodec* codec, + int bitrate, + int width, + int height, + int gop_size, + int qmax, + int qmin, + int pix_fmt, + AVRational time_base) { +#if LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(57, 33, 100) + stream->codec->bit_rate = bitrate; + stream->codec->width = width; + stream->codec->height = height; + stream->codec->gop_size = gop_size; + stream->codec->qmax = qmax; + stream->codec->qmin = qmin; + stream->codec->pix_fmt = pix_fmt; + stream->codec->time_base = time_base; +#if LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(55, 44, 100) + stream->codec->time_base = time_base; +#else + stream->time_base = time_base; +#endif + return stream->codec; +#else + AVCodecContext* context = avcodec_alloc_context3(codec); + if (context) { + context->bit_rate = bitrate; + context->width = width; + context->height = height; + context->gop_size = gop_size; + context->qmax = qmax; + context->qmin = qmin; + context->pix_fmt = pix_fmt; + context->time_base = time_base; + stream->time_base = time_base; + } + return context; +#endif + +} + +int guacenc_open_avcodec(AVCodecContext *avcodec_context, + AVCodec *codec, AVDictionary **options, + AVStream* stream) { + int ret = 0; + ret = avcodec_open2(avcodec_context, codec, options); +#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 33, 100) + int codecpar_ret = 0; + /* copy stream parameters to the muxer */ + codecpar_ret = avcodec_parameters_from_context(stream->codecpar, + avcodec_context); + if (codecpar_ret < 0) { + return codecpar_ret; + } +#endif + return ret; +} diff --git a/src/guacenc/ffmpeg-compat.h b/src/guacenc/ffmpeg-compat.h index 6f26b1d1..b88b493f 100644 --- a/src/guacenc/ffmpeg-compat.h +++ b/src/guacenc/ffmpeg-compat.h @@ -78,5 +78,77 @@ */ int guacenc_avcodec_encode_video(guacenc_video* video, AVFrame* frame); +/** + * Creates and sets up the AVCodecContext for the appropriate version + * of libavformat installed + * + * @param stream + * The open AVStream + * + * @param codec + * The codec used on the AVStream + * + * @param bitrate + * The target bitrate for the encoded video + * + * @param width + * The target width for the encoded video + * + * @param height + * The target width for the encoded video + * + * @param gop_size + * The size of the Group of Pictures + * + * @param qmax + * The max value of the quantizer + * + * @param qmin + * The min value of the quantizer + * + * @param pix_fmt + * The target pixel format for the encoded video + * + * @param time_base + * The target time base for the encoded video + * + */ +AVCodecContext* guacenc_build_avcodeccontext(AVStream* stream, + AVCodec* codec, + int bitrate, + int width, + int height, + int gop_size, + int qmax, + int qmin, + int pix_fmt, + AVRational time_base); + +/** + * A wrapper for avcodec_open2(). Because libavformat ver + * 57.33.100 and greater use stream->codecpar rather than + * stream->codec to handle information to the codec, + * there needs to be an additional step in that version. + * So this wrapper handles that. Otherwise, it's the + * same as avcodec_open2(). + * + * @param avcodec_context The context to initialize. + * @param codec The codec to open this context for. If a non-NULL codec has + * been previously passed to avcodec_alloc_context3() or + * for this context, then this parameter MUST be either NULL or + * equal to the previously passed codec. + * @param options A dictionary filled with AVCodecContext and codec-private + * options. On return this object will be filled with options + * that were not found. + * @param stream The stream for the codec context. + * Only used in libavformat >= 57.33.100. Can be NULL in + * lower versions + * + * @return zero on success, a negative value on error + */ +int guacenc_open_avcodec(AVCodecContext *avcodec_context, + AVCodec *codec, AVDictionary **options, + AVStream* stream); + #endif diff --git a/src/guacenc/guacenc.c b/src/guacenc/guacenc.c index 093623a2..0bd72d5f 100644 --- a/src/guacenc/guacenc.c +++ b/src/guacenc/guacenc.c @@ -25,6 +25,7 @@ #include "parse.h" #include +#include #include #include @@ -80,6 +81,9 @@ int main(int argc, char* argv[]) { avcodec_register_all(); #endif + /* Prepare libavformat */ + av_register_all(); + /* Track number of overall failures */ int total_files = argc - optind; int failures = 0; diff --git a/src/guacenc/video.c b/src/guacenc/video.c index 4592a2bb..9805a83f 100644 --- a/src/guacenc/video.c +++ b/src/guacenc/video.c @@ -25,6 +25,9 @@ #include #include +#ifndef AVFORMAT_AVFORMAT_H +#include +#endif #include #include #include @@ -43,6 +46,21 @@ guacenc_video* guacenc_video_alloc(const char* path, const char* codec_name, int width, int height, int bitrate) { + AVOutputFormat *container_format; + AVFormatContext *container_format_context; + AVStream *video_stream; + int ret; + int failed_header = 0; + + /* allocate the output media context */ + avformat_alloc_output_context2(&container_format_context, NULL, NULL, path); + if (container_format_context == NULL) { + guacenc_log(GUAC_LOG_ERROR, "Failed to determine container from output file name\n"); + goto fail_codec; + } + + container_format = container_format_context->oformat; + /* Pull codec based on name */ AVCodec* codec = avcodec_find_encoder_by_name(codec_name); if (codec == NULL) { @@ -51,25 +69,41 @@ guacenc_video* guacenc_video_alloc(const char* path, const char* codec_name, goto fail_codec; } + /* create stream */ + video_stream = NULL; + video_stream = avformat_new_stream(container_format_context, codec); + if (video_stream == NULL) { + guacenc_log(GUAC_LOG_ERROR, "Could not allocate encoder stream. Cannot continue.\n"); + goto fail_format_context; + } + video_stream->id = container_format_context->nb_streams - 1; + /* Retrieve encoding context */ - AVCodecContext* context = avcodec_alloc_context3(codec); - if (context == NULL) { + AVCodecContext* avcodec_context = + guacenc_build_avcodeccontext(video_stream, + codec, + bitrate, + width, + height, + /*gop size*/ 10, + /*qmax*/ 31, + /*qmin*/ 2, + /*pix fmt*/ AV_PIX_FMT_YUV420P, + /*time base*/ (AVRational) { 1, GUACENC_VIDEO_FRAMERATE }); + + if (avcodec_context == NULL) { guacenc_log(GUAC_LOG_ERROR, "Failed to allocate context for " "codec \"%s\".", codec_name); goto fail_context; } - /* Init context with encoding parameters */ - context->bit_rate = bitrate; - context->width = width; - context->height = height; - context->time_base = (AVRational) { 1, GUACENC_VIDEO_FRAMERATE }; - context->gop_size = 10; - context->max_b_frames = 1; - context->pix_fmt = AV_PIX_FMT_YUV420P; + //if format needs global headers, write them + if (container_format_context->oformat->flags & AVFMT_GLOBALHEADER) { + avcodec_context->flags |= CODEC_FLAG_GLOBAL_HEADER; + } /* Open codec for use */ - if (avcodec_open2(context, codec, NULL) < 0) { + if (guacenc_open_avcodec(avcodec_context, codec, NULL, video_stream) < 0) { guacenc_log(GUAC_LOG_ERROR, "Failed to open codec \"%s\".", codec_name); goto fail_codec_open; } @@ -81,9 +115,9 @@ guacenc_video* guacenc_video_alloc(const char* path, const char* codec_name, } /* Copy necessary data for frame from context */ - frame->format = context->pix_fmt; - frame->width = context->width; - frame->height = context->height; + frame->format = avcodec_context->pix_fmt; + frame->width = avcodec_context->width; + frame->height = avcodec_context->height; /* Allocate actual backing data for frame */ if (av_image_alloc(frame->data, frame->linesize, frame->width, @@ -91,31 +125,32 @@ guacenc_video* guacenc_video_alloc(const char* path, const char* codec_name, goto fail_frame_data; } - /* Open output file */ - int fd = open(path, O_CREAT | O_EXCL | O_WRONLY, S_IRUSR | S_IWUSR); - if (fd == -1) { - guacenc_log(GUAC_LOG_ERROR, "Failed to open output file \"%s\": %s", - path, strerror(errno)); - goto fail_output_fd; + /* Open output file, if the container needs it */ + if (!(container_format->flags & AVFMT_NOFILE)) { + ret = avio_open(&container_format_context->pb, path, AVIO_FLAG_WRITE); + if (ret < 0) { + guacenc_log(GUAC_LOG_ERROR, "Error occurred while opening output file.\n"); + goto fail_output_avio; + } } - /* Create stream for output file */ - FILE* output = fdopen(fd, "wb"); - if (output == NULL) { - guacenc_log(GUAC_LOG_ERROR, "Failed to allocate stream for output " - "file \"%s\": %s", path, strerror(errno)); - goto fail_output_file; + /* write the stream header, if needed */ + ret = avformat_write_header(container_format_context, NULL); + if (ret < 0) { + guacenc_log(GUAC_LOG_ERROR, "Error occurred while writing output file header.\n"); + failed_header = true; } /* Allocate video structure */ guacenc_video* video = malloc(sizeof(guacenc_video)); if (video == NULL) { - goto fail_video; + goto fail_output_file; } /* Init properties of video */ - video->output = output; - video->context = context; + video->output_stream = video_stream; + video->context = avcodec_context; + video->container_format_context = container_format_context; video->next_frame = frame; video->width = width; video->height = height; @@ -125,16 +160,24 @@ guacenc_video* guacenc_video_alloc(const char* path, const char* codec_name, video->last_timestamp = 0; video->next_pts = 0; + if (failed_header) { + guacenc_log(GUAC_LOG_ERROR, "An incompatible codec/container " + "combination was specified. Cannot encode.\n"); + goto fail_output_file; + } + return video; /* Free all allocated data in case of failure */ -fail_video: - fclose(output); fail_output_file: - close(fd); + avio_close(container_format_context->pb); + /* delete the file that was created if it was actually created */ + if (access(path, F_OK) != -1) { + remove(path); + } -fail_output_fd: +fail_output_avio: av_freep(&frame->data[0]); fail_frame_data: @@ -142,7 +185,13 @@ fail_frame_data: fail_frame: fail_codec_open: - avcodec_free_context(&context); + avcodec_free_context(&avcodec_context); + +fail_format_context: + /* failing to write the container implicitly frees the context */ + if (!failed_header) { + avformat_free_context(container_format_context); + } fail_context: fail_codec: @@ -435,26 +484,34 @@ int guacenc_video_free(guacenc_video* video) { /* Write final frame */ guacenc_video_flush_frame(video); - /* Init video packet for final flush of encoded data */ - AVPacket packet; - av_init_packet(&packet); - /* Flush any unwritten frames */ int retval; do { retval = guacenc_video_write_frame(video, NULL); } while (retval > 0); + /* write trailer, if needed */ + if (video->container_format_context != NULL && + video->output_stream != NULL) { + guacenc_log(GUAC_LOG_DEBUG, "Writing trailer: %d\n", + av_write_trailer(video->container_format_context) == 0 ? + "success" : "failure"); + } + /* File is now completely written */ - fclose(video->output); + if (video->container_format_context != NULL) { + avio_close(video->container_format_context->pb); + } /* Free frame encoding data */ av_freep(&video->next_frame->data[0]); av_frame_free(&video->next_frame); /* Clean up encoding context */ - avcodec_close(video->context); - avcodec_free_context(&(video->context)); + if (video->context != NULL) { + avcodec_close(video->context); + avcodec_free_context(&(video->context)); + } free(video); return 0; diff --git a/src/guacenc/video.h b/src/guacenc/video.h index c1d17486..37099b13 100644 --- a/src/guacenc/video.h +++ b/src/guacenc/video.h @@ -26,6 +26,14 @@ #include #include +#ifndef AVCODEC_AVCODEC_H +#include +#endif + +#ifndef AVFORMAT_AVFORMAT_H +#include +#endif + #include #include @@ -42,9 +50,10 @@ typedef struct guacenc_video { /** - * Output file stream. + * AVStream for video output. + * Persists via the AVFormatContext. */ - FILE* output; + AVStream* output_stream; /** * The open encoding context from libavcodec, created for the codec @@ -52,6 +61,12 @@ typedef struct guacenc_video { */ AVCodecContext* context; + /** + * The open format context from libavformat, created for the file + * container specified when this guacenc_video was created. + */ + AVFormatContext* container_format_context; + /** * The width of the video, in pixels. */