diff --git a/src/guacenc/guacenc.c b/src/guacenc/guacenc.c index 7662f91690..c8e0289f89 100644 --- a/src/guacenc/guacenc.c +++ b/src/guacenc/guacenc.c @@ -30,6 +30,7 @@ #include #include #include +#include int main(int argc, char* argv[]) { @@ -37,13 +38,14 @@ int main(int argc, char* argv[]) { /* Load defaults */ bool force = false; + const char* out_file = NULL; int width = GUACENC_DEFAULT_WIDTH; int height = GUACENC_DEFAULT_HEIGHT; int bitrate = GUACENC_DEFAULT_BITRATE; /* Parse arguments */ int opt; - while ((opt = getopt(argc, argv, "s:r:f")) != -1) { + while ((opt = getopt(argc, argv, "s:r:fo:")) != -1) { /* -s: Dimensions (WIDTHxHEIGHT) */ if (opt == 's') { @@ -65,6 +67,10 @@ int main(int argc, char* argv[]) { else if (opt == 'f') force = true; + /* -o: Output file ("-" for STDOUT) */ + else if (opt == 'o') + out_file = optarg; + /* Invalid option */ else { goto invalid_options; @@ -95,6 +101,14 @@ int main(int argc, char* argv[]) { return 0; } + /* A single explicit output cannot receive the encodings of multiple + * input files */ + if (out_file != NULL && total_files != 1) { + guacenc_log(GUAC_LOG_ERROR, "Only one input file may be given if " + "the output is specified with the -o option."); + goto invalid_options; + } + guacenc_log(GUAC_LOG_INFO, "%i input file(s) provided.", total_files); guacenc_log(GUAC_LOG_INFO, "Video will be encoded at %ix%i " @@ -106,15 +120,27 @@ int main(int argc, char* argv[]) { /* Get current filename */ const char* path = argv[i]; - /* Generate output filename */ - char out_path[4096]; - int len = snprintf(out_path, sizeof(out_path), "%s.m4v", path); + /* Use the explicitly-provided output path, if any, streaming to + * STDOUT if "-" was given as the output */ + const char* out_path; + char out_buffer[4096]; + if (out_file != NULL) + out_path = strcmp(out_file, "-") ? out_file : GUACENC_STDOUT_PATH; + + /* Otherwise, generate output filename from input filename */ + else { + + int len = snprintf(out_buffer, sizeof(out_buffer), "%s.m4v", path); + + /* Do not write if filename exceeds maximum length */ + if (len >= sizeof(out_buffer)) { + guacenc_log(GUAC_LOG_ERROR, "Cannot write output file for " + "\"%s\": Name too long", path); + continue; + } + + out_path = out_buffer; - /* Do not write if filename exceeds maximum length */ - if (len >= sizeof(out_path)) { - guacenc_log(GUAC_LOG_ERROR, "Cannot write output file for \"%s\": " - "Name too long", path); - continue; } /* Attempt encoding, log granular success/failure at debug level */ @@ -148,6 +174,7 @@ int main(int argc, char* argv[]) { " [-s WIDTHxHEIGHT]" " [-r BITRATE]" " [-f]" + " [-o OUTPUT]" " [FILE]...\n", argv[0]); return 1; diff --git a/src/guacenc/guacenc.h b/src/guacenc/guacenc.h index ec6d495889..233454b32d 100644 --- a/src/guacenc/guacenc.h +++ b/src/guacenc/guacenc.h @@ -47,5 +47,11 @@ */ #define GUACENC_DEFAULT_LOG_LEVEL GUAC_LOG_INFO +/** + * The libavformat URL of the current process' STDOUT, as used when encoded + * video should be streamed to STDOUT rather than written to a file. + */ +#define GUACENC_STDOUT_PATH "pipe:1" + #endif diff --git a/src/guacenc/man/guacenc.1.in b/src/guacenc/man/guacenc.1.in index bbfa3e1f69..0280d0f4db 100644 --- a/src/guacenc/man/guacenc.1.in +++ b/src/guacenc/man/guacenc.1.in @@ -26,6 +26,7 @@ guacenc \- Guacamole video encoder [\fB-s\fR \fIWIDTH\fRx\fIHEIGHT\fR] [\fB-r\fR \fIBITRATE\fR] [\fB-f\fR] +[\fB-o\fR \fIOUTPUT\fR] [\fIFILE\fR]... . .SH DESCRIPTION @@ -46,6 +47,13 @@ overridden with the \fB-s\fR and \fB-r\fR options respectively. Existing files will not be overwritten; the encoding process for any input file will be aborted if it would result in overwriting an existing file. .P +Alternatively, the \fB-o\fR option may be used to specify the output file +explicitly. In typical UNIX fashion, specifying "-" as the output file will +cause the encoded video to be streamed to STDOUT rather than written to a +file, allowing the video to be piped to another process or streamed as a +download while it is still being encoded. As only a single output may be +specified, only one \fIFILE\fR may be given if \fB-o\fR is used. +.P Guacamole acquires a write lock on recordings as they are being written. By default, .B guacenc @@ -75,6 +83,13 @@ Overrides the default behavior of .B guacenc such that input files will be encoded even if they appear to be recordings of in-progress Guacamole sessions. +.TP +\fB-o\fR \fIOUTPUT\fR +Writes the encoded video to \fIOUTPUT\fR instead of a file named after the +input file. If \fIOUTPUT\fR is "-", the encoded video will be streamed to +STDOUT rather than written to a file; as all log messages are written to +STDERR, they will not interfere with the video stream. Only one \fIFILE\fR may +be given when this option is used. . .SH SEE ALSO .BR guaclog (1) diff --git a/src/guacenc/video.c b/src/guacenc/video.c index 61e1842e1d..898a21ef54 100644 --- a/src/guacenc/video.c +++ b/src/guacenc/video.c @@ -29,6 +29,7 @@ #include #endif #include +#include #include #include #include @@ -41,6 +42,7 @@ #include #include #include +#include #include #include #include @@ -54,8 +56,16 @@ guacenc_video* guacenc_video_alloc(const char* path, const char* codec_name, int ret; int failed_header = 0; + /* The container format cannot be guessed from a pipe URL and must be + * specified explicitly. The "ipod" container is used, matching the + * container that would be guessed from the ".m4v" extension of the output + * files normally produced */ + bool is_pipe = (strncmp(path, "pipe:", 5) == 0); + const char* format_name = is_pipe ? "ipod" : NULL; + /* allocate the output media context */ - avformat_alloc_output_context2(&container_format_context, NULL, NULL, path); + avformat_alloc_output_context2(&container_format_context, NULL, + format_name, path); if (container_format_context == NULL) { guacenc_log(GUAC_LOG_ERROR, "Failed to determine container from output file name"); goto fail_codec; @@ -130,8 +140,17 @@ guacenc_video* guacenc_video_alloc(const char* path, const char* codec_name, } } + /* A pipe cannot be seeked backwards to finalize the container once + * encoding is complete, so fragmented MP4 must be used to produce a + * usable video stream */ + AVDictionary* header_options = NULL; + if (is_pipe) + av_dict_set(&header_options, "movflags", + "frag_keyframe+empty_moov", 0); + /* write the stream header, if needed */ - ret = avformat_write_header(container_format_context, NULL); + ret = avformat_write_header(container_format_context, &header_options); + av_dict_free(&header_options); if (ret < 0) { guacenc_log(GUAC_LOG_ERROR, "Error occurred while writing output file header."); failed_header = true; @@ -163,8 +182,9 @@ guacenc_video* guacenc_video_alloc(const char* path, const char* codec_name, fail_output_file: avio_close(container_format_context->pb); - /* Delete the file that was created if it was actually created */ - if (unlink(path) == -1 && errno != ENOENT) + /* Delete the file that was created if it was actually created (no file + * exists to be deleted if output was written to a pipe) */ + if (!is_pipe && unlink(path) == -1 && errno != ENOENT) guacenc_log(GUAC_LOG_WARNING, "Failed output file \"%s\" could not " "be automatically deleted: %s", path, strerror(errno));