cscg24-guacamole

CSCG 2024 Challenge 'Guacamole Mashup'
git clone https://git.sinitax.com/sinitax/cscg24-guacamole
Log | Files | Refs | sfeed.txt

video.c (15853B)


      1/*
      2 * Licensed to the Apache Software Foundation (ASF) under one
      3 * or more contributor license agreements.  See the NOTICE file
      4 * distributed with this work for additional information
      5 * regarding copyright ownership.  The ASF licenses this file
      6 * to you under the Apache License, Version 2.0 (the
      7 * "License"); you may not use this file except in compliance
      8 * with the License.  You may obtain a copy of the License at
      9 *
     10 *   http://www.apache.org/licenses/LICENSE-2.0
     11 *
     12 * Unless required by applicable law or agreed to in writing,
     13 * software distributed under the License is distributed on an
     14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
     15 * KIND, either express or implied.  See the License for the
     16 * specific language governing permissions and limitations
     17 * under the License.
     18 */
     19
     20#include "config.h"
     21#include "buffer.h"
     22#include "ffmpeg-compat.h"
     23#include "log.h"
     24#include "video.h"
     25
     26#include <cairo/cairo.h>
     27#include <libavcodec/avcodec.h>
     28#ifndef AVFORMAT_AVFORMAT_H
     29#include <libavformat/avformat.h>
     30#endif
     31#include <libavutil/common.h>
     32#include <libavutil/imgutils.h>
     33#include <libswscale/swscale.h>
     34#include <guacamole/client.h>
     35#include <guacamole/mem.h>
     36#include <guacamole/timestamp.h>
     37
     38#include <sys/types.h>
     39#include <sys/stat.h>
     40#include <assert.h>
     41#include <errno.h>
     42#include <fcntl.h>
     43#include <inttypes.h>
     44#include <stdlib.h>
     45#include <string.h>
     46#include <unistd.h>
     47
     48guacenc_video* guacenc_video_alloc(const char* path, const char* codec_name,
     49        int width, int height, int bitrate) {
     50
     51    AVOutputFormat *container_format;
     52    AVFormatContext *container_format_context;
     53    AVStream *video_stream;
     54    int ret;
     55    int failed_header = 0;
     56
     57    /* allocate the output media context */
     58    avformat_alloc_output_context2(&container_format_context, NULL, NULL, path);
     59    if (container_format_context == NULL) {
     60        guacenc_log(GUAC_LOG_ERROR, "Failed to determine container from output file name");
     61        goto fail_codec;
     62    }
     63
     64    container_format = (AVOutputFormat *) container_format_context->oformat;
     65
     66    /* Pull codec based on name */
     67    AVCodec* codec = (AVCodec *)avcodec_find_encoder_by_name(codec_name);
     68    if (codec == NULL) {
     69        guacenc_log(GUAC_LOG_ERROR, "Failed to locate codec \"%s\".",
     70                codec_name);
     71        goto fail_codec;
     72    }
     73
     74    /* create stream */
     75    video_stream = NULL;
     76    video_stream = avformat_new_stream(container_format_context, codec);
     77    if (video_stream == NULL) {
     78        guacenc_log(GUAC_LOG_ERROR, "Could not allocate encoder stream. Cannot continue.");
     79        goto fail_format_context;
     80    }
     81    video_stream->id = container_format_context->nb_streams - 1;
     82
     83    /* Retrieve encoding context */
     84    AVCodecContext* avcodec_context =
     85            guacenc_build_avcodeccontext(video_stream, codec, bitrate, width,
     86                    height, /*gop size*/ 10, /*qmax*/ 31, /*qmin*/ 2,
     87                    /*pix fmt*/ AV_PIX_FMT_YUV420P,
     88                    /*time base*/ (AVRational) { 1, GUACENC_VIDEO_FRAMERATE });
     89
     90    if (avcodec_context == NULL) {
     91        guacenc_log(GUAC_LOG_ERROR, "Failed to allocate context for "
     92                "codec \"%s\".", codec_name);
     93        goto fail_context;
     94    }
     95
     96    /* If format needs global headers, write them */
     97    if (container_format_context->oformat->flags & AVFMT_GLOBALHEADER) {
     98        avcodec_context->flags |= GUACENC_FLAG_GLOBAL_HEADER;
     99    }
    100
    101    /* Open codec for use */
    102    if (guacenc_open_avcodec(avcodec_context, codec, NULL, video_stream) < 0) {
    103        guacenc_log(GUAC_LOG_ERROR, "Failed to open codec \"%s\".", codec_name);
    104        goto fail_codec_open;
    105    }
    106
    107    /* Allocate corresponding frame */
    108    AVFrame* frame = av_frame_alloc();
    109    if (frame == NULL) {
    110        goto fail_frame;
    111    }
    112
    113    /* Copy necessary data for frame from context */
    114    frame->format = avcodec_context->pix_fmt;
    115    frame->width = avcodec_context->width;
    116    frame->height = avcodec_context->height;
    117
    118    /* Allocate actual backing data for frame */
    119    if (av_image_alloc(frame->data, frame->linesize, frame->width,
    120                frame->height, frame->format, 32) < 0) {
    121        goto fail_frame_data;
    122    }
    123
    124    /* Open output file, if the container needs it */
    125    if (!(container_format->flags & AVFMT_NOFILE)) {
    126        ret = avio_open(&container_format_context->pb, path, AVIO_FLAG_WRITE);
    127        if (ret < 0) {
    128            guacenc_log(GUAC_LOG_ERROR, "Error occurred while opening output file.");
    129            goto fail_output_avio;
    130        }
    131    }
    132
    133    /* write the stream header, if needed */
    134    ret = avformat_write_header(container_format_context, NULL);
    135    if (ret < 0) {
    136        guacenc_log(GUAC_LOG_ERROR, "Error occurred while writing output file header.");
    137        failed_header = true;
    138        goto fail_output_file;
    139    }
    140
    141    /* Allocate video structure */
    142    guacenc_video* video = guac_mem_alloc(sizeof(guacenc_video));
    143    if (video == NULL)
    144        goto fail_alloc_video;
    145
    146    /* Init properties of video */
    147    video->output_stream = video_stream;
    148    video->context = avcodec_context;
    149    video->container_format_context = container_format_context;
    150    video->next_frame = frame;
    151    video->width = width;
    152    video->height = height;
    153    video->bitrate = bitrate;
    154
    155    /* No frames have been written or prepared yet */
    156    video->last_timestamp = 0;
    157    video->next_pts = 0;
    158
    159    return video;
    160
    161    /* Free all allocated data in case of failure */
    162fail_alloc_video:
    163fail_output_file:
    164    avio_close(container_format_context->pb);
    165
    166    /* Delete the file that was created if it was actually created */
    167    if (unlink(path) == -1 && errno != ENOENT)
    168        guacenc_log(GUAC_LOG_WARNING, "Failed output file \"%s\" could not "
    169                "be automatically deleted: %s", path, strerror(errno));
    170
    171fail_output_avio:
    172    av_freep(&frame->data[0]);
    173
    174fail_frame_data:
    175    av_frame_free(&frame);
    176
    177fail_frame:
    178fail_codec_open:
    179    avcodec_free_context(&avcodec_context);
    180
    181fail_format_context:
    182    /* failing to write the container implicitly frees the context */
    183    if (!failed_header) {
    184        avformat_free_context(container_format_context);
    185    }
    186
    187fail_context:
    188fail_codec:
    189    return NULL;
    190
    191}
    192
    193/**
    194 * Flushes the specied frame as a new frame of video, updating the internal
    195 * video timestamp by one frame's worth of time. The pts member of the given
    196 * frame structure will be updated with the current presentation timestamp of
    197 * the video. If pending frames of the video are being flushed, the given frame
    198 * may be NULL (as required by avcodec_encode_video2()).
    199 *
    200 * @param video
    201 *     The video to write the given frame to.
    202 *
    203 * @param frame
    204 *     The frame to write to the video, or NULL if previously-written frames
    205 *     are being flushed.
    206 *
    207 * @return
    208 *     A positive value if the frame was successfully written, zero if the
    209 *     frame has been saved for later writing / reordering, negative if an
    210 *     error occurs.
    211 */
    212static int guacenc_video_write_frame(guacenc_video* video, AVFrame* frame) {
    213
    214    /* Set timestamp of frame, if frame given */
    215    if (frame != NULL)
    216        frame->pts = video->next_pts;
    217
    218    /* Write frame to video */
    219    int got_data = guacenc_avcodec_encode_video(video, frame);
    220    if (got_data < 0)
    221        return -1;
    222
    223    /* Update presentation timestamp for next frame */
    224    video->next_pts++;
    225
    226    /* Write was successful */
    227    return got_data;
    228
    229}
    230
    231/**
    232 * Flushes the frame previously specified by guacenc_video_prepare_frame() as a
    233 * new frame of video, updating the internal video timestamp by one frame's
    234 * worth of time.
    235 *
    236 * @param video
    237 *     The video to flush.
    238 *
    239 * @return
    240 *     Zero if flushing was successful, non-zero if an error occurs.
    241 */
    242static int guacenc_video_flush_frame(guacenc_video* video) {
    243
    244    /* Write frame to video */
    245    return guacenc_video_write_frame(video, video->next_frame) < 0;
    246
    247}
    248
    249int guacenc_video_advance_timeline(guacenc_video* video,
    250        guac_timestamp timestamp) {
    251
    252    guac_timestamp next_timestamp = timestamp;
    253
    254    /* Flush frames as necessary if previously updated */
    255    if (video->last_timestamp != 0) {
    256
    257        /* Calculate the number of frames that should have been written */
    258        int elapsed = (timestamp - video->last_timestamp)
    259                    * GUACENC_VIDEO_FRAMERATE / 1000;
    260
    261        /* Keep previous timestamp if insufficient time has elapsed */
    262        if (elapsed == 0)
    263            return 0;
    264
    265        /* Use frame time as last_timestamp */
    266        next_timestamp = video->last_timestamp
    267                        + elapsed * 1000 / GUACENC_VIDEO_FRAMERATE;
    268
    269        /* Flush frames to bring timeline in sync, duplicating if necessary */
    270        do {
    271            if (guacenc_video_flush_frame(video)) {
    272                guacenc_log(GUAC_LOG_ERROR, "Unable to flush frame to video "
    273                        "stream.");
    274                return 1;
    275            }
    276        } while (--elapsed != 0);
    277
    278    }
    279
    280    /* Update timestamp */
    281    video->last_timestamp = next_timestamp;
    282    return 0;
    283
    284}
    285
    286/**
    287 * Converts the given Guacamole video encoder buffer to a frame in the format
    288 * required by libavcodec / libswscale. Black margins of the specified sizes
    289 * will be added. No scaling is performed; the image data is copied verbatim.
    290 *
    291 * @param buffer
    292 *     The guacenc_buffer to copy as a new AVFrame.
    293 *
    294 * @param lsize
    295 *     The size of the letterboxes to add, in pixels. Letterboxes are the
    296 *     horizontal black boxes added to images which are scaled down to fit the
    297 *     destination because they are too wide (the width is scaled to exactly
    298 *     fit the destination, resulting in extra space at the top and bottom).
    299 *
    300 * @param psize
    301 *     The size of the pillarboxes to add, in pixels. Pillarboxes are the
    302 *     vertical black boxes added to images which are scaled down to fit the
    303 *     destination because they are too tall (the height is scaled to exactly
    304 *     fit the destination, resulting in extra space on the sides).
    305 *
    306 * @return
    307 *     A pointer to a newly-allocated AVFrame containing exactly the same image
    308 *     data as the given buffer. The image data within the frame and the frame
    309 *     itself must be manually freed later.
    310 */
    311static AVFrame* guacenc_video_frame_convert(guacenc_buffer* buffer, int lsize,
    312        int psize) {
    313
    314    /* Init size of left/right pillarboxes */
    315    int left = psize;
    316    int right = psize;
    317
    318    /* Init size of top/bottom letterboxes */
    319    int top = lsize;
    320    int bottom = lsize;
    321
    322    /* Prepare source frame for buffer */
    323    AVFrame* frame = av_frame_alloc();
    324    if (frame == NULL)
    325        return NULL;
    326
    327    /* Copy buffer properties to frame */
    328    frame->format = AV_PIX_FMT_RGB32;
    329    frame->width = buffer->width + left + right;
    330    frame->height = buffer->height + top + bottom;
    331
    332    /* Allocate actual backing data for frame */
    333    if (av_image_alloc(frame->data, frame->linesize, frame->width,
    334                frame->height, frame->format, 32) < 0) {
    335        av_frame_free(&frame);
    336        return NULL;
    337    }
    338
    339    /* Flush any pending operations */
    340    cairo_surface_flush(buffer->surface);
    341
    342    /* Get pointer to source image data */
    343    unsigned char* src_data = buffer->image;
    344    int src_stride = buffer->stride;
    345
    346    /* Get pointer to destination image data */
    347    unsigned char* dst_data = frame->data[0];
    348    int dst_stride = frame->linesize[0];
    349
    350    /* Get source/destination dimensions */
    351    int width = buffer->width;
    352    int height = buffer->height;
    353
    354    /* Source buffer is guaranteed to fit within destination buffer */
    355    assert(width <= frame->width);
    356    assert(height <= frame->height);
    357
    358    /* Add top margin */
    359    while (top > 0) {
    360        memset(dst_data, 0, frame->width * 4);
    361        dst_data += dst_stride;
    362        top--;
    363    }
    364
    365    /* Copy all data from source buffer to destination frame */
    366    while (height > 0) {
    367
    368        /* Calculate size of margin and data regions */
    369        int left_size = left * 4;
    370        int data_size = width * 4;
    371        int right_size = right * 4;
    372
    373        /* Add left margin */
    374        memset(dst_data, 0, left_size);
    375
    376        /* Copy data */
    377        memcpy(dst_data + left_size, src_data, data_size);
    378
    379        /* Add right margin */
    380        memset(dst_data + left_size + data_size, 0, right_size);
    381
    382        dst_data += dst_stride;
    383        src_data += src_stride;
    384
    385        height--;
    386
    387    }
    388
    389    /* Add bottom margin */
    390    while (bottom > 0) {
    391        memset(dst_data, 0, frame->width * 4);
    392        dst_data += dst_stride;
    393        bottom--;
    394    }
    395
    396    /* Frame converted */
    397    return frame;
    398
    399}
    400
    401void guacenc_video_prepare_frame(guacenc_video* video, guacenc_buffer* buffer) {
    402
    403    int lsize;
    404    int psize;
    405
    406    /* Ignore NULL buffers */
    407    if (buffer == NULL || buffer->surface == NULL)
    408        return;
    409
    410    /* Obtain destination frame */
    411    AVFrame* dst = video->next_frame;
    412
    413    /* Determine width of image if height is scaled to match destination */
    414    int scaled_width = buffer->width * dst->height / buffer->height;
    415
    416    /* Determine height of image if width is scaled to match destination */
    417    int scaled_height = buffer->height * dst->width / buffer->width;
    418
    419    /* If height-based scaling results in a fit width, add pillarboxes */
    420    if (scaled_width <= dst->width) {
    421        lsize = 0;
    422        psize = (dst->width - scaled_width)
    423               * buffer->height / dst->height / 2;
    424    }
    425
    426    /* If width-based scaling results in a fit width, add letterboxes */
    427    else {
    428        assert(scaled_height <= dst->height);
    429        psize = 0;
    430        lsize = (dst->height - scaled_height)
    431               * buffer->width / dst->width / 2;
    432    }
    433
    434    /* Prepare source frame for buffer */
    435    AVFrame* src = guacenc_video_frame_convert(buffer, lsize, psize);
    436    if (src == NULL) {
    437        guacenc_log(GUAC_LOG_WARNING, "Failed to allocate source frame. "
    438                "Frame dropped.");
    439        return;
    440    }
    441
    442    /* Prepare scaling context */
    443    struct SwsContext* sws = sws_getContext(src->width, src->height,
    444            AV_PIX_FMT_RGB32, dst->width, dst->height, AV_PIX_FMT_YUV420P,
    445            SWS_BICUBIC, NULL, NULL, NULL);
    446
    447    /* Abort if scaling context could not be created */
    448    if (sws == NULL) {
    449        guacenc_log(GUAC_LOG_WARNING, "Failed to allocate software scaling "
    450                "context. Frame dropped.");
    451        av_freep(&src->data[0]);
    452        av_frame_free(&src);
    453        return;
    454    }
    455
    456    /* Apply scaling, copying the source frame to the destination */
    457    sws_scale(sws, (const uint8_t* const*) src->data, src->linesize,
    458            0, src->height, dst->data, dst->linesize);
    459
    460    /* Free scaling context */
    461    sws_freeContext(sws);
    462
    463    /* Free source frame */
    464    av_freep(&src->data[0]);
    465    av_frame_free(&src);
    466
    467}
    468
    469int guacenc_video_free(guacenc_video* video) {
    470
    471    /* Ignore NULL video */
    472    if (video == NULL)
    473        return 0;
    474
    475    /* Write final frame */
    476    guacenc_video_flush_frame(video);
    477
    478    /* Flush any unwritten frames */
    479    int retval;
    480    do {
    481        retval = guacenc_video_write_frame(video, NULL);
    482    } while (retval > 0);
    483
    484    /* write trailer, if needed */
    485    if (video->container_format_context != NULL &&
    486            video->output_stream != NULL) {
    487        guacenc_log(GUAC_LOG_DEBUG, "Writing trailer: %s",
    488                av_write_trailer(video->container_format_context) == 0 ?
    489                        "success" : "failure");
    490    }
    491
    492    /* File is now completely written */
    493    if (video->container_format_context != NULL) {
    494        avio_close(video->container_format_context->pb);
    495    }
    496
    497    /* Free frame encoding data */
    498    av_freep(&video->next_frame->data[0]);
    499    av_frame_free(&video->next_frame);
    500
    501    /* Clean up encoding context */
    502    if (video->context != NULL) {
    503        avcodec_close(video->context);
    504        avcodec_free_context(&(video->context));
    505    }
    506
    507    guac_mem_free(video);
    508    return 0;
    509
    510}
    511