diff --git a/configure.ac b/configure.ac index 2f8145a3..1873a6fb 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 # @@ -995,10 +1013,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 @@ -1091,6 +1110,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..db6bec63 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; + +#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(54,1,0) + + AVPacket pkt; + + /* 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); + +#else + + /* We know data is already a packet if we're using a newer libavcodec */ + AVPacket* 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) { @@ -113,6 +145,7 @@ int guacenc_avcodec_encode_video(guacenc_video* video, AVFrame* frame) { /* For libavcodec < 57.37.100: input/output was not decoupled */ #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(57,37,100) + /* Write frame to video */ int got_data; if (avcodec_encode_video2(video->context, &packet, frame, &got_data) < 0) { @@ -123,10 +156,12 @@ 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 + /* Write frame to video */ int result = avcodec_send_frame(video->context, frame); @@ -149,10 +184,11 @@ 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); } + #endif /* Frame may have been queued for later writing / reordering */ @@ -165,3 +201,54 @@ 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->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 = avcodec_open2(avcodec_context, codec, options); + +#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 33, 100) + /* Copy stream parameters to the muxer */ + int 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..a5cfb2f5 100644 --- a/src/guacenc/ffmpeg-compat.h +++ b/src/guacenc/ffmpeg-compat.h @@ -52,6 +52,16 @@ #define av_packet_unref av_free_packet #endif +/* For libavcodec <= 56.41.100: Global header flag didn't have AV_ prefix. + * Guacenc defines its own flag here to avoid conflicts with libavcodec + * macros. + */ +#if LIBAVCODEC_VERSION_INT <= AV_VERSION_INT(56,41,100) +#define GUACENC_FLAG_GLOBAL_HEADER CODEC_FLAG_GLOBAL_HEADER +#else +#define GUACENC_FLAG_GLOBAL_HEADER AV_CODEC_FLAG_GLOBAL_HEADER +#endif + /* For libavutil < 51.42.0: AV_PIX_FMT_* was PIX_FMT_* */ #if LIBAVUTIL_VERSION_INT < AV_VERSION_INT(51,42,0) #define AV_PIX_FMT_RGB32 PIX_FMT_RGB32 @@ -78,5 +88,78 @@ */ int guacenc_avcodec_encode_video(guacenc_video* video, AVFrame* frame); +/** + * Creates and sets up the AVCodecContext for the appropriate version of + * libavformat installed. The AVCodecContext will be built, but the AVStream + * will also be affected by having its time_base field set to the value passed + * into this function. + * + * @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 height 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. + * + * @return + * The pointer to the configured AVCodecContext. + * + */ +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. + * + * @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..7662f916 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,10 @@ int main(int argc, char* argv[]) { avcodec_register_all(); #endif +#if LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(58, 9, 100) + av_register_all(); +#endif + /* Track number of overall failures */ int total_files = argc - optind; int failures = 0; diff --git a/src/guacenc/man/guacenc.1.in b/src/guacenc/man/guacenc.1.in index 4f007875..bbfa3e1f 100644 --- a/src/guacenc/man/guacenc.1.in +++ b/src/guacenc/man/guacenc.1.in @@ -38,7 +38,7 @@ is essentially an implementation of a Guacamole client which accepts its input from files instead of a network connection, and renders directly to video instead of to the user's screen. .P -Each \fIFILE\fR specified will be encoded as a raw MPEG-4 video stream to a new +Each \fIFILE\fR specified will be encoded as MPEG-4 video to a new file named \fIFILE\fR.m4v, encoded according to the other options specified. By default, the output video will be \fI640\fRx\fI480\fR pixels, and will be saved with a bitrate of \fI2000000\fR bits per second (2 Mbps). These defaults can be diff --git a/src/guacenc/video.c b/src/guacenc/video.c index 4592a2bb..2dd41f16 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,35 @@ 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 |= GUACENC_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 +109,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 +119,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 +154,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 +179,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 +478,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..ea0c1615 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,11 @@ typedef struct guacenc_video { /** - * Output file stream. + * AVStream for video output. + * Frames sent to this stream are written into + * the output file in the specified container format. */ - FILE* output; + AVStream* output_stream; /** * The open encoding context from libavcodec, created for the codec @@ -52,6 +62,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. */