GUACAMOLE-465: Merge produce MPEG-4 output within a proper container.

This commit is contained in:
Virtually Nick 2020-06-24 13:10:32 -04:00 committed by GitHub
commit 614f38767e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 324 additions and 60 deletions

View File

@ -202,6 +202,24 @@ fi
AM_CONDITIONAL([ENABLE_AVCODEC], [test "x${have_libavcodec}" = "xyes"]) 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 # libavutil
# #
@ -998,7 +1016,8 @@ AC_ARG_ENABLE([guacenc],
AM_CONDITIONAL([ENABLE_GUACENC], [test "x${enable_guacenc}" = "xyes" \ AM_CONDITIONAL([ENABLE_GUACENC], [test "x${enable_guacenc}" = "xyes" \
-a "x${have_libavcodec}" = "xyes" \ -a "x${have_libavcodec}" = "xyes" \
-a "x${have_libavutil}" = "xyes" \ -a "x${have_libavutil}" = "xyes" \
-a "x${have_libswscale}" = "xyes"]) -a "x${have_libswscale}" = "xyes" \
-a "x${have_libavformat}" = "xyes"])
# #
# guaclog # guaclog
@ -1091,6 +1110,7 @@ $PACKAGE_NAME version $PACKAGE_VERSION
freerdp2 ............ ${have_freerdp2} freerdp2 ............ ${have_freerdp2}
pango ............... ${have_pango} pango ............... ${have_pango}
libavcodec .......... ${have_libavcodec} libavcodec .......... ${have_libavcodec}
libavformat.......... ${have_libavformat}
libavutil ........... ${have_libavutil} libavutil ........... ${have_libavutil}
libssh2 ............. ${have_libssh2} libssh2 ............. ${have_libssh2}
libssl .............. ${have_ssl} libssl .............. ${have_ssl}

View File

@ -90,6 +90,7 @@ endif
guacenc_CFLAGS = \ guacenc_CFLAGS = \
-Werror -Wall \ -Werror -Wall \
@AVCODEC_CFLAGS@ \ @AVCODEC_CFLAGS@ \
@AVFORMAT_CFLAGS@ \
@AVUTIL_CFLAGS@ \ @AVUTIL_CFLAGS@ \
@LIBGUAC_INCLUDE@ \ @LIBGUAC_INCLUDE@ \
@SWSCALE_CFLAGS@ @SWSCALE_CFLAGS@
@ -99,6 +100,7 @@ guacenc_LDADD = \
guacenc_LDFLAGS = \ guacenc_LDFLAGS = \
@AVCODEC_LIBS@ \ @AVCODEC_LIBS@ \
@AVFORMAT_LIBS@ \
@AVUTIL_LIBS@ \ @AVUTIL_LIBS@ \
@CAIRO_LIBS@ \ @CAIRO_LIBS@ \
@JPEG_LIBS@ \ @JPEG_LIBS@ \

View File

@ -51,8 +51,41 @@
*/ */
static int guacenc_write_packet(guacenc_video* video, void* data, int size) { static int guacenc_write_packet(guacenc_video* video, void* data, int size) {
/* Write data, logging any errors */ int ret;
if (fwrite(data, 1, size, video->output) == 0) {
#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 " guacenc_log(GUAC_LOG_ERROR, "Unable to write frame "
"#%" PRId64 ": %s", video->next_pts, strerror(errno)); "#%" PRId64 ": %s", video->next_pts, strerror(errno));
return -1; 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", guacenc_log(GUAC_LOG_DEBUG, "Frame #%08" PRId64 ": wrote %i bytes",
video->next_pts, size); video->next_pts, size);
return 0; return ret;
} }
int guacenc_avcodec_encode_video(guacenc_video* video, AVFrame* frame) { 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 */ /* For libavcodec < 57.37.100: input/output was not decoupled */
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(57,37,100) #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(57,37,100)
/* Write frame to video */ /* Write frame to video */
int got_data; int got_data;
if (avcodec_encode_video2(video->context, &packet, frame, &got_data) < 0) { 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 */ /* Write corresponding data to file */
if (got_data) { if (got_data) {
guacenc_write_packet(video, packet.data, packet.size); guacenc_write_packet(video, (void*) &packet, packet.size);
av_packet_unref(&packet); av_packet_unref(&packet);
} }
#else #else
/* Write frame to video */ /* Write frame to video */
int result = avcodec_send_frame(video->context, frame); 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; got_data = 1;
/* Attempt to write data to output file */ /* 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); av_packet_unref(&packet);
} }
#endif #endif
/* Frame may have been queued for later writing / reordering */ /* Frame may have been queued for later writing / reordering */
@ -165,3 +201,54 @@ int guacenc_avcodec_encode_video(guacenc_video* video, AVFrame* frame) {
#endif #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;
}

View File

@ -52,6 +52,16 @@
#define av_packet_unref av_free_packet #define av_packet_unref av_free_packet
#endif #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_* */ /* For libavutil < 51.42.0: AV_PIX_FMT_* was PIX_FMT_* */
#if LIBAVUTIL_VERSION_INT < AV_VERSION_INT(51,42,0) #if LIBAVUTIL_VERSION_INT < AV_VERSION_INT(51,42,0)
#define AV_PIX_FMT_RGB32 PIX_FMT_RGB32 #define AV_PIX_FMT_RGB32 PIX_FMT_RGB32
@ -78,5 +88,78 @@
*/ */
int guacenc_avcodec_encode_video(guacenc_video* video, AVFrame* frame); 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 #endif

View File

@ -25,6 +25,7 @@
#include "parse.h" #include "parse.h"
#include <libavcodec/avcodec.h> #include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <getopt.h> #include <getopt.h>
#include <stdbool.h> #include <stdbool.h>
@ -80,6 +81,10 @@ int main(int argc, char* argv[]) {
avcodec_register_all(); avcodec_register_all();
#endif #endif
#if LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(58, 9, 100)
av_register_all();
#endif
/* Track number of overall failures */ /* Track number of overall failures */
int total_files = argc - optind; int total_files = argc - optind;
int failures = 0; int failures = 0;

View File

@ -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 its input from files instead of a network connection, and renders directly to
video instead of to the user's screen. video instead of to the user's screen.
.P .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 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 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 with a bitrate of \fI2000000\fR bits per second (2 Mbps). These defaults can be

View File

@ -25,6 +25,9 @@
#include <cairo/cairo.h> #include <cairo/cairo.h>
#include <libavcodec/avcodec.h> #include <libavcodec/avcodec.h>
#ifndef AVFORMAT_AVFORMAT_H
#include <libavformat/avformat.h>
#endif
#include <libavutil/common.h> #include <libavutil/common.h>
#include <libavutil/imgutils.h> #include <libavutil/imgutils.h>
#include <libswscale/swscale.h> #include <libswscale/swscale.h>
@ -43,6 +46,21 @@
guacenc_video* guacenc_video_alloc(const char* path, const char* codec_name, guacenc_video* guacenc_video_alloc(const char* path, const char* codec_name,
int width, int height, int bitrate) { 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 */ /* Pull codec based on name */
AVCodec* codec = avcodec_find_encoder_by_name(codec_name); AVCodec* codec = avcodec_find_encoder_by_name(codec_name);
if (codec == NULL) { if (codec == NULL) {
@ -51,25 +69,35 @@ guacenc_video* guacenc_video_alloc(const char* path, const char* codec_name,
goto fail_codec; 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 */ /* Retrieve encoding context */
AVCodecContext* context = avcodec_alloc_context3(codec); AVCodecContext* avcodec_context =
if (context == NULL) { 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 " guacenc_log(GUAC_LOG_ERROR, "Failed to allocate context for "
"codec \"%s\".", codec_name); "codec \"%s\".", codec_name);
goto fail_context; goto fail_context;
} }
/* Init context with encoding parameters */ /* If format needs global headers, write them */
context->bit_rate = bitrate; if (container_format_context->oformat->flags & AVFMT_GLOBALHEADER) {
context->width = width; avcodec_context->flags |= GUACENC_FLAG_GLOBAL_HEADER;
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;
/* Open codec for use */ /* 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); guacenc_log(GUAC_LOG_ERROR, "Failed to open codec \"%s\".", codec_name);
goto fail_codec_open; 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 */ /* Copy necessary data for frame from context */
frame->format = context->pix_fmt; frame->format = avcodec_context->pix_fmt;
frame->width = context->width; frame->width = avcodec_context->width;
frame->height = context->height; frame->height = avcodec_context->height;
/* Allocate actual backing data for frame */ /* Allocate actual backing data for frame */
if (av_image_alloc(frame->data, frame->linesize, frame->width, 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; goto fail_frame_data;
} }
/* Open output file */ /* Open output file, if the container needs it */
int fd = open(path, O_CREAT | O_EXCL | O_WRONLY, S_IRUSR | S_IWUSR); if (!(container_format->flags & AVFMT_NOFILE)) {
if (fd == -1) { ret = avio_open(&container_format_context->pb, path, AVIO_FLAG_WRITE);
guacenc_log(GUAC_LOG_ERROR, "Failed to open output file \"%s\": %s", if (ret < 0) {
path, strerror(errno)); guacenc_log(GUAC_LOG_ERROR, "Error occurred while opening output file.\n");
goto fail_output_fd; goto fail_output_avio;
}
} }
/* Create stream for output file */ /* write the stream header, if needed */
FILE* output = fdopen(fd, "wb"); ret = avformat_write_header(container_format_context, NULL);
if (output == NULL) { if (ret < 0) {
guacenc_log(GUAC_LOG_ERROR, "Failed to allocate stream for output " guacenc_log(GUAC_LOG_ERROR, "Error occurred while writing output file header.\n");
"file \"%s\": %s", path, strerror(errno)); failed_header = true;
goto fail_output_file;
} }
/* Allocate video structure */ /* Allocate video structure */
guacenc_video* video = malloc(sizeof(guacenc_video)); guacenc_video* video = malloc(sizeof(guacenc_video));
if (video == NULL) { if (video == NULL) {
goto fail_video; goto fail_output_file;
} }
/* Init properties of video */ /* Init properties of video */
video->output = output; video->output_stream = video_stream;
video->context = context; video->context = avcodec_context;
video->container_format_context = container_format_context;
video->next_frame = frame; video->next_frame = frame;
video->width = width; video->width = width;
video->height = height; video->height = height;
@ -125,16 +154,24 @@ guacenc_video* guacenc_video_alloc(const char* path, const char* codec_name,
video->last_timestamp = 0; video->last_timestamp = 0;
video->next_pts = 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; return video;
/* Free all allocated data in case of failure */ /* Free all allocated data in case of failure */
fail_video:
fclose(output);
fail_output_file: 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]); av_freep(&frame->data[0]);
fail_frame_data: fail_frame_data:
@ -142,7 +179,13 @@ fail_frame_data:
fail_frame: fail_frame:
fail_codec_open: 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_context:
fail_codec: fail_codec:
@ -435,26 +478,34 @@ int guacenc_video_free(guacenc_video* video) {
/* Write final frame */ /* Write final frame */
guacenc_video_flush_frame(video); guacenc_video_flush_frame(video);
/* Init video packet for final flush of encoded data */
AVPacket packet;
av_init_packet(&packet);
/* Flush any unwritten frames */ /* Flush any unwritten frames */
int retval; int retval;
do { do {
retval = guacenc_video_write_frame(video, NULL); retval = guacenc_video_write_frame(video, NULL);
} while (retval > 0); } 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 */ /* 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 */ /* Free frame encoding data */
av_freep(&video->next_frame->data[0]); av_freep(&video->next_frame->data[0]);
av_frame_free(&video->next_frame); av_frame_free(&video->next_frame);
/* Clean up encoding context */ /* Clean up encoding context */
if (video->context != NULL) {
avcodec_close(video->context); avcodec_close(video->context);
avcodec_free_context(&(video->context)); avcodec_free_context(&(video->context));
}
free(video); free(video);
return 0; return 0;

View File

@ -26,6 +26,14 @@
#include <guacamole/timestamp.h> #include <guacamole/timestamp.h>
#include <libavcodec/avcodec.h> #include <libavcodec/avcodec.h>
#ifndef AVCODEC_AVCODEC_H
#include <libavcodec/avcodec.h>
#endif
#ifndef AVFORMAT_AVFORMAT_H
#include <libavformat/avformat.h>
#endif
#include <stdint.h> #include <stdint.h>
#include <stdio.h> #include <stdio.h>
@ -42,9 +50,11 @@
typedef struct guacenc_video { 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 * The open encoding context from libavcodec, created for the codec
@ -52,6 +62,12 @@ typedef struct guacenc_video {
*/ */
AVCodecContext* context; 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. * The width of the video, in pixels.
*/ */