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"])
#
# 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}

View File

@ -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 = \

View File

@ -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;
}

View File

@ -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

View File

@ -25,6 +25,7 @@
#include "parse.h"
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <getopt.h>
#include <stdbool.h>
@ -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;

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
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

View File

@ -25,6 +25,9 @@
#include <cairo/cairo.h>
#include <libavcodec/avcodec.h>
#ifndef AVFORMAT_AVFORMAT_H
#include <libavformat/avformat.h>
#endif
#include <libavutil/common.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
@ -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;

View File

@ -26,6 +26,14 @@
#include <guacamole/timestamp.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 <stdio.h>
@ -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.
*/