source: git/src/moviemaker.cc

Last change on this file was 4c83f84, checked in by Olly Betts <olly@…>, 3 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
Line 
1//
2//  moviemaker.cc
3//
4//  Class for writing movies from Aven.
5//
6//  Copyright (C) 2004-2024 Olly Betts
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
20//  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
21//
22
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
49#include <config.h>
50
51#define __STDC_CONSTANT_MACROS
52
53#include <assert.h>
54#include <stdlib.h>
55#include <string.h>
56
57#include "moviemaker.h"
58
59#ifdef WITH_FFMPEG
60extern "C" {
61# include <libavcodec/avcodec.h>
62# include <libavutil/imgutils.h>
63# include <libavutil/mathematics.h>
64# include <libavformat/avformat.h>
65# include <libswscale/swscale.h>
66}
67#endif
68
69// Handle the "no FFmpeg" case in this file.
70#if !defined WITH_FFMPEG || LIBAVCODEC_VERSION_MAJOR >= 57
71
72#ifdef WITH_FFMPEG
73enum {
74    MOVIE_NO_SUITABLE_FORMAT = 1,
75    MOVIE_AUDIO_ONLY,
76    MOVIE_FILENAME_TOO_LONG
77};
78#endif
79
80MovieMaker::MovieMaker()
81#ifdef WITH_FFMPEG
82    : oc(0), video_st(0), context(0), frame(0), pixels(0), sws_ctx(0), averrno(0)
83#endif
84{
85#ifdef WITH_FFMPEG
86    static bool initialised_ffmpeg = false;
87    if (initialised_ffmpeg) return;
88
89#if LIBAVCODEC_VERSION_MAJOR < 58
90    avcodec_register_all();
91    av_register_all();
92#endif
93
94    initialised_ffmpeg = true;
95#endif
96}
97
98#ifdef WITH_FFMPEG
99static int
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{
108    FILE * fh = (FILE*)opaque;
109    size_t res = fwrite(buf, 1, buf_size, fh);
110    return res > 0 ? res : -1;
111}
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}
118#endif
119
120#define MAX_EXTENSION_LEN 8
121
122bool MovieMaker::Open(FILE* fh, const char * ext, int width, int height)
123{
124#ifdef WITH_FFMPEG
125    fh_to_close = fh;
126
127    /* Allocate the output media context. */
128    char dummy_filename[MAX_EXTENSION_LEN + 3] = "x.";
129    oc = NULL;
130    if (strlen(ext) <= MAX_EXTENSION_LEN) {
131        // Use "x." + extension for format detection to avoid having to deal
132        // with wide character filenames.
133        strcpy(dummy_filename + 2, ext);
134        avformat_alloc_output_context2(&oc, NULL, NULL, dummy_filename);
135    }
136    if (!oc) {
137        averrno = MOVIE_NO_SUITABLE_FORMAT;
138        return false;
139    }
140
141    auto fmt = oc->oformat;
142    if (fmt->video_codec == AV_CODEC_ID_NONE) {
143        averrno = MOVIE_AUDIO_ONLY;
144        return false;
145    }
146
147    /* find the video encoder */
148    auto codec = avcodec_find_encoder(fmt->video_codec);
149    if (!codec) {
150        // FIXME : Erm - internal ffmpeg library problem?
151        averrno = AVERROR(ENOMEM);
152        return false;
153    }
154
155    // Add the video stream.
156    video_st = avformat_new_stream(oc, NULL);
157    if (!video_st) {
158        averrno = AVERROR(ENOMEM);
159        return false;
160    }
161
162    context = avcodec_alloc_context3(codec);
163    context->codec_id = fmt->video_codec;
164    context->width = width;
165    context->height = height;
166    video_st->time_base.den = 25; // Frames per second.
167    video_st->time_base.num = 1;
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    }
181
182    /* Some formats want stream headers to be separate. */
183    if (oc->oformat->flags & AVFMT_GLOBALHEADER)
184        context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
185
186    int retval;
187    retval = avcodec_open2(context, codec, NULL);
188    if (retval < 0) {
189        averrno = retval;
190        return false;
191    }
192
193    /* Allocate the encoded raw picture. */
194    frame = av_frame_alloc();
195    if (!frame) {
196        averrno = AVERROR(ENOMEM);
197        return false;
198    }
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);
206    if (retval < 0) {
207        averrno = retval;
208        return false;
209    }
210
211    if (frame->format != AV_PIX_FMT_YUV420P) {
212        // FIXME need to allocate another frame for this case if we stop
213        // hardcoding AV_PIX_FMT_YUV420P.
214        abort();
215    }
216
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    }
223
224    pixels = (unsigned char *)av_malloc(width * height * 6);
225    if (!pixels) {
226        averrno = AVERROR(ENOMEM);
227        return false;
228    }
229
230    // Show the format we've ended up with (for debug purposes).
231    // av_dump_format(oc, 0, dummy_filename, 1);
232
233    av_free(sws_ctx);
234    sws_ctx = sws_getContext(width, height, AV_PIX_FMT_RGB24,
235                             width, height, context->pix_fmt, SWS_BICUBIC,
236                             NULL, NULL, NULL);
237    if (sws_ctx == NULL) {
238        fprintf(stderr, "Cannot initialize the conversion context!\n");
239        averrno = AVERROR(ENOMEM);
240        return false;
241    }
242
243    if (!(fmt->flags & AVFMT_NOFILE)) {
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,
247                                    fh, NULL, write_packet, seek_stream);
248        if (!oc->pb) {
249            averrno = AVERROR(ENOMEM);
250            return false;
251        }
252    }
253
254    // Write the stream header, if any.
255    retval = avformat_write_header(oc, NULL);
256    if (retval < 0) {
257        averrno = retval;
258        return false;
259    }
260
261    averrno = 0;
262    return true;
263#else
264    (void)fh;
265    (void)ext;
266    (void)width;
267    (void)height;
268    return false;
269#endif
270}
271
272unsigned char * MovieMaker::GetBuffer() const {
273#ifdef WITH_FFMPEG
274    return pixels + GetWidth() * GetHeight() * 3;
275#else
276    return NULL;
277#endif
278}
279
280int MovieMaker::GetWidth() const {
281#ifdef WITH_FFMPEG
282    assert(video_st);
283    return video_st->codecpar->width;
284#else
285    return 0;
286#endif
287}
288
289int MovieMaker::GetHeight() const {
290#ifdef WITH_FFMPEG
291    assert(video_st);
292    return video_st->codecpar->height;
293#else
294    return 0;
295#endif
296}
297
298#ifdef WITH_FFMPEG
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
326bool MovieMaker::AddFrame()
327{
328#ifdef WITH_FFMPEG
329    int ret = av_frame_make_writable(frame);
330    if (ret < 0) {
331        averrno = ret;
332        return false;
333    }
334
335    enum AVPixelFormat pix_fmt = context->pix_fmt;
336
337    if (pix_fmt != AV_PIX_FMT_YUV420P) {
338        // FIXME convert...
339        abort();
340    }
341
342    int len = 3 * GetWidth();
343    {
344        // Flip image vertically
345        int h = GetHeight();
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        }
353    }
354    sws_scale(sws_ctx, &pixels, &len, 0, GetHeight(),
355              frame->data, frame->linesize);
356
357    ++frame->pts;
358
359    // Encode this frame.
360    ret = encode_frame(frame);
361    if (ret < 0) {
362        averrno = ret;
363        return false;
364    }
365#endif
366    return true;
367}
368
369bool
370MovieMaker::Close()
371{
372#ifdef WITH_FFMPEG
373    if (video_st && averrno == 0) {
374        // Flush out any remaining data.
375        int ret = encode_frame(NULL);
376        if (ret < 0) {
377            averrno = ret;
378            return false;
379        }
380        av_write_trailer(oc);
381    }
382
383    release();
384#endif
385    return true;
386}
387
388#ifdef WITH_FFMPEG
389void
390MovieMaker::release()
391{
392    // Close codec.
393    avcodec_free_context(&context);
394    av_frame_free(&frame);
395    av_free(pixels);
396    pixels = NULL;
397    sws_freeContext(sws_ctx);
398    sws_ctx = NULL;
399
400    // Free the stream.
401    avformat_free_context(oc);
402    oc = NULL;
403
404    if (fh_to_close) {
405        fclose(fh_to_close);
406        fh_to_close = NULL;
407    }
408}
409#endif
410
411MovieMaker::~MovieMaker()
412{
413#ifdef WITH_FFMPEG
414    release();
415#endif
416}
417
418const char *
419MovieMaker::get_error_string() const
420{
421#ifdef WITH_FFMPEG
422    switch (averrno) {
423        case AVERROR(EIO):
424            return "I/O error";
425        case AVERROR(EDOM):
426            return "Number syntax expected in filename";
427        case AVERROR_INVALIDDATA:
428            /* same as AVERROR_UNKNOWN: return "unknown error"; */
429            return "invalid data found";
430        case AVERROR(ENOMEM):
431            return "not enough memory";
432        case AVERROR(EILSEQ):
433            return "unknown format";
434        case AVERROR(ENOSYS):
435            return "Operation not supported";
436        case AVERROR(ENOENT):
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    }
451#endif
452    return "Unknown error";
453}
454
455#else
456
457#include "moviemaker-legacy.cc"
458
459#endif
Note: See TracBrowser for help on using the repository browser.