source: git/src/moviemaker.cc @ 8a7804fb

main
Last change on this file since 8a7804fb was 0b99107, checked in by Olly Betts <olly@…>, 5 weeks ago

Eliminate old FSF addresses

Update GPL/LGPL licence files and boilerplate to direct people who
didn't receive the licence text to the FSF website, as the current
versions of the FSF licence texts now do, rather than giving a postal
address.

  • Property mode set to 100644
File size: 10.8 KB
RevLine 
[6a4cdcb6]1//
2//  moviemaker.cc
3//
4//  Class for writing movies from Aven.
5//
[02e7e56]6//  Copyright (C) 2004-2025 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
[0b99107]19//  along with this program; if not, see
20//  <https://www.gnu.org/licenses/>.
[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>
[bf063be]54#include <stdint.h>
[6a4cdcb6]55#include <stdlib.h>
56#include <string.h>
57
58#include "moviemaker.h"
59
[40d1db36]60#ifdef WITH_FFMPEG
[cc9e7a06]61extern "C" {
[44c4b70]62# include <libavcodec/avcodec.h>
[b282f44]63# include <libavutil/imgutils.h>
[008f2f3]64# include <libavutil/mathematics.h>
[64d06c0]65# include <libavformat/avformat.h>
[86d8ee5]66# include <libswscale/swscale.h>
[cc9e7a06]67}
[0aaa578]68#endif
[6a4cdcb6]69
[40d1db36]70// Handle the "no FFmpeg" case in this file.
71#if !defined WITH_FFMPEG || LIBAVCODEC_VERSION_MAJOR >= 57
[0aaa578]72
[40d1db36]73#ifdef WITH_FFMPEG
[091069f]74enum {
75    MOVIE_NO_SUITABLE_FORMAT = 1,
76    MOVIE_AUDIO_ONLY,
[c648bd1]77    MOVIE_FILENAME_TOO_LONG
[091069f]78};
[bc6faa9]79#endif
[6a4cdcb6]80
[1805274]81MovieMaker::MovieMaker()
[6a4cdcb6]82{
[40d1db36]83#ifdef WITH_FFMPEG
[6a4cdcb6]84    static bool initialised_ffmpeg = false;
[1805274]85    if (initialised_ffmpeg) return;
[6a4cdcb6]86
[0482978]87#if LIBAVCODEC_VERSION_MAJOR < 58
[64d06c0]88    avcodec_register_all();
[1805274]89    av_register_all();
[0482978]90#endif
[6a4cdcb6]91
[1805274]92    initialised_ffmpeg = true;
93#endif
94}
95
[40d1db36]96#ifdef WITH_FFMPEG
[f4e4b56]97static int
[91a758c]98write_packet(void *opaque,
99#if LIBAVFORMAT_VERSION_MAJOR < 61
100             uint8_t *buf,
101#else
102             const uint8_t *buf,
103#endif
104             int buf_size)
105{
[f4e4b56]106    FILE * fh = (FILE*)opaque;
[87c9067]107    size_t res = FWRITE_(buf, 1, buf_size, fh);
[f4e4b56]108    return res > 0 ? res : -1;
109}
[420cda9]110
111static int64_t
112seek_stream(void *opaque, int64_t offset, int whence) {
113    FILE * fh = (FILE*)opaque;
114    return fseek(fh, offset, whence);
115}
[0db0f91]116#endif
[f4e4b56]117
118#define MAX_EXTENSION_LEN 8
119
120bool MovieMaker::Open(FILE* fh, const char * ext, int width, int height)
[1805274]121{
[40d1db36]122#ifdef WITH_FFMPEG
[f4e4b56]123    fh_to_close = fh;
124
[0aaa578]125    /* Allocate the output media context. */
[f4e4b56]126    char dummy_filename[MAX_EXTENSION_LEN + 3] = "x.";
[0aaa578]127    oc = NULL;
[f4e4b56]128    if (strlen(ext) <= MAX_EXTENSION_LEN) {
[0aaa578]129        // Use "x." + extension for format detection to avoid having to deal
[f4e4b56]130        // with wide character filenames.
[0aaa578]131        strcpy(dummy_filename + 2, ext);
132        avformat_alloc_output_context2(&oc, NULL, NULL, dummy_filename);
[1805274]133    }
[0aaa578]134    if (!oc) {
135        averrno = MOVIE_NO_SUITABLE_FORMAT;
[1805274]136        return false;
[6a4cdcb6]137    }
138
[44c4b70]139    auto fmt = oc->oformat;
[0aaa578]140    if (fmt->video_codec == AV_CODEC_ID_NONE) {
141        averrno = MOVIE_AUDIO_ONLY;
[1805274]142        return false;
[6a4cdcb6]143    }
144
[64d06c0]145    /* find the video encoder */
[44c4b70]146    auto codec = avcodec_find_encoder(fmt->video_codec);
[64d06c0]147    if (!codec) {
148        // FIXME : Erm - internal ffmpeg library problem?
[63621a7]149        averrno = AVERROR(ENOMEM);
[1805274]150        return false;
[6a4cdcb6]151    }
152
[64d06c0]153    // Add the video stream.
[0aaa578]154    video_st = avformat_new_stream(oc, NULL);
[64d06c0]155    if (!video_st) {
156        averrno = AVERROR(ENOMEM);
157        return false;
158    }
[6a4cdcb6]159
[0aaa578]160    context = avcodec_alloc_context3(codec);
161    context->codec_id = fmt->video_codec;
162    context->width = width;
163    context->height = height;
[5270758]164    video_st->time_base.den = 25; // Frames per second.
165    video_st->time_base.num = 1;
[0aaa578]166    context->time_base = video_st->time_base;
167    context->bit_rate = width * height * (4 * 0.07) * context->time_base.den / context->time_base.num;
168    context->bit_rate_tolerance = context->bit_rate;
169    context->global_quality = 4;
170    context->rc_buffer_size = 2 * 1024 * 1024;
171    context->rc_max_rate = context->bit_rate * 8;
172    context->gop_size = 50; /* Twice the framerate */
173    context->pix_fmt = AV_PIX_FMT_YUV420P;
174    if (context->has_b_frames) {
175        // B frames are backwards predicted - they can improve compression,
176        // but may slow encoding and decoding.
177        context->max_b_frames = 4;
178    }
[6a4cdcb6]179
[64d06c0]180    /* Some formats want stream headers to be separate. */
[cc9e7a06]181    if (oc->oformat->flags & AVFMT_GLOBALHEADER)
[5e46ae2]182        context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
[cc9e7a06]183
[7831cef]184    int retval;
[0aaa578]185    retval = avcodec_open2(context, codec, NULL);
[091069f]186    if (retval < 0) {
187        averrno = retval;
[1805274]188        return false;
[6a4cdcb6]189    }
[1805274]190
[64d06c0]191    /* Allocate the encoded raw picture. */
[8364c65f]192    frame = av_frame_alloc();
[cc9e7a06]193    if (!frame) {
[63621a7]194        averrno = AVERROR(ENOMEM);
[1805274]195        return false;
[6a4cdcb6]196    }
[0aaa578]197
198    frame->format = context->pix_fmt;
199    frame->width = width;
200    frame->height = height;
201    frame->pts = 0;
202
203    retval = av_frame_get_buffer(frame, 32);
[64d06c0]204    if (retval < 0) {
205        averrno = retval;
[1805274]206        return false;
[6a4cdcb6]207    }
[cc9e7a06]208
[0aaa578]209    if (frame->format != AV_PIX_FMT_YUV420P) {
[cc9e7a06]210        // FIXME need to allocate another frame for this case if we stop
[8364c65f]211        // hardcoding AV_PIX_FMT_YUV420P.
[cc9e7a06]212        abort();
213    }
[6a4cdcb6]214
[0aaa578]215    /* copy the stream parameters to the muxer */
216    retval = avcodec_parameters_from_context(video_st->codecpar, context);
217    if (retval < 0) {
218        averrno = retval;
219        return false;
220    }
[5270758]221
[d13aea6]222    pixels = (unsigned char *)av_malloc(width * height * 6);
[6a4cdcb6]223    if (!pixels) {
[63621a7]224        averrno = AVERROR(ENOMEM);
[1805274]225        return false;
[6a4cdcb6]226    }
[1805274]227
[64d06c0]228    // Show the format we've ended up with (for debug purposes).
[0aaa578]229    // av_dump_format(oc, 0, dummy_filename, 1);
[64d06c0]230
231    av_free(sws_ctx);
[ad04c87]232    sws_ctx = sws_getContext(width, height, AV_PIX_FMT_RGB24,
[0aaa578]233                             width, height, context->pix_fmt, SWS_BICUBIC,
[64d06c0]234                             NULL, NULL, NULL);
235    if (sws_ctx == NULL) {
236        fprintf(stderr, "Cannot initialize the conversion context!\n");
237        averrno = AVERROR(ENOMEM);
[1805274]238        return false;
239    }
240
[64d06c0]241    if (!(fmt->flags & AVFMT_NOFILE)) {
[f4e4b56]242        const int buf_size = 8192;
243        void * buf = av_malloc(buf_size);
244        oc->pb = avio_alloc_context(static_cast<uint8_t*>(buf), buf_size, 1,
[420cda9]245                                    fh, NULL, write_packet, seek_stream);
[f4e4b56]246        if (!oc->pb) {
247            averrno = AVERROR(ENOMEM);
[64d06c0]248            return false;
249        }
250    }
251
[1805274]252    // Write the stream header, if any.
[7831cef]253    retval = avformat_write_header(oc, NULL);
[091069f]254    if (retval < 0) {
255        averrno = retval;
[1805274]256        return false;
257    }
[9e516d0d]258
[091069f]259    averrno = 0;
[1805274]260    return true;
[6a4cdcb6]261#else
[f4e4b56]262    (void)fh;
263    (void)ext;
[6e22f11]264    (void)width;
265    (void)height;
[6a4cdcb6]266    return false;
267#endif
268}
269
270unsigned char * MovieMaker::GetBuffer() const {
[40d1db36]271#ifdef WITH_FFMPEG
[cd04101]272    return pixels + GetWidth() * GetHeight() * 3;
[eac4514]273#else
274    return NULL;
275#endif
[6a4cdcb6]276}
277
278int MovieMaker::GetWidth() const {
[40d1db36]279#ifdef WITH_FFMPEG
[ccb83b7]280    assert(video_st);
[0aaa578]281    return video_st->codecpar->width;
[1805274]282#else
283    return 0;
284#endif
[6a4cdcb6]285}
286
287int MovieMaker::GetHeight() const {
[40d1db36]288#ifdef WITH_FFMPEG
[ccb83b7]289    assert(video_st);
[0aaa578]290    return video_st->codecpar->height;
[1805274]291#else
292    return 0;
293#endif
[6a4cdcb6]294}
295
[40d1db36]296#ifdef WITH_FFMPEG
[0aaa578]297// Call with frame=NULL when done.
298int
299MovieMaker::encode_frame(AVFrame* frame_or_null)
300{
301    int ret = avcodec_send_frame(context, frame_or_null);
302    if (ret < 0) return ret;
303
304    AVPacket *pkt = av_packet_alloc();
305    pkt->size = 0;
306    while ((ret = avcodec_receive_packet(context, pkt)) == 0) {
307        // Rescale output packet timestamp values from codec to stream timebase.
308        av_packet_rescale_ts(pkt, context->time_base, video_st->time_base);
309        pkt->stream_index = video_st->index;
310
311        // Write the compressed frame to the media file.
312        ret = av_interleaved_write_frame(oc, pkt);
313        if (ret < 0) {
314            av_packet_free(&pkt);
315            release();
316            return ret;
317        }
318    }
319    av_packet_free(&pkt);
320    return 0;
321}
322#endif
323
[98fd937]324bool MovieMaker::AddFrame()
[6a4cdcb6]325{
[40d1db36]326#ifdef WITH_FFMPEG
[0aaa578]327    int ret = av_frame_make_writable(frame);
328    if (ret < 0) {
329        averrno = ret;
330        return false;
331    }
[cc9e7a06]332
[0aaa578]333    enum AVPixelFormat pix_fmt = context->pix_fmt;
334
335    if (pix_fmt != AV_PIX_FMT_YUV420P) {
[cc9e7a06]336        // FIXME convert...
337        abort();
338    }
[1805274]339
[0aaa578]340    int len = 3 * GetWidth();
[fed3713]341    {
342        // Flip image vertically
[0aaa578]343        int h = GetHeight();
[fed3713]344        unsigned char * src = pixels + h * len;
345        unsigned char * dest = src - len;
346        while (h--) {
347            memcpy(dest, src, len);
348            src += len;
349            dest -= len;
350        }
[6a4cdcb6]351    }
[0aaa578]352    sws_scale(sws_ctx, &pixels, &len, 0, GetHeight(),
353              frame->data, frame->linesize);
[6a4cdcb6]354
[0aaa578]355    ++frame->pts;
[64d06c0]356
[0aaa578]357    // Encode this frame.
358    ret = encode_frame(frame);
[64d06c0]359    if (ret < 0) {
[98fd937]360        averrno = ret;
361        return false;
[64d06c0]362    }
363#endif
[98fd937]364    return true;
[6a4cdcb6]365}
366
[98fd937]367bool
368MovieMaker::Close()
[6a4cdcb6]369{
[40d1db36]370#ifdef WITH_FFMPEG
[64d06c0]371    if (video_st && averrno == 0) {
[0aaa578]372        // Flush out any remaining data.
373        int ret = encode_frame(NULL);
374        if (ret < 0) {
375            averrno = ret;
376            return false;
[6a4cdcb6]377        }
[cc9e7a06]378        av_write_trailer(oc);
[091069f]379    }
[cc9e7a06]380
[98fd937]381    release();
382#endif
383    return true;
384}
385
[40d1db36]386#ifdef WITH_FFMPEG
[98fd937]387void
388MovieMaker::release()
389{
[0aaa578]390    // Close codec.
391    avcodec_free_context(&context);
392    av_frame_free(&frame);
[d13aea6]393    av_free(pixels);
[98fd937]394    pixels = NULL;
[0aaa578]395    sws_freeContext(sws_ctx);
[98fd937]396    sws_ctx = NULL;
[1805274]397
[0aaa578]398    // Free the stream.
399    avformat_free_context(oc);
400    oc = NULL;
[1805274]401
[f4e4b56]402    if (fh_to_close) {
403        fclose(fh_to_close);
404        fh_to_close = NULL;
405    }
[6a4cdcb6]406}
[c648bd1]407#endif
[091069f]408
[98fd937]409MovieMaker::~MovieMaker()
410{
[40d1db36]411#ifdef WITH_FFMPEG
[98fd937]412    release();
[c648bd1]413#endif
[98fd937]414}
415
[091069f]416const char *
417MovieMaker::get_error_string() const
418{
[40d1db36]419#ifdef WITH_FFMPEG
[091069f]420    switch (averrno) {
[63621a7]421        case AVERROR(EIO):
[091069f]422            return "I/O error";
[63621a7]423        case AVERROR(EDOM):
[091069f]424            return "Number syntax expected in filename";
425        case AVERROR_INVALIDDATA:
426            /* same as AVERROR_UNKNOWN: return "unknown error"; */
427            return "invalid data found";
[63621a7]428        case AVERROR(ENOMEM):
[091069f]429            return "not enough memory";
[63621a7]430        case AVERROR(EILSEQ):
[091069f]431            return "unknown format";
[63621a7]432        case AVERROR(ENOSYS):
[091069f]433            return "Operation not supported";
[63621a7]434        case AVERROR(ENOENT):
[091069f]435            return "No such file or directory";
436        case AVERROR_EOF:
437            return "End of file";
438        case AVERROR_PATCHWELCOME:
439            return "Not implemented in FFmpeg";
440        case 0:
441            return "No error";
442        case MOVIE_NO_SUITABLE_FORMAT:
443            return "Couldn't find a suitable output format";
444        case MOVIE_AUDIO_ONLY:
445            return "Audio-only format specified";
446        case MOVIE_FILENAME_TOO_LONG:
447            return "Filename too long";
448    }
[eac4514]449#endif
[f4e4b56]450    return "Unknown error";
[091069f]451}
[0aaa578]452
453#else
454
[02e7e56]455#error Need libavcodec 57 or newer (FFmpeg >= 3.2)
[0aaa578]456
457#endif
Note: See TracBrowser for help on using the repository browser.