source: git/src/moviemaker.cc

Last change on this file was 4c83f84, checked in by Olly Betts <olly@…>, 13 days ago

Don't check HAVE_CONFIG_H in most cases

This check is only useful for img.c, which is intended to be usable
outside of Survex (and had fallbacks for functions which may not be
available which will get used if built in a non-autotools project).
For all the other source files it's just useless boilerplate.

  • Property mode set to 100644
File size: 11.0 KB
RevLine 
[6a4cdcb6]1//
2//  moviemaker.cc
3//
4//  Class for writing movies from Aven.
5//
[91a758c]6//  Copyright (C) 2004-2024 Olly Betts
[6a4cdcb6]7//
8//  This program is free software; you can redistribute it and/or modify
9//  it under the terms of the GNU General Public License as published by
10//  the Free Software Foundation; either version 2 of the License, or
11//  (at your option) any later version.
12//
13//  This program is distributed in the hope that it will be useful,
14//  but WITHOUT ANY WARRANTY; without even the implied warranty of
15//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16//  GNU General Public License for more details.
17//
18//  You should have received a copy of the GNU General Public License
19//  along with this program; if not, write to the Free Software
[ecbc6c18]20//  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
[6a4cdcb6]21//
22
[cc9e7a06]23/* Based on output-example.c:
24 *
25 * Libavformat API example: Output a media file in any supported
26 * libavformat format. The default codecs are used.
27 *
28 * Copyright (c) 2003 Fabrice Bellard
29 *
30 * Permission is hereby granted, free of charge, to any person obtaining a copy
31 * of this software and associated documentation files (the "Software"), to deal
32 * in the Software without restriction, including without limitation the rights
33 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
34 * copies of the Software, and to permit persons to whom the Software is
35 * furnished to do so, subject to the following conditions:
36 *
37 * The above copyright notice and this permission notice shall be included in
38 * all copies or substantial portions of the Software.
39 *
40 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
41 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
42 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
43 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
44 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
45 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
46 * THE SOFTWARE.
47 */
48
[6a4cdcb6]49#include <config.h>
50
[cc9e7a06]51#define __STDC_CONSTANT_MACROS
52
[1805274]53#include <assert.h>
[6a4cdcb6]54#include <stdlib.h>
55#include <string.h>
56
57#include "moviemaker.h"
58
[40d1db36]59#ifdef WITH_FFMPEG
[cc9e7a06]60extern "C" {
[44c4b70]61# include <libavcodec/avcodec.h>
[b282f44]62# include <libavutil/imgutils.h>
[008f2f3]63# include <libavutil/mathematics.h>
[64d06c0]64# include <libavformat/avformat.h>
[86d8ee5]65# include <libswscale/swscale.h>
[cc9e7a06]66}
[0aaa578]67#endif
[6a4cdcb6]68
[40d1db36]69// Handle the "no FFmpeg" case in this file.
70#if !defined WITH_FFMPEG || LIBAVCODEC_VERSION_MAJOR >= 57
[0aaa578]71
[40d1db36]72#ifdef WITH_FFMPEG
[091069f]73enum {
74    MOVIE_NO_SUITABLE_FORMAT = 1,
75    MOVIE_AUDIO_ONLY,
[c648bd1]76    MOVIE_FILENAME_TOO_LONG
[091069f]77};
[bc6faa9]78#endif
[6a4cdcb6]79
[1805274]80MovieMaker::MovieMaker()
[40d1db36]81#ifdef WITH_FFMPEG
[0aaa578]82    : oc(0), video_st(0), context(0), frame(0), pixels(0), sws_ctx(0), averrno(0)
[ccb83b7]83#endif
[6a4cdcb6]84{
[40d1db36]85#ifdef WITH_FFMPEG
[6a4cdcb6]86    static bool initialised_ffmpeg = false;
[1805274]87    if (initialised_ffmpeg) return;
[6a4cdcb6]88
[0482978]89#if LIBAVCODEC_VERSION_MAJOR < 58
[64d06c0]90    avcodec_register_all();
[1805274]91    av_register_all();
[0482978]92#endif
[6a4cdcb6]93
[1805274]94    initialised_ffmpeg = true;
95#endif
96}
97
[40d1db36]98#ifdef WITH_FFMPEG
[f4e4b56]99static int
[91a758c]100write_packet(void *opaque,
101#if LIBAVFORMAT_VERSION_MAJOR < 61
102             uint8_t *buf,
103#else
104             const uint8_t *buf,
105#endif
106             int buf_size)
107{
[f4e4b56]108    FILE * fh = (FILE*)opaque;
109    size_t res = fwrite(buf, 1, buf_size, fh);
110    return res > 0 ? res : -1;
111}
[420cda9]112
113static int64_t
114seek_stream(void *opaque, int64_t offset, int whence) {
115    FILE * fh = (FILE*)opaque;
116    return fseek(fh, offset, whence);
117}
[0db0f91]118#endif
[f4e4b56]119
120#define MAX_EXTENSION_LEN 8
121
122bool MovieMaker::Open(FILE* fh, const char * ext, int width, int height)
[1805274]123{
[40d1db36]124#ifdef WITH_FFMPEG
[f4e4b56]125    fh_to_close = fh;
126
[0aaa578]127    /* Allocate the output media context. */
[f4e4b56]128    char dummy_filename[MAX_EXTENSION_LEN + 3] = "x.";
[0aaa578]129    oc = NULL;
[f4e4b56]130    if (strlen(ext) <= MAX_EXTENSION_LEN) {
[0aaa578]131        // Use "x." + extension for format detection to avoid having to deal
[f4e4b56]132        // with wide character filenames.
[0aaa578]133        strcpy(dummy_filename + 2, ext);
134        avformat_alloc_output_context2(&oc, NULL, NULL, dummy_filename);
[1805274]135    }
[0aaa578]136    if (!oc) {
137        averrno = MOVIE_NO_SUITABLE_FORMAT;
[1805274]138        return false;
[6a4cdcb6]139    }
140
[44c4b70]141    auto fmt = oc->oformat;
[0aaa578]142    if (fmt->video_codec == AV_CODEC_ID_NONE) {
143        averrno = MOVIE_AUDIO_ONLY;
[1805274]144        return false;
[6a4cdcb6]145    }
146
[64d06c0]147    /* find the video encoder */
[44c4b70]148    auto codec = avcodec_find_encoder(fmt->video_codec);
[64d06c0]149    if (!codec) {
150        // FIXME : Erm - internal ffmpeg library problem?
[63621a7]151        averrno = AVERROR(ENOMEM);
[1805274]152        return false;
[6a4cdcb6]153    }
154
[64d06c0]155    // Add the video stream.
[0aaa578]156    video_st = avformat_new_stream(oc, NULL);
[64d06c0]157    if (!video_st) {
158        averrno = AVERROR(ENOMEM);
159        return false;
160    }
[6a4cdcb6]161
[0aaa578]162    context = avcodec_alloc_context3(codec);
163    context->codec_id = fmt->video_codec;
164    context->width = width;
165    context->height = height;
[5270758]166    video_st->time_base.den = 25; // Frames per second.
167    video_st->time_base.num = 1;
[0aaa578]168    context->time_base = video_st->time_base;
169    context->bit_rate = width * height * (4 * 0.07) * context->time_base.den / context->time_base.num;
170    context->bit_rate_tolerance = context->bit_rate;
171    context->global_quality = 4;
172    context->rc_buffer_size = 2 * 1024 * 1024;
173    context->rc_max_rate = context->bit_rate * 8;
174    context->gop_size = 50; /* Twice the framerate */
175    context->pix_fmt = AV_PIX_FMT_YUV420P;
176    if (context->has_b_frames) {
177        // B frames are backwards predicted - they can improve compression,
178        // but may slow encoding and decoding.
179        context->max_b_frames = 4;
180    }
[6a4cdcb6]181
[64d06c0]182    /* Some formats want stream headers to be separate. */
[cc9e7a06]183    if (oc->oformat->flags & AVFMT_GLOBALHEADER)
[5e46ae2]184        context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
[cc9e7a06]185
[7831cef]186    int retval;
[0aaa578]187    retval = avcodec_open2(context, codec, NULL);
[091069f]188    if (retval < 0) {
189        averrno = retval;
[1805274]190        return false;
[6a4cdcb6]191    }
[1805274]192
[64d06c0]193    /* Allocate the encoded raw picture. */
[8364c65f]194    frame = av_frame_alloc();
[cc9e7a06]195    if (!frame) {
[63621a7]196        averrno = AVERROR(ENOMEM);
[1805274]197        return false;
[6a4cdcb6]198    }
[0aaa578]199
200    frame->format = context->pix_fmt;
201    frame->width = width;
202    frame->height = height;
203    frame->pts = 0;
204
205    retval = av_frame_get_buffer(frame, 32);
[64d06c0]206    if (retval < 0) {
207        averrno = retval;
[1805274]208        return false;
[6a4cdcb6]209    }
[cc9e7a06]210
[0aaa578]211    if (frame->format != AV_PIX_FMT_YUV420P) {
[cc9e7a06]212        // FIXME need to allocate another frame for this case if we stop
[8364c65f]213        // hardcoding AV_PIX_FMT_YUV420P.
[cc9e7a06]214        abort();
215    }
[6a4cdcb6]216
[0aaa578]217    /* copy the stream parameters to the muxer */
218    retval = avcodec_parameters_from_context(video_st->codecpar, context);
219    if (retval < 0) {
220        averrno = retval;
221        return false;
222    }
[5270758]223
[d13aea6]224    pixels = (unsigned char *)av_malloc(width * height * 6);
[6a4cdcb6]225    if (!pixels) {
[63621a7]226        averrno = AVERROR(ENOMEM);
[1805274]227        return false;
[6a4cdcb6]228    }
[1805274]229
[64d06c0]230    // Show the format we've ended up with (for debug purposes).
[0aaa578]231    // av_dump_format(oc, 0, dummy_filename, 1);
[64d06c0]232
233    av_free(sws_ctx);
[ad04c87]234    sws_ctx = sws_getContext(width, height, AV_PIX_FMT_RGB24,
[0aaa578]235                             width, height, context->pix_fmt, SWS_BICUBIC,
[64d06c0]236                             NULL, NULL, NULL);
237    if (sws_ctx == NULL) {
238        fprintf(stderr, "Cannot initialize the conversion context!\n");
239        averrno = AVERROR(ENOMEM);
[1805274]240        return false;
241    }
242
[64d06c0]243    if (!(fmt->flags & AVFMT_NOFILE)) {
[f4e4b56]244        const int buf_size = 8192;
245        void * buf = av_malloc(buf_size);
246        oc->pb = avio_alloc_context(static_cast<uint8_t*>(buf), buf_size, 1,
[420cda9]247                                    fh, NULL, write_packet, seek_stream);
[f4e4b56]248        if (!oc->pb) {
249            averrno = AVERROR(ENOMEM);
[64d06c0]250            return false;
251        }
252    }
253
[1805274]254    // Write the stream header, if any.
[7831cef]255    retval = avformat_write_header(oc, NULL);
[091069f]256    if (retval < 0) {
257        averrno = retval;
[1805274]258        return false;
259    }
[9e516d0d]260
[091069f]261    averrno = 0;
[1805274]262    return true;
[6a4cdcb6]263#else
[f4e4b56]264    (void)fh;
265    (void)ext;
[6e22f11]266    (void)width;
267    (void)height;
[6a4cdcb6]268    return false;
269#endif
270}
271
272unsigned char * MovieMaker::GetBuffer() const {
[40d1db36]273#ifdef WITH_FFMPEG
[cd04101]274    return pixels + GetWidth() * GetHeight() * 3;
[eac4514]275#else
276    return NULL;
277#endif
[6a4cdcb6]278}
279
280int MovieMaker::GetWidth() const {
[40d1db36]281#ifdef WITH_FFMPEG
[ccb83b7]282    assert(video_st);
[0aaa578]283    return video_st->codecpar->width;
[1805274]284#else
285    return 0;
286#endif
[6a4cdcb6]287}
288
289int MovieMaker::GetHeight() const {
[40d1db36]290#ifdef WITH_FFMPEG
[ccb83b7]291    assert(video_st);
[0aaa578]292    return video_st->codecpar->height;
[1805274]293#else
294    return 0;
295#endif
[6a4cdcb6]296}
297
[40d1db36]298#ifdef WITH_FFMPEG
[0aaa578]299// Call with frame=NULL when done.
300int
301MovieMaker::encode_frame(AVFrame* frame_or_null)
302{
303    int ret = avcodec_send_frame(context, frame_or_null);
304    if (ret < 0) return ret;
305
306    AVPacket *pkt = av_packet_alloc();
307    pkt->size = 0;
308    while ((ret = avcodec_receive_packet(context, pkt)) == 0) {
309        // Rescale output packet timestamp values from codec to stream timebase.
310        av_packet_rescale_ts(pkt, context->time_base, video_st->time_base);
311        pkt->stream_index = video_st->index;
312
313        // Write the compressed frame to the media file.
314        ret = av_interleaved_write_frame(oc, pkt);
315        if (ret < 0) {
316            av_packet_free(&pkt);
317            release();
318            return ret;
319        }
320    }
321    av_packet_free(&pkt);
322    return 0;
323}
324#endif
325
[98fd937]326bool MovieMaker::AddFrame()
[6a4cdcb6]327{
[40d1db36]328#ifdef WITH_FFMPEG
[0aaa578]329    int ret = av_frame_make_writable(frame);
330    if (ret < 0) {
331        averrno = ret;
332        return false;
333    }
[cc9e7a06]334
[0aaa578]335    enum AVPixelFormat pix_fmt = context->pix_fmt;
336
337    if (pix_fmt != AV_PIX_FMT_YUV420P) {
[cc9e7a06]338        // FIXME convert...
339        abort();
340    }
[1805274]341
[0aaa578]342    int len = 3 * GetWidth();
[fed3713]343    {
344        // Flip image vertically
[0aaa578]345        int h = GetHeight();
[fed3713]346        unsigned char * src = pixels + h * len;
347        unsigned char * dest = src - len;
348        while (h--) {
349            memcpy(dest, src, len);
350            src += len;
351            dest -= len;
352        }
[6a4cdcb6]353    }
[0aaa578]354    sws_scale(sws_ctx, &pixels, &len, 0, GetHeight(),
355              frame->data, frame->linesize);
[6a4cdcb6]356
[0aaa578]357    ++frame->pts;
[64d06c0]358
[0aaa578]359    // Encode this frame.
360    ret = encode_frame(frame);
[64d06c0]361    if (ret < 0) {
[98fd937]362        averrno = ret;
363        return false;
[64d06c0]364    }
365#endif
[98fd937]366    return true;
[6a4cdcb6]367}
368
[98fd937]369bool
370MovieMaker::Close()
[6a4cdcb6]371{
[40d1db36]372#ifdef WITH_FFMPEG
[64d06c0]373    if (video_st && averrno == 0) {
[0aaa578]374        // Flush out any remaining data.
375        int ret = encode_frame(NULL);
376        if (ret < 0) {
377            averrno = ret;
378            return false;
[6a4cdcb6]379        }
[cc9e7a06]380        av_write_trailer(oc);
[091069f]381    }
[cc9e7a06]382
[98fd937]383    release();
384#endif
385    return true;
386}
387
[40d1db36]388#ifdef WITH_FFMPEG
[98fd937]389void
390MovieMaker::release()
391{
[0aaa578]392    // Close codec.
393    avcodec_free_context(&context);
394    av_frame_free(&frame);
[d13aea6]395    av_free(pixels);
[98fd937]396    pixels = NULL;
[0aaa578]397    sws_freeContext(sws_ctx);
[98fd937]398    sws_ctx = NULL;
[1805274]399
[0aaa578]400    // Free the stream.
401    avformat_free_context(oc);
402    oc = NULL;
[1805274]403
[f4e4b56]404    if (fh_to_close) {
405        fclose(fh_to_close);
406        fh_to_close = NULL;
407    }
[6a4cdcb6]408}
[c648bd1]409#endif
[091069f]410
[98fd937]411MovieMaker::~MovieMaker()
412{
[40d1db36]413#ifdef WITH_FFMPEG
[98fd937]414    release();
[c648bd1]415#endif
[98fd937]416}
417
[091069f]418const char *
419MovieMaker::get_error_string() const
420{
[40d1db36]421#ifdef WITH_FFMPEG
[091069f]422    switch (averrno) {
[63621a7]423        case AVERROR(EIO):
[091069f]424            return "I/O error";
[63621a7]425        case AVERROR(EDOM):
[091069f]426            return "Number syntax expected in filename";
427        case AVERROR_INVALIDDATA:
428            /* same as AVERROR_UNKNOWN: return "unknown error"; */
429            return "invalid data found";
[63621a7]430        case AVERROR(ENOMEM):
[091069f]431            return "not enough memory";
[63621a7]432        case AVERROR(EILSEQ):
[091069f]433            return "unknown format";
[63621a7]434        case AVERROR(ENOSYS):
[091069f]435            return "Operation not supported";
[63621a7]436        case AVERROR(ENOENT):
[091069f]437            return "No such file or directory";
438        case AVERROR_EOF:
439            return "End of file";
440        case AVERROR_PATCHWELCOME:
441            return "Not implemented in FFmpeg";
442        case 0:
443            return "No error";
444        case MOVIE_NO_SUITABLE_FORMAT:
445            return "Couldn't find a suitable output format";
446        case MOVIE_AUDIO_ONLY:
447            return "Audio-only format specified";
448        case MOVIE_FILENAME_TOO_LONG:
449            return "Filename too long";
450    }
[eac4514]451#endif
[f4e4b56]452    return "Unknown error";
[091069f]453}
[0aaa578]454
455#else
456
457#include "moviemaker-legacy.cc"
458
459#endif
Note: See TracBrowser for help on using the repository browser.