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. */