blob: 4592a2bb777eef05f500cbaf8813fee9b7dffa6e [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#include "config.h"
#include "buffer.h"
#include "ffmpeg-compat.h"
#include "log.h"
#include "video.h"
#include <cairo/cairo.h>
#include <libavcodec/avcodec.h>
#include <libavutil/common.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
#include <guacamole/client.h>
#include <guacamole/timestamp.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <assert.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
guacenc_video* guacenc_video_alloc(const char* path, const char* codec_name,
int width, int height, int bitrate) {
/* Pull codec based on name */
AVCodec* codec = avcodec_find_encoder_by_name(codec_name);
if (codec == NULL) {
guacenc_log(GUAC_LOG_ERROR, "Failed to locate codec \"%s\".",
codec_name);
goto fail_codec;
}
/* Retrieve encoding context */
AVCodecContext* context = avcodec_alloc_context3(codec);
if (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;
/* Open codec for use */
if (avcodec_open2(context, codec, NULL) < 0) {
guacenc_log(GUAC_LOG_ERROR, "Failed to open codec \"%s\".", codec_name);
goto fail_codec_open;
}
/* Allocate corresponding frame */
AVFrame* frame = av_frame_alloc();
if (frame == NULL) {
goto fail_frame;
}
/* Copy necessary data for frame from context */
frame->format = context->pix_fmt;
frame->width = context->width;
frame->height = context->height;
/* Allocate actual backing data for frame */
if (av_image_alloc(frame->data, frame->linesize, frame->width,
frame->height, frame->format, 32) < 0) {
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;
}
/* 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;
}
/* Allocate video structure */
guacenc_video* video = malloc(sizeof(guacenc_video));
if (video == NULL) {
goto fail_video;
}
/* Init properties of video */
video->output = output;
video->context = context;
video->next_frame = frame;
video->width = width;
video->height = height;
video->bitrate = bitrate;
/* No frames have been written or prepared yet */
video->last_timestamp = 0;
video->next_pts = 0;
return video;
/* Free all allocated data in case of failure */
fail_video:
fclose(output);
fail_output_file:
close(fd);
fail_output_fd:
av_freep(&frame->data[0]);
fail_frame_data:
av_frame_free(&frame);
fail_frame:
fail_codec_open:
avcodec_free_context(&context);
fail_context:
fail_codec:
return NULL;
}
/**
* Flushes the specied frame as a new frame of video, updating the internal
* video timestamp by one frame's worth of time. The pts member of the given
* frame structure will be updated with the current presentation timestamp of
* the video. If pending frames of the video are being flushed, the given frame
* may be NULL (as required by avcodec_encode_video2()).
*
* @param video
* The video to write the given frame to.
*
* @param frame
* The frame to write to the video, or NULL if previously-written frames
* are being flushed.
*
* @return
* A positive value if the frame was successfully written, zero if the
* frame has been saved for later writing / reordering, negative if an
* error occurs.
*/
static int guacenc_video_write_frame(guacenc_video* video, AVFrame* frame) {
/* Set timestamp of frame, if frame given */
if (frame != NULL)
frame->pts = video->next_pts;
/* Write frame to video */
int got_data = guacenc_avcodec_encode_video(video, frame);
if (got_data < 0)
return -1;
/* Update presentation timestamp for next frame */
video->next_pts++;
/* Write was successful */
return got_data;
}
/**
* Flushes the frame previously specified by guacenc_video_prepare_frame() as a
* new frame of video, updating the internal video timestamp by one frame's
* worth of time.
*
* @param video
* The video to flush.
*
* @return
* Zero if flushing was successful, non-zero if an error occurs.
*/
static int guacenc_video_flush_frame(guacenc_video* video) {
/* Write frame to video */
return guacenc_video_write_frame(video, video->next_frame) < 0;
}
int guacenc_video_advance_timeline(guacenc_video* video,
guac_timestamp timestamp) {
guac_timestamp next_timestamp = timestamp;
/* Flush frames as necessary if previously updated */
if (video->last_timestamp != 0) {
/* Calculate the number of frames that should have been written */
int elapsed = (timestamp - video->last_timestamp)
* GUACENC_VIDEO_FRAMERATE / 1000;
/* Keep previous timestamp if insufficient time has elapsed */
if (elapsed == 0)
return 0;
/* Use frame time as last_timestamp */
next_timestamp = video->last_timestamp
+ elapsed * 1000 / GUACENC_VIDEO_FRAMERATE;
/* Flush frames to bring timeline in sync, duplicating if necessary */
do {
if (guacenc_video_flush_frame(video)) {
guacenc_log(GUAC_LOG_ERROR, "Unable to flush frame to video "
"stream.");
return 1;
}
} while (--elapsed != 0);
}
/* Update timestamp */
video->last_timestamp = next_timestamp;
return 0;
}
/**
* Converts the given Guacamole video encoder buffer to a frame in the format
* required by libavcodec / libswscale. Black margins of the specified sizes
* will be added. No scaling is performed; the image data is copied verbatim.
*
* @param buffer
* The guacenc_buffer to copy as a new AVFrame.
*
* @param lsize
* The size of the letterboxes to add, in pixels. Letterboxes are the
* horizontal black boxes added to images which are scaled down to fit the
* destination because they are too wide (the width is scaled to exactly
* fit the destination, resulting in extra space at the top and bottom).
*
* @param psize
* The size of the pillarboxes to add, in pixels. Pillarboxes are the
* vertical black boxes added to images which are scaled down to fit the
* destination because they are too tall (the height is scaled to exactly
* fit the destination, resulting in extra space on the sides).
*
* @return
* A pointer to a newly-allocated AVFrame containing exactly the same image
* data as the given buffer. The image data within the frame and the frame
* itself must be manually freed later.
*/
static AVFrame* guacenc_video_frame_convert(guacenc_buffer* buffer, int lsize,
int psize) {
/* Init size of left/right pillarboxes */
int left = psize;
int right = psize;
/* Init size of top/bottom letterboxes */
int top = lsize;
int bottom = lsize;
/* Prepare source frame for buffer */
AVFrame* frame = av_frame_alloc();
if (frame == NULL)
return NULL;
/* Copy buffer properties to frame */
frame->format = AV_PIX_FMT_RGB32;
frame->width = buffer->width + left + right;
frame->height = buffer->height + top + bottom;
/* Allocate actual backing data for frame */
if (av_image_alloc(frame->data, frame->linesize, frame->width,
frame->height, frame->format, 32) < 0) {
av_frame_free(&frame);
return NULL;
}
/* Flush any pending operations */
cairo_surface_flush(buffer->surface);
/* Get pointer to source image data */
unsigned char* src_data = buffer->image;
int src_stride = buffer->stride;
/* Get pointer to destination image data */
unsigned char* dst_data = frame->data[0];
int dst_stride = frame->linesize[0];
/* Get source/destination dimensions */
int width = buffer->width;
int height = buffer->height;
/* Source buffer is guaranteed to fit within destination buffer */
assert(width <= frame->width);
assert(height <= frame->height);
/* Add top margin */
while (top > 0) {
memset(dst_data, 0, frame->width * 4);
dst_data += dst_stride;
top--;
}
/* Copy all data from source buffer to destination frame */
while (height > 0) {
/* Calculate size of margin and data regions */
int left_size = left * 4;
int data_size = width * 4;
int right_size = right * 4;
/* Add left margin */
memset(dst_data, 0, left_size);
/* Copy data */
memcpy(dst_data + left_size, src_data, data_size);
/* Add right margin */
memset(dst_data + left_size + data_size, 0, right_size);
dst_data += dst_stride;
src_data += src_stride;
height--;
}
/* Add bottom margin */
while (bottom > 0) {
memset(dst_data, 0, frame->width * 4);
dst_data += dst_stride;
bottom--;
}
/* Frame converted */
return frame;
}
void guacenc_video_prepare_frame(guacenc_video* video, guacenc_buffer* buffer) {
int lsize;
int psize;
/* Ignore NULL buffers */
if (buffer == NULL || buffer->surface == NULL)
return;
/* Obtain destination frame */
AVFrame* dst = video->next_frame;
/* Determine width of image if height is scaled to match destination */
int scaled_width = buffer->width * dst->height / buffer->height;
/* Determine height of image if width is scaled to match destination */
int scaled_height = buffer->height * dst->width / buffer->width;
/* If height-based scaling results in a fit width, add pillarboxes */
if (scaled_width <= dst->width) {
lsize = 0;
psize = (dst->width - scaled_width)
* buffer->height / dst->height / 2;
}
/* If width-based scaling results in a fit width, add letterboxes */
else {
assert(scaled_height <= dst->height);
psize = 0;
lsize = (dst->height - scaled_height)
* buffer->width / dst->width / 2;
}
/* Prepare source frame for buffer */
AVFrame* src = guacenc_video_frame_convert(buffer, lsize, psize);
if (src == NULL) {
guacenc_log(GUAC_LOG_WARNING, "Failed to allocate source frame. "
"Frame dropped.");
return;
}
/* Prepare scaling context */
struct SwsContext* sws = sws_getContext(src->width, src->height,
AV_PIX_FMT_RGB32, dst->width, dst->height, AV_PIX_FMT_YUV420P,
SWS_BICUBIC, NULL, NULL, NULL);
/* Abort if scaling context could not be created */
if (sws == NULL) {
guacenc_log(GUAC_LOG_WARNING, "Failed to allocate software scaling "
"context. Frame dropped.");
av_freep(&src->data[0]);
av_frame_free(&src);
return;
}
/* Apply scaling, copying the source frame to the destination */
sws_scale(sws, (const uint8_t* const*) src->data, src->linesize,
0, src->height, dst->data, dst->linesize);
/* Free scaling context */
sws_freeContext(sws);
/* Free source frame */
av_freep(&src->data[0]);
av_frame_free(&src);
}
int guacenc_video_free(guacenc_video* video) {
/* Ignore NULL video */
if (video == NULL)
return 0;
/* 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);
/* File is now completely written */
fclose(video->output);
/* 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));
free(video);
return 0;
}