diff --git a/src/celestia/CMakeLists.txt b/src/celestia/CMakeLists.txt index 1fb89792e..978244d2e 100644 --- a/src/celestia/CMakeLists.txt +++ b/src/celestia/CMakeLists.txt @@ -23,7 +23,8 @@ set(CELESTIA_SOURCES if(ENABLE_FFMPEG) list(APPEND CELESTIA_SOURCES - moviecapture.cpp + ffmpegcapture.cpp + ffmpegcapture.h moviecapture.h ) endif() diff --git a/src/celestia/celestiacore.cpp b/src/celestia/celestiacore.cpp index 8397fda29..1671274ff 100644 --- a/src/celestia/celestiacore.cpp +++ b/src/celestia/celestiacore.cpp @@ -87,13 +87,6 @@ static const float MinimumFOV = degToRad(0.001f); static float KeyRotationAccel = degToRad(120.0f); static float MouseRotationSensitivity = degToRad(1.0f); -#ifdef USE_FFMPEG -enum CodecID -{ - CEL_CODEC_ID_FFVHUFF = AV_CODEC_ID_FFVHUFF, - CEL_CODEC_ID_H264 = AV_CODEC_ID_H264, -}; -#endif static void warning(string s) { @@ -176,10 +169,8 @@ CelestiaCore::CelestiaCore() : CelestiaCore::~CelestiaCore() { -#ifdef USE_FFMPEG if (movieCapture != nullptr) recordEnd(); -#endif delete timer; delete renderer; @@ -816,7 +807,6 @@ void CelestiaCore::keyDown(int key, int modifiers) case Key_F7: sim->setTargetSpeed(1.0_ly); break; -#ifdef USE_FFMPEG case Key_F11: if (movieCapture != nullptr) { @@ -830,7 +820,6 @@ void CelestiaCore::keyDown(int key, int modifiers) if (movieCapture != nullptr) recordEnd(); break; -#endif case Key_NumPad2: case Key_NumPad4: case Key_NumPad6: @@ -1926,7 +1915,6 @@ void CelestiaCore::tick() // The time step is normally driven by the system clock; however, when // recording a movie, we fix the time step the frame rate of the movie. double dt = 0.0; -#ifdef USE_FFMPEG if (movieCapture != nullptr && recording) { dt = 1.0 / movieCapture->getFrameRate(); @@ -1935,9 +1923,6 @@ void CelestiaCore::tick() { dt = sysTime - lastTime; } -#else - dt = sysTime - lastTime; -#endif // Pause script execution if (scriptState == ScriptPaused) @@ -2147,10 +2132,8 @@ void CelestiaCore::draw() if (toggleAA) renderer->enableMSAA(); -#ifdef USE_FFMPEG if (movieCapture != nullptr && recording) movieCapture->captureFrame(); -#endif // Frame rate counter nFrames++; @@ -3523,7 +3506,6 @@ void CelestiaCore::renderOverlay() overlay->setFont(font); } -#ifdef USE_FFMPEG if (movieCapture != nullptr) { int movieWidth = movieCapture->getWidth(); @@ -3571,7 +3553,6 @@ void CelestiaCore::renderOverlay() overlay->restorePos(); } -#endif if (editMode) { @@ -4417,33 +4398,25 @@ void CelestiaCore::setOverlayElements(int _overlayElements) overlayElements = _overlayElements; } -#ifdef USE_FFMPEG -bool CelestiaCore::initMovieCapture(const fs::path &path, int width, int height, - float fps, int64_t bitrate, int codec) +void CelestiaCore::initMovieCapture(MovieCapture* mc) { - if (movieCapture != nullptr) - return false; - - movieCapture = make_unique(getRenderer()); - movieCapture->setVideoCodec(static_cast(codec)); - movieCapture->setBitRate(bitrate); - if (codec == CEL_CODEC_ID_H264) - movieCapture->setEncoderOptions(getConfig()->x264EncoderOptions); - else - movieCapture->setEncoderOptions(getConfig()->ffvhEncoderOptions); - - return movieCapture->start(path, width, height, fps); + if (movieCapture == nullptr) + movieCapture = mc; } void CelestiaCore::recordBegin() { if (movieCapture != nullptr) + { recording = true; + movieCapture->recordingStatus(true); + } } void CelestiaCore::recordPause() { recording = false; + if (movieCapture != nullptr) movieCapture->recordingStatus(false); } void CelestiaCore::recordEnd() @@ -4452,6 +4425,7 @@ void CelestiaCore::recordEnd() { recordPause(); movieCapture->end(); + delete movieCapture; movieCapture = nullptr; } } @@ -4465,7 +4439,6 @@ bool CelestiaCore::isRecording() { return recording; } -#endif void CelestiaCore::flash(const string& s, double duration) { @@ -4779,43 +4752,3 @@ void CelestiaCore::setLogFile(const fs::path &fn) fmt::fprintf(cerr, "Unable to open log file %s\n", fn.string()); } } - -#ifdef USE_FFMPEG -auto CelestiaCore::getSupportedMovieSizes() const - -> celestia::util::array_view -{ - static std::array MovieSizes = - {{ - { 320, 240 }, - { 640, 480 }, - { 720, 480 }, - { 720, 576 }, - { 1024, 768 }, - { 1280, 720 }, - { 1920, 1080 }, - { 3840, 2160 } - }}; - return MovieSizes; -} - -auto CelestiaCore::getSupportedMovieFramerates() const - -> celestia::util::array_view -{ - static std::array MovieFramerates = - { - 15.0f, 24.0f, 25.0f, 29.97f, 30.0f - }; - return MovieFramerates; -} - -auto CelestiaCore::getSupportedMovieCodecs() const - -> celestia::util::array_view -{ - static std::array MovieCodecs = - {{ - { CEL_CODEC_ID_FFVHUFF, N_("Lossless") }, - { CEL_CODEC_ID_H264, N_("Lossy (H.264)") } - }}; - return MovieCodecs; -} -#endif diff --git a/src/celestia/celestiacore.h b/src/celestia/celestiacore.h index 0e78931b0..3b9ac5408 100644 --- a/src/celestia/celestiacore.h +++ b/src/celestia/celestiacore.h @@ -12,7 +12,6 @@ #include #include -#include #include #include #include @@ -29,9 +28,7 @@ #include "configfile.h" #include "favorites.h" #include "destination.h" -#ifdef USE_FFMPEG #include "moviecapture.h" -#endif #include "view.h" #ifdef CELX #include @@ -58,18 +55,6 @@ public: virtual void update(const std::string&) = 0; }; -struct MovieSize -{ - int width; - int height; -}; - -struct MovieCodec -{ - int codecId; - const char *codecDescr; -}; - class CelestiaCore // : public Watchable { public: @@ -259,17 +244,12 @@ class CelestiaCore // : public Watchable void setTextEnterMode(int); int getTextEnterMode() const; -#ifdef USE_FFMPEG - bool initMovieCapture(const fs::path &path, int width, int height, float fps, int64_t bitrate, int codec); + void initMovieCapture(MovieCapture*); void recordBegin(); void recordPause(); void recordEnd(); bool isCaptureActive(); bool isRecording(); - celestia::util::array_view getSupportedMovieSizes() const; - celestia::util::array_view getSupportedMovieFramerates() const; - celestia::util::array_view getSupportedMovieCodecs() const; -#endif void runScript(const fs::path& filename, bool i18n = true); void cancelScript(); @@ -491,10 +471,8 @@ class CelestiaCore // : public Watchable bool shiftKeysPressed[KeyCount]; double KeyAccel{ 1.0 }; -#ifdef USE_FFMPEG - std::unique_ptr movieCapture; + MovieCapture* movieCapture{ nullptr }; bool recording{ false }; -#endif Alerter* alerter{ nullptr }; std::vector watchers; diff --git a/src/celestia/moviecapture.cpp b/src/celestia/ffmpegcapture.cpp similarity index 88% rename from src/celestia/moviecapture.cpp rename to src/celestia/ffmpegcapture.cpp index ac44a89b0..123fd40f9 100644 --- a/src/celestia/moviecapture.cpp +++ b/src/celestia/ffmpegcapture.cpp @@ -1,13 +1,10 @@ #define AVCODEC_DEBUG 0 -#include -#include -#include +#include "ffmpegcapture.h" #define __STDC_CONSTANT_MACROS extern "C" { -#include #include #include #include @@ -15,18 +12,17 @@ extern "C" #include } -#include -#include "moviecapture.h" +#include +#include +#include using namespace std; -namespace celestia -{ // a wrapper around a single output AVStream -class MovieCapturePrivate +class FFMPEGCapturePrivate { - MovieCapturePrivate() = default; - ~MovieCapturePrivate(); + FFMPEGCapturePrivate() = default; + ~FFMPEGCapturePrivate(); bool init(const fs::path& fn); bool addStream(int w, int h, float fps); @@ -45,7 +41,7 @@ class MovieCapturePrivate AVFrame *tmpfr { nullptr }; AVCodecContext *enc { nullptr }; AVFormatContext *oc { nullptr }; - const AVCodec *vc { nullptr }; + AVCodec *vc { nullptr }; AVPacket *pkt { nullptr }; SwsContext *swsc { nullptr }; @@ -54,7 +50,7 @@ class MovieCapturePrivate // pts of the next frame that will be generated int64_t nextPts { 0 }; // requested bitrate - int64_t bitrate { 400000 }; + int64_t bit_rate { 400000 }; AVCodecID vc_id { AV_CODEC_ID_FFVHUFF }; AVPixelFormat format { AV_PIX_FMT_NONE }; @@ -69,22 +65,22 @@ class MovieCapturePrivate #if (LIBAVCODEC_VERSION_INT < AV_VERSION_INT(58, 10, 100)) // ffmpeg < 4.0 static bool registered; #endif - friend class MovieCapture; + friend class FFMPEGCapture; }; #if (LIBAVCODEC_VERSION_INT < AV_VERSION_INT(58, 10, 100)) // ffmpeg < 4.0 -bool MovieCapturePrivate::registered = false; +bool FFMPEGCapturePrivate::registered = false; #endif -bool MovieCapturePrivate::init(const fs::path& filename) +bool FFMPEGCapturePrivate::init(const fs::path& filename) { this->filename = filename; #if (LIBAVCODEC_VERSION_INT < AV_VERSION_INT(58, 10, 100)) // ffmpeg < 4.0 - if (!MovieCapturePrivate::registered) + if (!FFMPEGCapturePrivate::registered) { av_register_all(); - MovieCapturePrivate::registered = true; + FFMPEGCapturePrivate::registered = true; } #endif @@ -96,7 +92,7 @@ bool MovieCapturePrivate::init(const fs::path& filename) return oc != nullptr; } -bool MovieCapturePrivate::isSupportedPixelFormat(enum AVPixelFormat format) const +bool FFMPEGCapturePrivate::isSupportedPixelFormat(enum AVPixelFormat format) const { const enum AVPixelFormat *p = vc->pix_fmts; if (p == nullptr) @@ -193,7 +189,7 @@ static void listEncoderParameters(const AVCodec *vc) } #endif -int MovieCapturePrivate::writePacket() +int FFMPEGCapturePrivate::writePacket() { // rescale output packet timestamp values from codec to stream timebase av_packet_rescale_ts(pkt, enc->time_base, st->time_base); @@ -204,7 +200,7 @@ int MovieCapturePrivate::writePacket() } // add an output stream -bool MovieCapturePrivate::addStream(int width, int height, float fps) +bool FFMPEGCapturePrivate::addStream(int width, int height, float fps) { this->fps = fps; @@ -235,12 +231,13 @@ bool MovieCapturePrivate::addStream(int width, int height, float fps) return false; } - enc->codec_id = vc_id; - enc->bit_rate = bitrate; + enc->codec_id = oc->oformat->video_codec = vc_id; + + enc->bit_rate = bit_rate; #if 0 enc->rc_min_rate = ...; enc->rc_max_rate = ...; - enc->bitrate_tolerance = 0; + enc->bit_rate_tolerance = 0; #endif // Resolution must be a multiple of two enc->width = width; @@ -290,7 +287,7 @@ bool MovieCapturePrivate::addStream(int width, int height, float fps) return true; } -bool MovieCapturePrivate::start() +bool FFMPEGCapturePrivate::start() { // open the output file, if needed if ((oc->oformat->flags & AVFMT_NOFILE) == 0) @@ -320,7 +317,7 @@ bool MovieCapturePrivate::start() return true; } -bool MovieCapturePrivate::openVideo() +bool FFMPEGCapturePrivate::openVideo() { AVDictionary *opts = nullptr; const char *str = ""; @@ -419,7 +416,7 @@ static void captureImage(AVFrame *pict, int width, int height, const Renderer *r // encode one video frame and send it to the muxer // return 1 when encoding is finished, 0 otherwise -bool MovieCapturePrivate::writeVideoFrame(bool finalize) +bool FFMPEGCapturePrivate::writeVideoFrame(bool finalize) { AVFrame *frame = finalize ? nullptr : this->frame; const int bytesPerPixel = hasAlpha ? 4 : 3; @@ -451,9 +448,7 @@ bool MovieCapturePrivate::writeVideoFrame(bool finalize) frame->pts = nextPts++; } -#if (LIBAVCODEC_VERSION_INT < AV_VERSION_INT(58, 133, 100)) av_init_packet(pkt); -#endif // encode the image if (avcodec_send_frame(enc, frame) < 0) @@ -485,7 +480,7 @@ bool MovieCapturePrivate::writeVideoFrame(bool finalize) return true; } -void MovieCapturePrivate::finish() +void FFMPEGCapturePrivate::finish() { writeVideoFrame(true); @@ -499,7 +494,7 @@ void MovieCapturePrivate::finish() avio_closep(&oc->pb); } -MovieCapturePrivate::~MovieCapturePrivate() +FFMPEGCapturePrivate::~FFMPEGCapturePrivate() { avcodec_free_context(&enc); av_frame_free(&frame); @@ -509,40 +504,41 @@ MovieCapturePrivate::~MovieCapturePrivate() av_packet_free(&pkt); } -MovieCapture::MovieCapture(const Renderer *r) : - d(new MovieCapturePrivate) +FFMPEGCapture::FFMPEGCapture(const Renderer *r) : + MovieCapture(r), + d(new FFMPEGCapturePrivate) { d->renderer = r; d->hasAlpha = r->getPreferredCaptureFormat() == PixelFormat::RGBA; d->format = d->hasAlpha ? AV_PIX_FMT_RGBA : AV_PIX_FMT_RGB24; } -MovieCapture::~MovieCapture() +FFMPEGCapture::~FFMPEGCapture() { delete d; } -int MovieCapture::getFrameCount() const +int FFMPEGCapture::getFrameCount() const { return d->nextPts; } -int MovieCapture::getWidth() const +int FFMPEGCapture::getWidth() const { return d->enc->width; } -int MovieCapture::getHeight() const +int FFMPEGCapture::getHeight() const { return d->enc->height; } -float MovieCapture::getFrameRate() const +float FFMPEGCapture::getFrameRate() const { return d->fps; } -bool MovieCapture::start(const fs::path& filename, int width, int height, float fps) +bool FFMPEGCapture::start(const fs::path& filename, int width, int height, float fps) { if (!d->init(filename) || !d->addStream(width, height, fps) || @@ -557,7 +553,7 @@ bool MovieCapture::start(const fs::path& filename, int width, int height, float return true; } -bool MovieCapture::end() +bool FFMPEGCapture::end() { if (!d->capturing) return false; @@ -569,23 +565,22 @@ bool MovieCapture::end() return true; } -bool MovieCapture::captureFrame() +bool FFMPEGCapture::captureFrame() { return d->capturing && d->writeVideoFrame(); } -void MovieCapture::setVideoCodec(AVCodecID vc_id) +void FFMPEGCapture::setVideoCodec(AVCodecID vc_id) { d->vc_id = vc_id; } -void MovieCapture::setBitRate(int64_t bitrate) +void FFMPEGCapture::setBitRate(int64_t bit_rate) { - d->bitrate = bitrate; + d->bit_rate = bit_rate; } -void MovieCapture::setEncoderOptions(const std::string &s) +void FFMPEGCapture::setEncoderOptions(const std::string &s) { d->vc_options = s; } -} diff --git a/src/celestia/ffmpegcapture.h b/src/celestia/ffmpegcapture.h new file mode 100644 index 000000000..4c0defe73 --- /dev/null +++ b/src/celestia/ffmpegcapture.h @@ -0,0 +1,39 @@ +#pragma once + +#include "moviecapture.h" +#define __STDC_CONSTANT_MACROS +extern "C" +{ +#include +} + +#include + +class FFMPEGCapturePrivate; + +class FFMPEGCapture : public MovieCapture +{ + public: + FFMPEGCapture(const Renderer *r); + ~FFMPEGCapture() override; + + bool start(const fs::path&, int, int, float) override; + bool end() override; + bool captureFrame() override; + + int getFrameCount() const override; + int getWidth() const override; + int getHeight() const override; + float getFrameRate() const override; + + void setAspectRatio(int, int) override {}; + void setQuality(float) override {}; + void recordingStatus(bool) override {}; + + void setVideoCodec(AVCodecID); + void setBitRate(int64_t); + void setEncoderOptions(const std::string&); + + private: + FFMPEGCapturePrivate *d{ nullptr }; +}; diff --git a/src/celestia/gtk/actions.cpp b/src/celestia/gtk/actions.cpp index 5abc03e16..1414cd2c0 100644 --- a/src/celestia/gtk/actions.cpp +++ b/src/celestia/gtk/actions.cpp @@ -30,6 +30,9 @@ #include #include #include +#ifdef USE_FFMPEG +#include +#endif #include "actions.h" #include "common.h" @@ -53,14 +56,41 @@ using namespace std; static void openScript(const char* filename, AppData* app); static void captureImage(const char* filename, AppData* app); #ifdef USE_FFMPEG -static void captureMovie(const char* filename, int w, int h, float fps, - int codec, int64_t bitrate, AppData* app); +static void captureMovie(const char* filename, const int resolution[], float fps, + AVCodecID codec, float bitrate, AppData* app); #endif static void textInfoDialog(const char *txt, const char *title, AppData* app); static void setRenderFlag(AppData* a, uint64_t flag, gboolean state); static void setOrbitMask(AppData* a, int mask, gboolean state); static void setLabelMode(AppData* a, int mode, gboolean state); +#ifdef USE_FFMPEG +static const int MovieSizes[][2] = +{ + { 160, 120 }, + { 320, 240 }, + { 640, 480 }, + { 720, 480 }, + { 720, 576 }, + { 1024, 768 }, + { 1280, 720 }, + { 1920, 1080 } +}; + +static const float MovieFramerates[] = { 15.0f, 23.976f, 24.0f, 25.0f, 29.97f, 30.0f, 60.0f }; + +struct MovieCodec +{ + AVCodecID codecId; + const char *codecDesc; +}; + +static MovieCodec MovieCodecs[2] = +{ + { AV_CODEC_ID_FFVHUFF, N_("Lossless") }, + { AV_CODEC_ID_H264, N_("Lossy (H.264)") } +}; + static void insert_text_event(GtkEditable *editable, const gchar *text, gint length, gint *position, gpointer data) { for (int i = 0; i < length; i++) @@ -72,6 +102,7 @@ static void insert_text_event(GtkEditable *editable, const gchar *text, gint len } } } +#endif /* File -> Copy URL */ void actionCopyURL(GtkAction*, AppData* app) @@ -237,12 +268,10 @@ void actionCaptureMovie(GtkAction*, AppData* app) gtk_box_pack_start(GTK_BOX(hbox), rlabel, TRUE, TRUE, 0); GtkWidget* vscombo = gtk_combo_box_text_new(); - auto movieSizes = app->core->getSupportedMovieSizes(); - for (const auto& size : movieSizes) + for (const auto& size : MovieSizes) { - auto s = fmt::format("{} x {}", size.width, size.height); gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(vscombo), - s.c_str()); + fmt::format("{} x {}", size[0], size[1]).c_str()); } gtk_combo_box_set_active(GTK_COMBO_BOX(vscombo), 0); gtk_box_pack_start(GTK_BOX(hbox), vscombo, FALSE, FALSE, 0); @@ -251,8 +280,7 @@ void actionCaptureMovie(GtkAction*, AppData* app) gtk_box_pack_start(GTK_BOX(hbox), flabel, TRUE, TRUE, 0); GtkWidget* frcombo = gtk_combo_box_text_new(); - auto movieFramerates = app->core->getSupportedMovieFramerates(); - for (float i : movieFramerates) + for (float i : MovieFramerates) { gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(frcombo), fmt::format("{:.3f}", i).c_str()); @@ -264,11 +292,10 @@ void actionCaptureMovie(GtkAction*, AppData* app) gtk_box_pack_start(GTK_BOX(hbox), vclabel, TRUE, TRUE, 0); GtkWidget* vccombo = gtk_combo_box_text_new(); - auto movieCodecs = app->core->getSupportedMovieCodecs(); - for (const auto &mcodec : movieCodecs) + for (const auto &mcodec : MovieCodecs) { gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(vccombo), - mcodec.codecDescr); + mcodec.codecDesc); } gtk_combo_box_set_active(GTK_COMBO_BOX(vccombo), 0); gtk_box_pack_start(GTK_BOX(hbox), vccombo, FALSE, FALSE, 0); @@ -290,17 +317,17 @@ void actionCaptureMovie(GtkAction*, AppData* app) int fridx = gtk_combo_box_get_active(GTK_COMBO_BOX(frcombo)); int vcidx = gtk_combo_box_get_active(GTK_COMBO_BOX(vccombo)); const gchar *brtext = gtk_entry_get_text(GTK_ENTRY(brentry)); - const auto &dim = movieSizes[vsidx]; - float fps = movieFramerates[fridx]; - int codec = movieCodecs[vcidx].codecId; - int64_t bitrate = 400000; + const int *resolution = MovieSizes[vsidx]; + float fps = MovieFramerates[fridx]; + AVCodecID codec = MovieCodecs[vcidx].codecId; + float bitrate = 400000; const gchar *last = &brtext[gtk_entry_get_text_length(GTK_ENTRY(brentry))]; std::from_chars(brtext, last, bitrate); gtk_widget_destroy(fs); for (int i=0; i < 10 && gtk_events_pending ();i++) gtk_main_iteration (); - captureMovie(filename, dim.width, dim.height, fps, codec, bitrate, app); + captureMovie(filename, resolution, fps, codec, bitrate, app); g_free(filename); } else @@ -1085,17 +1112,26 @@ static void captureImage(const char* filename, AppData* app) /* Image capturing helper called by actionCaptureImage() */ #ifdef USE_FFMPEG -static void captureMovie(const char* filename, - int width, int height, - float fps, int codec, - int64_t bitrate, AppData* app) +static void captureMovie(const char* filename, const int resolution[], float fps, + AVCodecID codec, float bitrate, AppData* app) { - bool ok = app->core->initMovieCapture(filename, - width, height, - fps, bitrate, - codec); - if (!ok) + auto* movieCapture = new FFMPEGCapture(app->renderer); + movieCapture->setVideoCodec(codec); + movieCapture->setBitRate(bitrate); + if (codec == AV_CODEC_ID_H264) + movieCapture->setEncoderOptions(app->core->getConfig()->x264EncoderOptions); + else + movieCapture->setEncoderOptions(app->core->getConfig()->ffvhEncoderOptions); + + bool success = movieCapture->start(filename, resolution[0], resolution[1], fps); + if (success) { + app->core->initMovieCapture(movieCapture); + } + else + { + delete movieCapture; + GtkWidget* errBox = gtk_message_dialog_new(GTK_WINDOW(app->mainWindow), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, diff --git a/src/celestia/moviecapture.h b/src/celestia/moviecapture.h index bc1003e45..fae9836bd 100644 --- a/src/celestia/moviecapture.h +++ b/src/celestia/moviecapture.h @@ -1,34 +1,44 @@ -#pragma once +// moviecapture.h +// +// Copyright (C) 2001, Chris Laurel +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. -#define __STDC_CONSTANT_MACROS -extern "C" -{ -#include -} +#ifndef _MOVIECAPTURE_H_ +#define _MOVIECAPTURE_H_ + +#include +#include +#include +#include -namespace celestia -{ -class MovieCapturePrivate; class MovieCapture { public: - MovieCapture(const Renderer *r); - ~MovieCapture(); + MovieCapture(const Renderer *r) : renderer(r) {}; + virtual ~MovieCapture() {}; - bool start(const fs::path&, int, int, float); - bool end(); - bool captureFrame(); + virtual bool start(const fs::path& filename, + int width, int height, + float fps) = 0; + virtual bool end() = 0; + virtual bool captureFrame() = 0; - int getFrameCount() const; - int getWidth() const; - int getHeight() const; - float getFrameRate() const; + virtual int getFrameCount() const = 0; + virtual int getWidth() const = 0; + virtual int getHeight() const = 0; + virtual float getFrameRate() const = 0; - void setVideoCodec(AVCodecID); - void setBitRate(int64_t); - void setEncoderOptions(const std::string&); + virtual void setAspectRatio(int aspectNumerator, int aspectDenominator) = 0; + virtual void setQuality(float) = 0; + virtual void recordingStatus(bool started) = 0; /* to update UI recording status indicator */ - private: - MovieCapturePrivate *d{ nullptr }; + protected: + const Renderer *renderer{ nullptr }; }; -} + +#endif // _MOVIECAPTURE_H_ + diff --git a/src/celestia/qt/qtappwin.cpp b/src/celestia/qt/qtappwin.cpp index 27837bfa7..18aaa85af 100644 --- a/src/celestia/qt/qtappwin.cpp +++ b/src/celestia/qt/qtappwin.cpp @@ -11,6 +11,7 @@ // of the License, or (at your option) any later version. +//#include #include #include @@ -64,6 +65,10 @@ #include #include "qtbookmark.h" +#ifdef USE_FFMPEG +#include "celestia/ffmpegcapture.h" +#endif + #ifndef CONFIG_DATA_DIR #define CONFIG_DATA_DIR "./" #endif @@ -84,6 +89,22 @@ static const int CELESTIA_MAIN_WINDOW_VERSION = 12; static int fps_to_ms(int fps) { return fps > 0 ? 1000 / fps : 0; } static int ms_to_fps(int ms) { return ms > 0? 1000 / ms : 0; } +#ifdef USE_FFMPEG +static const int videoSizes[][2] = +{ + { 160, 120 }, + { 320, 240 }, + { 640, 480 }, + { 720, 480 }, + { 720, 576 }, + { 1024, 768 }, + { 1280, 720 }, + { 1920, 1080 } +}; + +static const float videoFrameRates[] = { 15.0f, 23.976f, 24.0f, 25.0f, 29.97f, 30.0f, 60.0f }; +#endif + // Progress notifier class receives update messages from CelestiaCore // at startup. This simple implementation just forwards messages on // to the main Celestia window. @@ -644,27 +665,20 @@ void CelestiaAppWindow::slotCaptureVideo() QComboBox* resolutionCombo = new QComboBox(&videoInfoDialog); layout->addWidget(new QLabel(_("Resolution:"), &videoInfoDialog), 0, 0); layout->addWidget(resolutionCombo, 0, 1); - auto videoSizes = m_appCore->getSupportedMovieSizes(); for (const auto& size : videoSizes) - { - int w = size.width; - int h = size.height; - resolutionCombo->addItem(QString(_("%1 x %2")).arg(w).arg(h), QSize(w, h)); - } + resolutionCombo->addItem(QString(_("%1 x %2")).arg(size[0]).arg(size[1]), QSize(size[0], size[1])); QComboBox* frameRateCombo = new QComboBox(&videoInfoDialog); layout->addWidget(new QLabel(_("Frame rate:"), &videoInfoDialog), 1, 0); layout->addWidget(frameRateCombo, 1, 1); - auto videoFrameRates = m_appCore->getSupportedMovieFramerates(); for (float i : videoFrameRates) frameRateCombo->addItem(QString::number(i), i); QComboBox* codecCombo = new QComboBox(&videoInfoDialog); layout->addWidget(new QLabel(_("Video codec:"), &videoInfoDialog), 2, 0); layout->addWidget(codecCombo, 2, 1); - auto videoCodecs = m_appCore->getSupportedMovieCodecs(); - for (const auto &c : videoCodecs) - codecCombo->addItem(_(c.codecDescr), c.codecId); + codecCombo->addItem(_("Lossless"), AV_CODEC_ID_FFVHUFF); + codecCombo->addItem(_("Lossy (H.264)"), AV_CODEC_ID_H264); QLineEdit* bitrateEdit = new QLineEdit("400000", &videoInfoDialog); bitrateEdit->setInputMask("D000000000"); @@ -682,12 +696,24 @@ void CelestiaAppWindow::slotCaptureVideo() { QSize videoSize = resolutionCombo->itemData(resolutionCombo->currentIndex()).toSize(); float frameRate = frameRateCombo->itemData(frameRateCombo->currentIndex()).toFloat(); - int codec = codecCombo->itemData(codecCombo->currentIndex()).toInt(); - int64_t bitrate = bitrateEdit->text().toLongLong(); + AVCodecID vc = static_cast(codecCombo->itemData(codecCombo->currentIndex()).toInt()); + int br = bitrateEdit->text().toLongLong(); - m_appCore->initMovieCapture(saveAsName.toStdString(), - videoSize.width(), videoSize.height(), - frameRate, bitrate, codec); + auto *movieCapture = new FFMPEGCapture(m_appCore->getRenderer()); + movieCapture->setVideoCodec(vc); + movieCapture->setBitRate(br); + if (vc == AV_CODEC_ID_H264) + movieCapture->setEncoderOptions(m_appCore->getConfig()->x264EncoderOptions); + else + movieCapture->setEncoderOptions(m_appCore->getConfig()->ffvhEncoderOptions); + + bool ok = movieCapture->start(saveAsName.toStdString(), + videoSize.width(), videoSize.height(), + frameRate); + if (ok) + m_appCore->initMovieCapture(movieCapture); + else + delete movieCapture; } settings.beginGroup("Preferences"); diff --git a/src/celestia/win32/res/celestia.rc b/src/celestia/win32/res/celestia.rc index 51db0ae67..d457cc5e8 100644 --- a/src/celestia/win32/res/celestia.rc +++ b/src/celestia/win32/res/celestia.rc @@ -488,9 +488,9 @@ FONT 8, "Segoe UI", 0, 0, 0 COMBOBOX IDC_COMBO_MOVIE_SIZE, 38, 2, 82, 93, WS_TABSTOP | WS_VSCROLL | CBS_DROPDOWNLIST, WS_EX_LEFT RTEXT "Frame rate:", IDC_STATIC, 120, 4, 74, 8, SS_RIGHT, WS_EX_LEFT COMBOBOX IDC_COMBO_MOVIE_FRAMERATE, 196, 2, 82, 90, WS_TABSTOP | WS_VSCROLL | CBS_DROPDOWNLIST, WS_EX_LEFT - LTEXT "Codec:", IDC_STATIC, 6, 20, 30, 8, SS_LEFT, WS_EX_LEFT - COMBOBOX IDC_COMBO_MOVIE_CODEC, 38, 18, 82, 90, WS_TABSTOP | WS_VSCROLL | CBS_DROPDOWNLIST, WS_EX_LEFT - RTEXT "Bitrate:", IDC_STATIC, 120, 20, 74, 8, SS_RIGHT, WS_EX_LEFT + LTEXT "Codec:", 0, 6, 20, 30, 8, SS_LEFT, WS_EX_LEFT + COMBOBOX IDC_COMBO_MOVIE_CODEC, 38, 18, 82, 30, CBS_DROPDOWNLIST | CBS_HASSTRINGS, WS_EX_LEFT + RTEXT "Bitrate:", 0, 120, 20, 74, 8, SS_RIGHT, WS_EX_LEFT EDITTEXT IDC_EDIT_MOVIE_BITRATE, 196, 18, 82, 14, ES_AUTOHSCROLL | ES_NUMBER, WS_EX_LEFT } diff --git a/src/celestia/win32/winmain.cpp b/src/celestia/win32/winmain.cpp index 001dc43ba..46bd5d911 100644 --- a/src/celestia/win32/winmain.cpp +++ b/src/celestia/win32/winmain.cpp @@ -22,10 +22,8 @@ #include #include #include -#include // GET_X_LPARAM, GET_Y_LPARAM #include #include -#include // IDropTarget #include #include @@ -44,6 +42,7 @@ #include #include "celestia/celestiacore.h" +#include "celestia/ffmpegcapture.h" #include "celestia/helper.h" #include "celestia/scriptmenu.h" #include "celestia/url.h" @@ -128,6 +127,31 @@ static POINT lastMouseMove; class WinCursorHandler; WinCursorHandler* cursorHandler = NULL; +static int MovieSizes[8][2] = { + { 160, 120 }, + { 320, 240 }, + { 640, 480 }, + { 720, 480 }, + { 720, 576 }, + { 1024, 768 }, + { 1280, 720 }, + { 1920, 1080 } + }; + +static float MovieFramerates[5] = { 15.0f, 24.0f, 25.0f, 29.97f, 30.0f }; + +struct MovieCodec +{ + AVCodecID codecId; + const char *codecDesc; +}; + +static MovieCodec MovieCodecs[2] = +{ + { AV_CODEC_ID_FFVHUFF, N_("Lossless") }, + { AV_CODEC_ID_H264, N_("Lossy (H.264)") } +}; + static int movieSize = 1; static int movieFramerate = 1; static int movieCodec = 1; @@ -417,6 +441,31 @@ static void ShowLocalTime(CelestiaCore* appCore) } +static bool BeginMovieCapture(const Renderer* renderer, + const std::string& filename, + int width, int height, + float framerate, + AVCodecID codec, + int64_t bitrate) +{ + auto* movieCapture = new FFMPEGCapture(renderer); + movieCapture->setVideoCodec(codec); + movieCapture->setBitRate(bitrate); + if (vc == AV_CODEC_ID_H264) + movieCapture->setEncoderOptions(appCore->getConfig()->x264EncoderOptions); + else + movieCapture->setEncoderOptions(appCore->getConfig()->ffvhEncoderOptions); + + bool success = movieCapture->start(filename, width, height, framerate); + if (success) + appCore->initMovieCapture(movieCapture); + else + delete movieCapture; + + return success; +} + + static bool CopyStateURLToClipboard() { BOOL b; @@ -628,7 +677,6 @@ BOOL APIENTRY GLInfoProc(HWND hDlg, } -#ifdef USE_FFMPEG UINT CALLBACK ChooseMovieParamsProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) { @@ -638,32 +686,33 @@ UINT CALLBACK ChooseMovieParamsProc(HWND hDlg, UINT message, { char buf[100]; HWND hwnd = GetDlgItem(hDlg, IDC_COMBO_MOVIE_SIZE); + int nSizes = sizeof MovieSizes / sizeof MovieSizes[0]; - auto movieSizes = appCore->getSupportedMovieSizes(); - for (auto const &s : movieSizes) + for (int i = 0; i < nSizes; i++) { - sprintf(buf, _("%d x %d"), s.width, s.height); + sprintf(buf, _("%d x %d"), MovieSizes[i][0], MovieSizes[i][1]); SendMessage(hwnd, CB_INSERTSTRING, -1, reinterpret_cast(buf)); } SendMessage(hwnd, CB_SETCURSEL, movieSize, 0); hwnd = GetDlgItem(hDlg, IDC_COMBO_MOVIE_FRAMERATE); - auto movieFramerates = appCore->getSupportedMovieFramerates(); - for (float fps : movieFramerates) + int nFramerates = sizeof MovieFramerates / sizeof MovieFramerates[0]; + for (int i = 0; i < nFramerates; i++) { - sprintf(buf, "%.2f", fps); + sprintf(buf, "%.2f", MovieFramerates[i]); SendMessage(hwnd, CB_INSERTSTRING, -1, reinterpret_cast(buf)); } SendMessage(hwnd, CB_SETCURSEL, movieFramerate, 0); hwnd = GetDlgItem(hDlg, IDC_COMBO_MOVIE_CODEC); - auto movieCodecs = appCore->getSupportedMovieCodecs(); - for (auto &c : movieCodecs) + int nCodecs = sizeof MovieCodecs / sizeof MovieCodecs[0]; + for (int i = 0; i < nCodecs; i++) { - SendMessage(hwnd, CB_INSERTSTRING, -1, - reinterpret_cast(_(c.codecDescr))); + SendMessage(hwnd, CB_INSERTSTRING, + reinterpret_cast(MovieCodecs[i].codecId), + reinterpret_cast(_(MovieCodecs[i].codecDesc))); } SendMessage(hwnd, CB_SETCURSEL, movieCodec, 0); @@ -705,15 +754,14 @@ UINT CALLBACK ChooseMovieParamsProc(HWND hDlg, UINT message, movieCodec = item; } } - else if (LOWORD(wParam) == IDOK) + else if (LOWORD(wParam) == IDOK IDC_EDIT_MOVIE_BITRATE) { char buf[24], out[24]; wchar_t wbuff[48]; - int wlen = 0; int len = GetDlgItemText(hDlg, IDC_EDIT_MOVIE_BITRATE, buf, sizeof(buf)); if (len > 0) { - wlen = MultiByteToWideChar(CP_ACP, 0, buf, -1, wbuff, sizeof(wbuff)); + int wlen = MultiByteToWideChar(CP_ACP, 0, buf, -1, wbuff, sizeof(wbuff)); WideCharToMultiByte(CP_UTF8, 0, wbuff, wlen, out, sizeof(out), NULL, NULL); } @@ -728,7 +776,6 @@ UINT CALLBACK ChooseMovieParamsProc(HWND hDlg, UINT message, return FALSE; } -#endif BOOL APIENTRY FindObjectProc(HWND hDlg, @@ -2691,7 +2738,6 @@ static void HandleCaptureImage(HWND hWnd) } -#ifdef USE_FFMPEG static void HandleCaptureMovie(HWND hWnd) { // TODO: The menu item should be disable so that the user doesn't even @@ -2785,15 +2831,13 @@ static void HandleCaptureMovie(HWND hWnd) } else { - auto movieSizes = appCore->getSupportedMovieSizes(); - auto movieFramerates = appCore->getSupportedMovieFramerates(); - auto movieCodecs = appCore->getSupportedMovieCodecs(); - success = appCore->initMovieCapture(Ofn.lpstrFile, - movieSizes[movieSize].width, - movieSizes[movieSize].height, - movieFramerates[movieFramerate], - movieBitrate, - movieCodecs[movieCodec].codecId); + success = BeginMovieCapture(appCore->getRenderer(), + string(Ofn.lpstrFile), + MovieSizes[movieSize][0], + MovieSizes[movieSize][1], + MovieFramerates[movieFramerate], + MovieCodecs[movieCodec], + movieBitrate); } if (!success) @@ -2809,7 +2853,6 @@ static void HandleCaptureMovie(HWND hWnd) } } } -#endif static void HandleOpenScript(HWND hWnd, CelestiaCore* appCore) @@ -4268,11 +4311,9 @@ LRESULT CALLBACK MainWindowProc(HWND hWnd, HandleCaptureImage(hWnd); break; -#ifdef USE_FFMPEG case ID_FILE_CAPTUREMOVIE: HandleCaptureMovie(hWnd); break; -#endif case ID_FILE_EXIT: SendMessage(hWnd, WM_CLOSE, 0, 0);