[graphics/krita/krita/6.0] /: [android] Implement animation video rendering

Carsten Hartenfels <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit 3cf46b57a785c44c5a1d911c5eefb7ea0cd88a0c by Carsten Hartenfels.
Committed on 27/07/2026 at 17:19.
Pushed by hartenfels into branch 'krita/6.0'.

[android] Implement animation video rendering

Through the Android Media Encoder API and libav, like it is done for
timelapse exports. Does not yet support audio.

M  +2    -1    libs/ui/animation/KisAndroidMediaEncoderRunnable.cpp
M  +110  -26   libs/ui/animation/KisAnimationRender.cpp
M  +1    -1    libs/ui/animation/KisAnimationRender.h
M  +22   -8    libs/ui/animation/KisAnimationRenderingOptions.cpp
M  +16   -5    libs/ui/animation/KisAnimationRenderingOptions.h
M  +185  -65   libs/ui/animation/KisDlgAnimationRenderer.cpp
M  +30   -8    libs/ui/animation/KisDlgAnimationRenderer.h
M  +19   -0    libs/ui/animation/KisLibavEncoderContext.h
M  +2    -1    libs/ui/animation/KisLibavMediaEncoderRunnable.cpp
M  +81   -0    libs/ui/animation/KisMediaEncoderWrapper.cpp
M  +4    -0    libs/ui/animation/KisMediaEncoderWrapper.h
M  +68   -4    libs/ui/animation/KisVideoSaver.cpp
M  +12   -12   libs/ui/animation/KisVideoSaver.h
M  +17   -10   libs/ui/animation/wdg_animationrenderer.ui
M  +23   -0    plugins/dockers/compositiondocker/compositiondocker_dock.cpp
M  +1    -0    plugins/dockers/recorder/recorder_export.cpp

https://invent.kde.org/graphics/krita/-/commit/3cf46b57a785c44c5a1d911c5eefb7ea0cd88a0c

diff --git a/libs/ui/animation/KisAndroidMediaEncoderRunnable.cpp b/libs/ui/animation/KisAndroidMediaEncoderRunnable.cpp
index 06ed3515e4e..9bea0460824 100644
--- a/libs/ui/animation/KisAndroidMediaEncoderRunnable.cpp
+++ b/libs/ui/animation/KisAndroidMediaEncoderRunnable.cpp
@@ -440,6 +440,7 @@ KisMediaEncoderRunnable::EncodeResult KisAndroidMediaEncoderRunnable::encode(QSt
 
     // Encode the frames.
     Frame frame;
+    int swsFlags = ctx.getSwsFlags(settings().scaleFilter);
     while (nextFrame(frame)) {
         if (isCancelled()) {
             return EncodeResult::Cancelled;
@@ -516,7 +517,7 @@ KisMediaEncoderRunnable::EncodeResult KisAndroidMediaEncoderRunnable::encode(QSt
                                                           outputWidth,
                                                           outputHeight,
                                                           outputPixelFormat,
-                                                          SWS_FAST_BILINEAR);
+                                                          swsFlags);
             if (!swsContext) {
                 ctx.setInternalErrorMessage(QStringLiteral("sws_getCachedContext"));
                 return EncodeResult::Failed;
diff --git a/libs/ui/animation/KisAnimationRender.cpp b/libs/ui/animation/KisAnimationRender.cpp
index 78a45dd57fd..b820646ee80 100644
--- a/libs/ui/animation/KisAnimationRender.cpp
+++ b/libs/ui/animation/KisAnimationRender.cpp
@@ -24,13 +24,74 @@
 
 #include "KisVideoSaver.h"
 
+#ifdef Q_OS_ANDROID
+#include <QTemporaryDir>
+#include <memory>
+#endif
+
+namespace
+{
+
+bool looksLikeMp4(const QString &videoType)
+{
+#ifdef Q_OS_ANDROID
+    return videoType.contains(QStringLiteral("mp4"));
+#else
+    return videoType == QStringLiteral("video/mp4");
+#endif
+}
+
+bool looksLikeMatroska(const QString &videoType)
+{
+#ifdef Q_OS_ANDROID
+    return videoType.contains(QStringLiteral("matroska"));
+#else
+    return videoType == QStringLiteral("video/x-matroska");
+#endif
+}
+
+} // namespace
+
 bool KisAnimationRender::render(KisDocument *doc, KisViewManager *viewManager, KisAnimationRenderingOptions encoderOptions) {
+#ifdef Q_OS_ANDROID
+    // The user may cancel the dialog prompting them for a video file, so bail
+    // out if we don't have one here. We can't create an implicit one next to
+    // the document on Android because of file system restrictions.
+    if (encoderOptions.shouldEncodeVideo && encoderOptions.videoFileName.isEmpty()) {
+        return false;
+    }
+#endif
+
+    bool isTemporaryFramesDirectory = false;
+    QString framesDirectory;
+#ifdef Q_OS_ANDROID
+    // Android uses weird content URIs instead of file paths and isn't allowed
+    // to scribble around in the file system without asking the user for access.
+    // We'll have to take the frames directory as it is given and if we don't
+    // get one then we'll create a temporary directory to stick our frames into.
+    std::unique_ptr<QTemporaryDir> tempDir;
+    if (encoderOptions.shouldDeleteSequence || encoderOptions.directory.isEmpty()) {
+        tempDir = std::make_unique<QTemporaryDir>();
+        KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(tempDir->isValid(), false);
+        framesDirectory = tempDir->path();
+        isTemporaryFramesDirectory = true;
+    } else {
+        framesDirectory = encoderOptions.directory;
+    }
+#else
+    framesDirectory = encoderOptions.resolveAbsoluteFramesDirectory();
+#endif
+
     const QString frameMimeType = encoderOptions.frameMimeType;
-    const QString framesDirectory = encoderOptions.resolveAbsoluteFramesDirectory();
     const QString extension = KisMimeDatabase::suffixesForMimeType(frameMimeType).first();
     const QString baseFileName = QString("%1/%2.%3").arg(framesDirectory, encoderOptions.basename, extension);
 
-    if (mustHaveEvenDimensions(encoderOptions.videoMimeType, encoderOptions.renderMode())) {
+#ifdef Q_OS_ANDROID
+    QString videoType = encoderOptions.videoFormatKey;
+#else
+    QString videoType = encoderOptions.videoMimeType;
+#endif
+    if (mustHaveEvenDimensions(videoType, encoderOptions.renderMode())) {
         if (hasEvenDimensions(encoderOptions.width, encoderOptions.height) != true) {
             encoderOptions.width = encoderOptions.width + (encoderOptions.width & 0x1);
             encoderOptions.height = encoderOptions.height + (encoderOptions.height & 0x1);
@@ -39,9 +100,9 @@ bool KisAnimationRender::render(KisDocument *doc, KisViewManager *viewManager, K
 
     const QSize scaledSize = doc->image()->bounds().size().scaled(encoderOptions.width, encoderOptions.height, Qt::IgnoreAspectRatio);
 
-    if (mustHaveEvenDimensions(encoderOptions.videoMimeType, encoderOptions.renderMode())) {
+    if (mustHaveEvenDimensions(videoType, encoderOptions.renderMode())) {
         if (hasEvenDimensions(scaledSize.width(), scaledSize.height()) != true) {
-            QString type = encoderOptions.videoMimeType == "video/mp4" ? "Mpeg4 (.mp4) " : "Matroska (.mkv) ";
+            QString type = looksLikeMp4(videoType) ? "Mpeg4 (.mp4) " : "Matroska (.mkv) ";
 
             qWarning() << type <<"requires width and height to be even, resize and try again!";
             doc->setErrorMessage(i18n("%1 requires width and height to be even numbers.  Please resize or crop the image before exporting.", type));
@@ -73,6 +134,12 @@ bool KisAnimationRender::render(KisDocument *doc, KisViewManager *viewManager, K
         const QString savedFilesMask = exporter.savedFilesMask();
 
         if (encoderOptions.shouldEncodeVideo) {
+            bool videoFileWriteAllowed = true;
+            // Android's weird file system doesn't work this way, the target
+            // file path is going to be a sandbox URL. Making it absolute,
+            // creating a directory above it or checking for its existence
+            // neither make sense nor are they necessary.
+#ifndef Q_OS_ANDROID
             const QString videoOutputFilePath = encoderOptions.resolveAbsoluteVideoFilePath();
             KIS_SAFE_ASSERT_RECOVER_NOOP(QFileInfo(videoOutputFilePath).isAbsolute());
 
@@ -85,7 +152,6 @@ bool KisAnimationRender::render(KisDocument *doc, KisViewManager *viewManager, K
             KIS_SAFE_ASSERT_RECOVER_NOOP(outputDir.exists());
 
             // If file exists at output path, prompt user for overwrite..
-            bool videoFileWriteAllowed = true;
             if (videoOutputFile.exists()) {
                 QMessageBox videoOverwritePrompt;
 
@@ -96,11 +162,16 @@ bool KisAnimationRender::render(KisDocument *doc, KisViewManager *viewManager, K
 
                 videoFileWriteAllowed = videoOverwritePrompt.exec() == QMessageBox::Ok ? true : false;
             }
+#endif
 
             // Write the video..
             if (videoFileWriteAllowed) {
                 KisImportExportErrorCode exportResult = ImportExportCodes::OK;
 
+                // Let's not mess with the file on Android like this, it's slow
+                // and could cause weird behavior depending on the provider.
+                // We'll notice that the file can't be opened later anyway.
+#ifndef Q_OS_ANDROID
                 QFile videoFile(videoOutputFilePath);
                 if (!videoFile.open(QIODevice::WriteOnly)) {
                     qWarning() << "Could not open" << videoFile.fileName() << "for writing! Do you have permission to write to this file?";
@@ -108,10 +179,16 @@ bool KisAnimationRender::render(KisDocument *doc, KisViewManager *viewManager, K
                 } else {
                     videoFile.close();
                 }
+#endif
 
                 if (exportResult.isOk()) {
                     QScopedPointer<KisAnimationVideoSaver> encoder(new KisAnimationVideoSaver(doc, batchMode));
-                    exportResult = encoder->convert(doc, savedFilesMask, encoderOptions, batchMode);
+                    exportResult = encoder->convert(doc,
+                                                    framesDirectory,
+                                                    savedFilesMask,
+                                                    exporter.savedFiles(),
+                                                    encoderOptions,
+                                                    batchMode);
                 }
 
                 if (!exportResult.isOk()) {
@@ -123,31 +200,36 @@ bool KisAnimationRender::render(KisDocument *doc, KisViewManager *viewManager, K
         }
 
         //File cleanup
-        QDir d(framesDirectory);
+        if (!isTemporaryFramesDirectory) {
+            QDir d(framesDirectory);
 
-        if (encoderOptions.shouldDeleteSequence || delayReturnSuccess == false) {
-            QStringList savedFiles = exporter.savedFiles();
+            if (encoderOptions.shouldDeleteSequence || !delayReturnSuccess) {
+                QStringList savedFiles = exporter.savedFiles();
 
-            Q_FOREACH(const QString &f, savedFiles) {
-                if (d.exists(f)) {
-                    d.remove(f);
+                Q_FOREACH(const QString &f, savedFiles) {
+                    if (d.exists(f)) {
+                        d.remove(f);
+                    }
                 }
-            }
-        } else if(encoderOptions.wantsOnlyUniqueFrameSequence) {
-            const QStringList fileNames = exporter.savedFiles();
-            const QStringList uniqueFrameNames = exporter.savedUniqueFiles();
-
-            Q_FOREACH(const QString &f, fileNames) {
-                if (!uniqueFrameNames.contains(f)) {
-                    d.remove(f);
+            } else if(encoderOptions.wantsOnlyUniqueFrameSequence) {
+                const QStringList fileNames = exporter.savedFiles();
+                const QStringList uniqueFrameNames = exporter.savedUniqueFiles();
+
+                Q_FOREACH(const QString &f, fileNames) {
+                    if (!uniqueFrameNames.contains(f)) {
+                        d.remove(f);
+                    }
                 }
             }
-        }
 
-        QStringList paletteFiles = d.entryList(QStringList() << "KritaTempPalettegen_*.png", QDir::Files);
+            // We don't generate palette files on Android, that's done in memory.
+#ifndef Q_OS_ANDROID
+            QStringList paletteFiles = d.entryList(QStringList() << "KritaTempPalettegen_*.png", QDir::Files);
 
-        Q_FOREACH(const QString &f, paletteFiles) {
-            d.remove(f);
+            Q_FOREACH(const QString &f, paletteFiles) {
+                d.remove(f);
+            }
+#endif
         }
     } else if (result == KisAsyncAnimationFramesSaveDialog::RenderTimedOut) {
         QMessageBox::critical(qApp->activeWindow(), i18nc("@title:window", "Rendering error"), "Animation frame rendering has timed out. Output files are incomplete.\nTry to increase \"Frame Rendering Timeout\" or reduce \"Frame Rendering Clones Limit\" in Krita settings");
@@ -158,9 +240,11 @@ bool KisAnimationRender::render(KisDocument *doc, KisViewManager *viewManager, K
     return delayReturnSuccess;
 }
 
-bool KisAnimationRender::mustHaveEvenDimensions(const QString &mimeType, KisAnimationRenderingOptions::RenderMode renderMode)
+bool KisAnimationRender::mustHaveEvenDimensions(const QString &videoType,
+                                                KisAnimationRenderingOptions::RenderMode renderMode)
 {
-    return (mimeType == "video/mp4" || mimeType == "video/x-matroska") && renderMode != KisAnimationRenderingOptions::RENDER_FRAMES_ONLY;
+    return renderMode != KisAnimationRenderingOptions::RENDER_FRAMES_ONLY
+        && (looksLikeMp4(videoType) || looksLikeMatroska(videoType));
 }
 
 bool KisAnimationRender::hasEvenDimensions(int width, int height)
diff --git a/libs/ui/animation/KisAnimationRender.h b/libs/ui/animation/KisAnimationRender.h
index 086465ea5d6..c5c9337e9c6 100644
--- a/libs/ui/animation/KisAnimationRender.h
+++ b/libs/ui/animation/KisAnimationRender.h
@@ -20,7 +20,7 @@ namespace KisAnimationRender {
     **/
     KRITAUI_EXPORT bool render(KisDocument *doc, KisViewManager* viewManager, KisAnimationRenderingOptions encoderOptions);
 
-    bool mustHaveEvenDimensions(const QString &mimeType, KisAnimationRenderingOptions::RenderMode renderMode);
+    bool mustHaveEvenDimensions(const QString &videoType, KisAnimationRenderingOptions::RenderMode renderMode);
     bool hasEvenDimensions(int width, int height);
 
 }
diff --git a/libs/ui/animation/KisAnimationRenderingOptions.cpp b/libs/ui/animation/KisAnimationRenderingOptions.cpp
index bd45474797f..d02a5a3407d 100644
--- a/libs/ui/animation/KisAnimationRenderingOptions.cpp
+++ b/libs/ui/animation/KisAnimationRenderingOptions.cpp
@@ -11,15 +11,16 @@
 
 #include <KisFileUtils.h>
 
+#ifdef Q_OS_ANDROID
+#include <QJsonDocument>
+#endif
+
 KisAnimationRenderingOptions::KisAnimationRenderingOptions()
-    : videoMimeType("video/mp4"),
-      frameMimeType("image/png"),
-      basename("frame"),
-      directory("")
 {
 
 }
 
+#ifndef Q_OS_ANDROID
 QString KisAnimationRenderingOptions::resolveAbsoluteDocumentFilePath(const QString &documentPath) const
 {
     return
@@ -53,6 +54,7 @@ QString KisAnimationRenderingOptions::resolveAbsoluteFramesDirectory() const
 {
     return resolveAbsoluteFramesDirectory(lastDocumentPath);
 }
+#endif
 
 KisAnimationRenderingOptions::RenderMode KisAnimationRenderingOptions::renderMode() const
 {
@@ -77,20 +79,26 @@ KisPropertiesConfigurationSP KisAnimationRenderingOptions::toProperties() const
     config->setProperty("first_frame", firstFrame);
     config->setProperty("last_frame", lastFrame);
     config->setProperty("sequence_start", sequenceStart);
-    config->setProperty("video_mimetype", videoMimeType);
     config->setProperty("frame_mimetype", frameMimeType);
 
     config->setProperty("encode_video", shouldEncodeVideo);
     config->setProperty("delete_sequence", shouldDeleteSequence);
     config->setProperty("only_unique_frames", wantsOnlyUniqueFrameSequence);
 
-    config->setProperty("ffmpeg_path", ffmpegPath);
     config->setProperty("framerate", frameRate);
     config->setProperty("height", height);
     config->setProperty("width", width);
     config->setProperty("include_audio", includeAudio);
     config->setProperty("filename", videoFileName);
+
+#ifdef Q_OS_ANDROID
+    config->setProperty("video_format_key", videoFormatKey);
+    config->setProperty("video_format_preferences_json", videoFormatPreferencesJson);
+#else
+    config->setProperty("video_mimetype", videoMimeType);
+    config->setProperty("ffmpeg_path", ffmpegPath);
     config->setProperty("custom_ffmpeg_options", customFFMpegOptions);
+#endif
 
     config->setPrefixedProperties("frame_export/", frameExportConfig);
 
@@ -105,20 +113,26 @@ void KisAnimationRenderingOptions::fromProperties(KisPropertiesConfigurationSP c
     firstFrame = config->getPropertyLazy("first_frame", 0);
     lastFrame = config->getPropertyLazy("last_frame", 0);
     sequenceStart = config->getPropertyLazy("sequence_start", 0);
-    videoMimeType = config->getPropertyLazy("video_mimetype", videoMimeType);
     frameMimeType = config->getPropertyLazy("frame_mimetype", frameMimeType);
 
     shouldEncodeVideo = config->getPropertyLazy("encode_video", false);
     shouldDeleteSequence = config->getPropertyLazy("delete_sequence", false);
     wantsOnlyUniqueFrameSequence = config->getPropertyLazy("only_unique_frames", false);
 
-    ffmpegPath = config->getPropertyLazy("ffmpeg_path", "");
     frameRate = config->getPropertyLazy("framerate", 25);
     height = config->getPropertyLazy("height", 0);
     width = config->getPropertyLazy("width", 0);
     includeAudio = config->getPropertyLazy("include_audio", true);
     videoFileName = config->getPropertyLazy("filename", "");
+
+#ifdef Q_OS_ANDROID
+    videoFormatKey = config->getPropertyLazy("video_format_key", QString());
+    videoFormatPreferencesJson = config->getPropertyLazy("video_format_preferences_json", QString());
+#else
+    videoMimeType = config->getPropertyLazy("video_mimetype", videoMimeType);
+    ffmpegPath = config->getPropertyLazy("ffmpeg_path", "");
     customFFMpegOptions = config->getPropertyLazy("custom_ffmpeg_options", "");
+#endif
 
     frameExportConfig = new KisPropertiesConfiguration();
     config->getPrefixedProperties("frame_export/", frameExportConfig);
diff --git a/libs/ui/animation/KisAnimationRenderingOptions.h b/libs/ui/animation/KisAnimationRenderingOptions.h
index 5e40598c769..0a247a4320e 100644
--- a/libs/ui/animation/KisAnimationRenderingOptions.h
+++ b/libs/ui/animation/KisAnimationRenderingOptions.h
@@ -18,11 +18,16 @@ public:
     KisAnimationRenderingOptions();
 
     QString lastDocumentPath;
-    QString videoMimeType;
-    QString frameMimeType;
-
-    QString basename;
-    QString directory;
+#ifdef Q_OS_ANDROID
+    QString videoFormatKey;
+    QString videoFormatPreferencesJson;
+#else
+    QString videoMimeType = QStringLiteral("video/mp4");
+#endif
+    QString frameMimeType = QStringLiteral("image/png");
+
+    QString basename = QStringLiteral("frame");
+    QString directory = QStringLiteral("");
     int firstFrame = 0;
     int lastFrame = 0;
     int sequenceStart = 0;
@@ -32,22 +37,28 @@ public:
     bool includeAudio = false;
     bool wantsOnlyUniqueFrameSequence = false;
 
+#ifndef Q_OS_ANDROID
     QString ffmpegPath;
+#endif
     int frameRate = 25;
     int width = 0;
     int height = 0;
     QString scaleFilter;
     QString videoFileName;
 
+#ifndef Q_OS_ANDROID
     QString customFFMpegOptions;
+#endif
     KisPropertiesConfigurationSP frameExportConfig;
 
+#ifndef Q_OS_ANDROID
     QString resolveAbsoluteDocumentFilePath(const QString &documentPath) const;
     QString resolveAbsoluteVideoFilePath(const QString &documentPath) const;
     QString resolveAbsoluteFramesDirectory(const QString &documentPath) const;
 
     QString resolveAbsoluteVideoFilePath() const;
     QString resolveAbsoluteFramesDirectory() const;
+#endif
 
 
     enum RenderMode {
diff --git a/libs/ui/animation/KisDlgAnimationRenderer.cpp b/libs/ui/animation/KisDlgAnimationRenderer.cpp
index 9603683610c..f226b16d8a2 100644
--- a/libs/ui/animation/KisDlgAnimationRenderer.cpp
+++ b/libs/ui/animation/KisDlgAnimationRenderer.cpp
@@ -24,6 +24,7 @@
 #include <KoJsonTrader.h>
 #include <KisImportExportFilter.h>
 #include <krita_container_utils.h>
+#include <kis_icon_utils.h>
 #include <kis_image.h>
 #include <kis_image_animation_interface.h>
 #include <kis_time_span.h>
@@ -39,10 +40,17 @@
 #include "kis_acyclic_signal_connector.h"
 #include "KisVideoSaver.h"
 #include "KisAnimationRenderingOptions.h"
-#include "animation/KisFFMpegWrapper.h"
-#include "VideoExportOptionsDialog.h"
 #include "kis_image_config.h"
 
+#ifdef Q_OS_ANDROID
+#include <QJsonDocument>
+#include "animation/KisMediaEncoderFormatPreferencesDialog.h"
+#include "animation/KisMediaEncoderWrapper.h"
+#else
+#include "VideoExportOptionsDialog.h"
+#include "animation/KisFFMpegWrapper.h"
+#endif
+
 
 KisDlgAnimationRenderer::KisDlgAnimationRenderer(KisDocument *doc, QWidget *parent)
     : KoDialog(parent)
@@ -58,6 +66,22 @@ KisDlgAnimationRenderer::KisDlgAnimationRenderer(KisDocument *doc, QWidget *pare
     m_page = new WdgAnimationRenderer(this);
     m_page->layout()->setContentsMargins(0, 0, 0, 0);
 
+    {
+        QIcon editIcon = KisIconUtils::loadIcon("document-edit");
+        m_page->bnExportOptions->setIcon(editIcon);
+        m_page->bnRenderOptions->setIcon(editIcon);
+    }
+
+#ifdef Q_OS_ANDROID
+    m_page->lblVideoFilenameTitle->hide();
+    m_page->videoFilename->hide();
+    m_page->dirRequester->setReadOnlyText(true);
+    m_page->lblFFMpegLocationTitle->hide();
+    m_page->ffmpegLocation->hide();
+    m_page->lblFFMpegVersionTitle->hide();
+    m_page->lblFFMpegVersion->hide();
+#endif
+
     m_page->dirRequester->setMode(KoFileDialog::OpenDirectory);
 
     m_page->intStart->setMinimum(0);
@@ -91,16 +115,26 @@ KisDlgAnimationRenderer::KisDlgAnimationRenderer(KisDocument *doc, QWidget *pare
             m_page->cmbMimetype->setCurrentIndex(m_page->cmbMimetype->count() - 1);
         }
     }
-    
+
+#ifdef Q_OS_ANDROID
+    // Set up video export formats on Android. No ffmpeg here.
+    m_videoFormatPreferences = loadVideoFormatPreferences();
+    const QVector<KisMediaEncoderFormat *> videoFormats = KisMediaEncoderWrapper::getSupportedFormats();
+    for (KisMediaEncoderFormat *videoFormat : videoFormats) {
+        m_page->cmbRenderType->addItem(videoFormat->title(), QVariant(videoFormat->key()));
+    }
+#endif
+
     m_page->cmbScaleFilter->addItem(i18nc("bicubic filtering", "bicubic"), "bicubic");
     m_page->cmbScaleFilter->addItem(i18nc("bilinear filtering", "bilinear"), "bilinear");
     m_page->cmbScaleFilter->addItem(i18nc("lanczos3 filtering", "lanczos3"), "lanczos");
     m_page->cmbScaleFilter->addItem(i18nc("nearest neighbor filtering", "neighbor"), "neighbor");
     m_page->cmbScaleFilter->addItem(i18nc("spline filtering", "spline"), "spline");
-    
-    m_page->videoFilename->setMode(KoFileDialog::SaveFile);
 
+#ifndef Q_OS_ANDROID
+    m_page->videoFilename->setMode(KoFileDialog::SaveFile);
     m_page->ffmpegLocation->setMode(KoFileDialog::OpenFile);
+#endif
 
     m_page->cmbRenderType->setPlaceholderText(i18nc("Not applicable. No render types without valid ffmpeg path.", "N/A"));
 
@@ -113,7 +147,9 @@ KisDlgAnimationRenderer::KisDlgAnimationRenderer(KisDocument *doc, QWidget *pare
 
         connect(m_page->intFramesPerSecond, SIGNAL(valueChanged(int)), SLOT(frameRateChanged(int)));
 
+#ifndef Q_OS_ANDROID
         connect(m_page->ffmpegLocation, SIGNAL(fileSelected(QString)), SLOT(setFFmpegPath(QString)));
+#endif
 
         connect(this, SIGNAL(accepted()), SLOT(slotDialogAccepted()));
     }
@@ -142,6 +178,7 @@ KisDlgAnimationRenderer::~KisDlgAnimationRenderer()
 
 void KisDlgAnimationRenderer::initializeRenderSettings(const KisDocument &doc, const KisAnimationRenderingOptions &lastUsedOptions)
 {
+#ifndef Q_OS_ANDROID
     // Initialize FFmpeg location... (!)
     KisConfig cfg(false);
     QString cfgFFmpegPath = cfg.ffmpegLocation();
@@ -178,6 +215,7 @@ void KisDlgAnimationRenderer::initializeRenderSettings(const KisDocument &doc, c
     if (!likelyFFmpegPath.isEmpty() && QFileInfo(likelyFFmpegPath).isExecutable()) {
         setFFmpegPath(likelyFFmpegPath);
     }
+#endif
 
     const QString documentPath = m_doc->localFilePath();
 
@@ -192,20 +230,27 @@ void KisDlgAnimationRenderer::initializeRenderSettings(const KisDocument &doc, c
         m_page->intWidth->setValue(lastUsedOptions.width);
         m_page->intHeight->setValue(lastUsedOptions.height);
 
+#ifdef Q_OS_ANDROID
+        m_page->dirRequester->setStartDir(documentPath);
+#else
         m_page->videoFilename->setStartDir(lastUsedOptions.resolveAbsoluteDocumentFilePath(documentPath));
         m_page->videoFilename->setFileName(lastUsedOptions.videoFileName);
-
         m_page->dirRequester->setStartDir(lastUsedOptions.resolveAbsoluteDocumentFilePath(documentPath));
+#endif
         m_page->dirRequester->setFileName(lastUsedOptions.directory);
+
     } else {
         m_page->sequenceStart->setValue(m_image->animationInterface()->activePlaybackRange().start());
         m_page->intWidth->setValue(m_image->width());
         m_page->intHeight->setValue(m_image->height());
 
+#ifdef Q_OS_ANDROID
+        m_page->dirRequester->setStartDir(documentPath);
+#else
         m_page->videoFilename->setStartDir(lastUsedOptions.resolveAbsoluteDocumentFilePath(documentPath));
         m_page->videoFilename->setFileName(defaultVideoFileName(m_doc, lastUsedOptions.videoMimeType));
-
         m_page->dirRequester->setStartDir(lastUsedOptions.resolveAbsoluteDocumentFilePath(documentPath));
+#endif
         m_page->dirRequester->setFileName(lastUsedOptions.directory);
     }
 
@@ -225,28 +270,25 @@ void KisDlgAnimationRenderer::initializeRenderSettings(const KisDocument &doc, c
     }
 
     // Initialize VIDEO render format...
+#ifdef Q_OS_ANDROID
+    const QString &lastVideoType = lastUsedOptions.videoFormatKey;
+#else
+    const QString &lastVideoType = lastUsedOptions.videoMimeType;
+#endif
     for (int i = 0; i < m_page->cmbRenderType->count(); ++i) {
-        if (m_page->cmbRenderType->itemData(i).toString() == lastUsedOptions.videoMimeType) {
+        if (m_page->cmbRenderType->itemData(i).toString() == lastVideoType) {
             m_page->cmbRenderType->setCurrentIndex(i);
             break;
         }
     }
 
     m_page->chkOnlyUniqueFrames->setChecked(lastUsedOptions.wantsOnlyUniqueFrameSequence);
-
-    if constexpr (PLATFORM_SUPPORTS_FFMPEG) {
-        m_page->shouldExportOnlyVideo->setChecked(lastUsedOptions.shouldEncodeVideo);
-        m_page->shouldExportOnlyImageSequence->setChecked(!lastUsedOptions.shouldDeleteSequence);
-    } else {
-        m_page->shouldExportOnlyVideo->setChecked(false);
-        m_page->shouldExportOnlyVideo->setEnabled(false);
-        m_page->shouldExportOnlyVideo->setVisible(false);
-        m_page->shouldExportOnlyImageSequence->setChecked(true);
-        m_page->shouldExportOnlyImageSequence->setCheckable(false);
-    }
+    m_page->shouldExportOnlyVideo->setChecked(lastUsedOptions.shouldEncodeVideo);
+    m_page->shouldExportOnlyImageSequence->setChecked(!lastUsedOptions.shouldDeleteSequence);
 
     slotExportTypeChanged();
 
+#ifndef Q_OS_ANDROID
     {
         KisPropertiesConfigurationSP settings = loadLastConfiguration("VIDEO_ENCODER");
 
@@ -269,6 +311,7 @@ void KisDlgAnimationRenderer::initializeRenderSettings(const KisDocument &doc, c
     m_page->ffmpegLocation->setFileName(likelyFFmpegPath);
     m_page->ffmpegLocation->setStartDir(QFileInfo(m_doc->localFilePath()).path());
     m_page->ffmpegLocation->setReadOnlyText(true);
+#endif
 
     // Initialize these settings based on the current document context..
     m_page->intStart->setValue(doc.image()->animationInterface()->activePlaybackRange().start());
@@ -293,6 +336,7 @@ void KisDlgAnimationRenderer::initializeRenderSettings(const KisDocument &doc, c
     m_page->chkIncludeAudio->setChecked(hasAudioLoaded);
 }
 
+#ifndef Q_OS_ANDROID
 void KisDlgAnimationRenderer::getDefaultVideoEncoderOptions(const QString &mimeType,
                                                             KisPropertiesConfigurationSP cfg,
                                                             const QStringList &availableEncoders,
@@ -311,6 +355,7 @@ void KisDlgAnimationRenderer::getDefaultVideoEncoderOptions(const QString &mimeT
     *customFFMpegOptionsString = encoderConfigWidget->customUserOptionsString();
     *renderHDR = encoderConfigWidget->videoConfiguredForHDR();
 }
+#endif
 
 void KisDlgAnimationRenderer::filterSequenceMimeTypes(QStringList &mimeTypes)
 {
@@ -321,6 +366,7 @@ void KisDlgAnimationRenderer::filterSequenceMimeTypes(QStringList &mimeTypes)
     });
 }
 
+#ifndef Q_OS_ANDROID
 QStringList KisDlgAnimationRenderer::makeVideoMimeTypesList()
 {
     QStringList supportedMimeTypes = QStringList();
@@ -394,12 +440,36 @@ QStringList KisDlgAnimationRenderer::filterMimeTypeListByAvailableEncoders(const
 
     return retValue;
 }
+#endif
 
 bool KisDlgAnimationRenderer::imageMimeSupportsHDR(QString &mime)
 {
     return (mime == "image/png");
 }
 
+#ifdef Q_OS_ANDROID
+QVariantMap KisDlgAnimationRenderer::loadVideoFormatPreferences()
+{
+    KisPropertiesConfigurationSP settings = loadLastConfiguration(QStringLiteral("VIDEO_ENCODER"));
+    QString s = settings->getString(QStringLiteral("format_preferences"));
+    if (!s.isEmpty()) {
+        QJsonDocument doc = QJsonDocument::fromJson(s.toUtf8());
+        if (doc.isObject()) {
+            return doc.toVariant().toMap();
+        }
+    }
+    return QVariantMap();
+}
+
+void KisDlgAnimationRenderer::saveVideoFormatPreferences(const QVariantMap &value)
+{
+    KisPropertiesConfigurationSP settings = new KisPropertiesConfiguration();
+    settings->setProperty(QStringLiteral("format_preferences"),
+                          QString::fromUtf8(QJsonDocument::fromVariant(value).toJson(QJsonDocument::Compact)));
+    saveLastUsedConfiguration(QStringLiteral("VIDEO_ENCODER"), settings);
+}
+#endif
+
 KisPropertiesConfigurationSP KisDlgAnimationRenderer::loadLastConfiguration(QString configurationID) {
     KisConfig globalConfig(true);
     return globalConfig.exportConfiguration(configurationID);
@@ -411,6 +481,16 @@ void KisDlgAnimationRenderer::saveLastUsedConfiguration(QString configurationID,
     globalConfig.setExportConfiguration(configurationID, config);
 }
 
+bool KisDlgAnimationRenderer::looksLikeGif(const QString &videoType)
+{
+#ifdef Q_OS_ANDROID
+    return videoType.contains(QStringLiteral(":gif"));
+#else
+    return videoType == QStringLiteral("image/gif");
+#endif
+}
+
+#ifndef Q_OS_ANDROID
 void KisDlgAnimationRenderer::setFFmpegPath(const QString& path) {
     // Let's START with the assumption that user-specified ffmpeg path is invalid
     // and clear out all of the ffmpeg-specific fields to fill post-validation...
@@ -493,25 +573,28 @@ void KisDlgAnimationRenderer::setFFmpegPath(const QString& path) {
         // Store configuration..
         cfg.setFFMpegLocation(ffmpegJsonObj["path"].toString());
 
-        ffmpegWarningCheck();
+        checkWarnings();
     }
 }
+#endif
+
+void KisDlgAnimationRenderer::checkWarnings()
+{
+    QStringList warnings;
 
-void KisDlgAnimationRenderer::ffmpegWarningCheck() {
-    const QString mimeType = m_page->cmbRenderType->itemData(m_page->cmbRenderType->currentIndex()).toString();
+    QString videoType = m_page->cmbRenderType->itemData(m_page->cmbRenderType->currentIndex()).toString();
+    bool gif = looksLikeGif(videoType);
 
+#ifndef Q_OS_ANDROID
     const QRegularExpression minVerFFMpegRX(R"(^n{0,1}(?:[0-3]|4\.[01])[\.\-])");
     const QRegularExpressionMatch minVerFFMpegMatch = minVerFFMpegRX.match(ffmpegVersion);
 
-    QStringList warnings;
-
-    if (mimeType == "image/gif" && minVerFFMpegMatch.hasMatch()) {
+    if (gif && minVerFFMpegMatch.hasMatch()) {
         warnings << i18nc("ffmpeg warning checks", "FFmpeg must be at least version 4.2+ for GIF transparency to work");
     }
+#endif
 
-    // m_page->bnRenderOptions->setEnabled(mimeType != "image/gif" && mimeType != "image/webp" && mimeType !=
-    // "image/png" );
-    if (mimeType == "image/gif" && m_page->intFramesPerSecond->value() > 50) {
+    if (gif && m_page->intFramesPerSecond->value() > 50) {
         warnings << i18nc("ffmpeg warning checks",
                           "Animated GIF images cannot have a framerate higher than 50. The framerate will be reduced "
                           "to 50 frames per second");
@@ -537,6 +620,7 @@ void KisDlgAnimationRenderer::ffmpegWarningCheck() {
     m_page->adjustSize();
 }
 
+#ifndef Q_OS_ANDROID
 QString KisDlgAnimationRenderer::defaultVideoFileName(KisDocument *doc, const QString &mimeType)
 {
     const QString docFileName = !doc->localFilePath().isEmpty() ? doc->localFilePath() : i18n("Untitled");
@@ -555,11 +639,7 @@ void KisDlgAnimationRenderer::selectRenderType(int index)
 
     const QString mimeType = m_page->cmbRenderType->itemData(index).toString();
 
-    /*
-    m_page->bnRenderOptions->setEnabled(mimeType != "image/gif" && mimeType != "image/webp" && mimeType != "image/png");
-    */
-
-    ffmpegWarningCheck();
+    checkWarnings();
 
     QString videoFileName = defaultVideoFileName(m_doc, mimeType);
 
@@ -592,9 +672,21 @@ void KisDlgAnimationRenderer::selectRenderType(int index)
                                       &m_wantsRenderWithHDR);
     }
 }
+#endif
 
 void KisDlgAnimationRenderer::selectRenderOptions()
 {
+#ifdef Q_OS_ANDROID
+    QString key = m_page->cmbRenderType->currentData().toString();
+    KisMediaEncoderFormat *format = KisMediaEncoderWrapper::getFormatByKey(key);
+    KIS_SAFE_ASSERT_RECOVER_RETURN(format);
+
+    KisMediaEncoderPreferencesDialog dlg(format, m_videoFormatPreferences.value(key).toMap(), this);
+    if (dlg.exec() == QDialog::Accepted) {
+        m_videoFormatPreferences.insert(key, dlg.preferences());
+        saveVideoFormatPreferences(m_videoFormatPreferences);
+    }
+#else
     const int index = m_page->cmbRenderType->currentIndex();
     const QString mimetype = m_page->cmbRenderType->itemData(index).toString();
 
@@ -623,12 +715,15 @@ void KisDlgAnimationRenderer::selectRenderOptions()
     dlg.setButtons(KoDialog::Ok | KoDialog::Cancel);
     if (dlg.exec() == QDialog::Accepted) {
         saveLastUsedConfiguration("VIDEO_ENCODER", encoderConfigWidget->configuration());
+#ifndef Q_OS_ANDROID
         m_customFFMpegOptionsString = encoderConfigWidget->customUserOptionsString();
         m_wantsRenderWithHDR = encoderConfigWidget->videoConfiguredForHDR();
+#endif
     }
 
     dlg.setMainWidget(0);
     encoderConfigWidget->deleteLater();
+#endif
 }
 
 void KisDlgAnimationRenderer::sequenceMimeTypeOptionsClicked()
@@ -650,19 +745,23 @@ void KisDlgAnimationRenderer::sequenceMimeTypeOptionsClicked()
             }
 
             //Important -- m_useHDR allows the synchronization of both the video and image render settings.
+#ifndef Q_OS_ANDROID
             if(imageMimeSupportsHDR(mimetype)) {
                 exportConfig->setProperty("saveAsHDR", m_wantsRenderWithHDR);
                 if (m_wantsRenderWithHDR) {
                     exportConfig->setProperty("forceSRGB", false);
                 }
             }
+#endif
 
             frameExportConfigWidget->setConfiguration(exportConfig);
             KoDialog dlg(this);
             dlg.setMainWidget(frameExportConfigWidget);
             dlg.setButtons(KoDialog::Ok | KoDialog::Cancel);
             if (dlg.exec() == QDialog::Accepted) {
+#ifndef Q_OS_ANDROID
                 m_wantsRenderWithHDR = frameExportConfigWidget->configuration()->getPropertyLazy("saveAsHDR", false);
+#endif
                 saveLastUsedConfiguration("img_sequence/" + mimetype, frameExportConfigWidget->configuration());
             }
 
@@ -681,7 +780,21 @@ KisAnimationRenderingOptions KisDlgAnimationRenderer::getEncoderOptions() const
     KisAnimationRenderingOptions options;
 
     options.lastDocumentPath = m_doc->localFilePath();
-    options.videoMimeType = m_page->cmbRenderType->currentData().toString();
+    QString videoType = m_page->cmbRenderType->currentData().toString();
+#ifdef Q_OS_ANDROID
+    options.videoFormatKey = videoType;
+    QVariantMap videoFormatPreferences = m_videoFormatPreferences.value(videoType).toMap();
+    if (!videoFormatPreferences.isEmpty()) {
+        options.videoFormatPreferencesJson =
+            QString::fromUtf8(QJsonDocument::fromVariant(videoFormatPreferences).toJson(QJsonDocument::Compact));
+    }
+    options.videoFileName = m_videoFileName;
+#else
+    options.videoMimeType = videoType;
+    options.videoFileName = m_page->videoFilename->fileName();
+    options.ffmpegPath = m_page->ffmpegLocation->fileName();
+    options.customFFMpegOptions = m_customFFMpegOptionsString;
+#endif
     options.frameMimeType = m_page->cmbMimetype->currentData().toString();
     options.scaleFilter = m_page->cmbScaleFilter->currentData().toString();
 
@@ -691,28 +804,18 @@ KisAnimationRenderingOptions KisDlgAnimationRenderer::getEncoderOptions() const
     options.lastFrame = m_page->intEnd->value();
     options.sequenceStart = m_page->sequenceStart->value();
 
-    if constexpr (PLATFORM_SUPPORTS_FFMPEG) {
-        options.shouldEncodeVideo = m_page->shouldExportOnlyVideo->isChecked();
-        options.shouldDeleteSequence = !m_page->shouldExportOnlyImageSequence->isChecked();
-    } else {
-        options.shouldEncodeVideo = false;
-        options.shouldDeleteSequence = false;
-    }
+    options.shouldEncodeVideo = m_page->shouldExportOnlyVideo->isChecked();
+    options.shouldDeleteSequence = !m_page->shouldExportOnlyImageSequence->isChecked();
     options.includeAudio = m_page->chkIncludeAudio->isChecked();
     options.wantsOnlyUniqueFrameSequence = m_page->chkOnlyUniqueFrames->isChecked();
 
-    options.ffmpegPath = m_page->ffmpegLocation->fileName();
     options.frameRate = m_page->intFramesPerSecond->value();
-
-    if (options.frameRate > 50 && options.videoMimeType == "image/gif") {
+    if (options.frameRate > 50 && looksLikeGif(videoType)) {
         options.frameRate = 50;
     }
 
     options.width = m_page->intWidth->value();
     options.height = m_page->intHeight->value();
-    options.videoFileName = m_page->videoFilename->fileName();
-
-    options.customFFMpegOptions = m_customFFMpegOptionsString;
 
     {
         KisPropertiesConfigurationSP cfg = loadLastConfiguration("img_sequence/" + options.frameMimeType);
@@ -720,12 +823,14 @@ KisAnimationRenderingOptions KisDlgAnimationRenderer::getEncoderOptions() const
             KisImportExportManager::fillStaticExportConfigurationProperties(cfg, m_image);
         }
 
+#ifndef Q_OS_ANDROID
         const bool forceNecessaryHDRSettings = m_wantsRenderWithHDR && imageMimeSupportsHDR(options.frameMimeType);
         if (forceNecessaryHDRSettings) {
             KIS_SAFE_ASSERT_RECOVER_NOOP(options.frameMimeType == "image/png");
             cfg->setProperty("forceSRGB", false);
             cfg->setProperty("saveAsHDR", true);
         }
+#endif
 
         options.frameExportConfig = cfg;
     }
@@ -733,6 +838,7 @@ KisAnimationRenderingOptions KisDlgAnimationRenderer::getEncoderOptions() const
     return options;
 }
 
+#ifndef Q_OS_ANDROID
 KisDlgAnimationRenderer::FFmpegValidationResult KisDlgAnimationRenderer::validateFFmpeg(const QString &ffmpegPath)
 {
     if (!ffmpegPath.isEmpty()) {
@@ -751,10 +857,12 @@ KisDlgAnimationRenderer::FFmpegValidationResult KisDlgAnimationRenderer::validat
     }
     return FFmpegValidationResult::INVALID;
 }
+#endif
 
+#ifndef Q_OS_ANDROID
 void KisDlgAnimationRenderer::slotButtonClicked(int button)
 {
-    if (button == KoDialog::Ok && (PLATFORM_SUPPORTS_FFMPEG && !m_page->shouldExportOnlyImageSequence->isChecked())) {
+    if (button == KoDialog::Ok && !m_page->shouldExportOnlyImageSequence->isChecked()) {
         QString fileName = m_page->videoFilename->fileName();
 
         if (fileName.isEmpty()) {
@@ -776,9 +884,26 @@ void KisDlgAnimationRenderer::slotButtonClicked(int button)
     }
     KoDialog::slotButtonClicked(button);
 }
+#endif
 
 void KisDlgAnimationRenderer::slotDialogAccepted()
 {
+#ifdef Q_OS_ANDROID
+    if (m_page->shouldExportOnlyVideo) {
+        KisMediaEncoderFormat *format = KisMediaEncoderWrapper::getFormatByKey(m_page->cmbRenderType->currentData().toString());
+        KIS_SAFE_ASSERT_RECOVER_RETURN(format);
+        KoFileDialog dialog(this, KoFileDialog::SaveFile, QStringLiteral("ExportAnimation"));
+        dialog.setMimeTypeFilters(QStringList(KisMimeDatabase::mimeTypeForSuffix(format->extension())));
+        dialog.setDefaultDir(m_doc->localFilePath());
+        m_videoFileName = dialog.filename();
+        if (m_videoFileName.isEmpty()) {
+            return;
+        }
+    } else {
+        m_videoFileName.clear();
+    }
+#endif
+
     KisConfig cfg(false);
     KisAnimationRenderingOptions options = getEncoderOptions();
     saveLastUsedConfiguration("ANIMATION_EXPORT", options.toProperties());
@@ -790,24 +915,19 @@ void KisDlgAnimationRenderer::slotDialogAccepted()
 
 void KisDlgAnimationRenderer::slotExportTypeChanged()
 {
-    if constexpr (PLATFORM_SUPPORTS_FFMPEG) {
-        // if a video format needs to be outputted
-        if (m_page->shouldExportOnlyVideo->isChecked()) {
-             // videos always uses PNG for creating video, so disable the ability to change the format
-             m_page->cmbMimetype->setEnabled(false);
-             m_page->cmbMimetype->setCurrentIndex(m_page->cmbMimetype->findData("image/png"));
-        }
+    // if a video format needs to be outputted
+    if (m_page->shouldExportOnlyVideo->isChecked()) {
+         // videos always uses PNG for creating video, so disable the ability to change the format
+         m_page->cmbMimetype->setEnabled(false);
+         m_page->cmbMimetype->setCurrentIndex(m_page->cmbMimetype->findData("image/png"));
+    }
 
-        /**
-         * A fallback fix for a case when both checkboxes are unchecked
-         */
-        if (!m_page->shouldExportOnlyVideo->isChecked() &&
-            !m_page->shouldExportOnlyImageSequence->isChecked()) {
+    /**
+     * A fallback fix for a case when both checkboxes are unchecked
+     */
+    if (!m_page->shouldExportOnlyVideo->isChecked() &&
+        !m_page->shouldExportOnlyImageSequence->isChecked()) {
 
-             KisSignalsBlocker b(m_page->shouldExportOnlyImageSequence);
-             m_page->shouldExportOnlyImageSequence->setChecked(true);
-        }
-    } else {
          KisSignalsBlocker b(m_page->shouldExportOnlyImageSequence);
          m_page->shouldExportOnlyImageSequence->setChecked(true);
     }
@@ -816,7 +936,7 @@ void KisDlgAnimationRenderer::slotExportTypeChanged()
 void KisDlgAnimationRenderer::frameRateChanged(int framerate)
 {
     Q_UNUSED(framerate);
-    ffmpegWarningCheck();
+    checkWarnings();
 }
 
 void KisDlgAnimationRenderer::slotLockAspectRatioDimensionsWidth(int width)
diff --git a/libs/ui/animation/KisDlgAnimationRenderer.h b/libs/ui/animation/KisDlgAnimationRenderer.h
index 9e2f0e80be8..be295b2e248 100644
--- a/libs/ui/animation/KisDlgAnimationRenderer.h
+++ b/libs/ui/animation/KisDlgAnimationRenderer.h
@@ -13,6 +13,9 @@
 
 #include <kis_types.h>
 
+#ifdef Q_OS_ANDROID
+#include <QVariantMap>
+#endif
 
 #include "kritaui_export.h"
 
@@ -48,7 +51,9 @@ public:
     KisAnimationRenderingOptions getEncoderOptions() const;
 
 private Q_SLOTS:
+#ifndef Q_OS_ANDROID
     void selectRenderType(int i);
+#endif
     void selectRenderOptions();
     /**
      * @brief sequenceMimeTypeSelected
@@ -60,32 +65,33 @@ private Q_SLOTS:
     void slotLockAspectRatioDimensionsHeight(int height);
 
     void slotExportTypeChanged();
+#ifndef Q_OS_ANDROID
     void setFFmpegPath(const QString& path);
+#endif
 
     void frameRateChanged(int framerate);
 
 protected Q_SLOTS:
-
+#ifndef Q_OS_ANDROID
     void slotButtonClicked(int button) override;
+#endif
     void slotDialogAccepted();
 
 
 private: 
-#ifdef Q_OS_ANDROID
-    static constexpr bool PLATFORM_SUPPORTS_FFMPEG = false;
-#else
-    static constexpr bool PLATFORM_SUPPORTS_FFMPEG = true;
-#endif
-
+#ifndef Q_OS_ANDROID
     enum FFmpegValidationResult {
         VALID = 1,
         INVALID = 0,
         NOT_A_BINARY = -1,
         COMPRESSED_FORMAT = -2
     };
+#endif
 
     void initializeRenderSettings(const KisDocument &doc, const KisAnimationRenderingOptions &lastUsedOptions);
-    void ffmpegWarningCheck();
+
+    void checkWarnings();
+#ifndef Q_OS_ANDROID
     FFmpegValidationResult validateFFmpeg(const QString &ffmpegPath);
 
     static QString defaultVideoFileName(KisDocument *doc, const QString &mimeType);
@@ -95,19 +101,34 @@ private:
                                               const QStringList &availableEncoders,
                                               QString *customFFMpegOptionsString,
                                               bool *forceHDRVideo);
+#endif
 
     static void filterSequenceMimeTypes(QStringList &mimeTypes);
+
+#ifndef Q_OS_ANDROID
     static QStringList makeVideoMimeTypesList();
     QStringList filterMimeTypeListByAvailableEncoders(const QStringList &mimeTypes);
+#endif
     static bool imageMimeSupportsHDR(QString &hdr);
 
+#ifdef Q_OS_ANDROID
+    static QVariantMap loadVideoFormatPreferences();
+    static void saveVideoFormatPreferences(const QVariantMap &value);
+#endif
+
     static KisPropertiesConfigurationSP loadLastConfiguration(QString configurationID);
     static void saveLastUsedConfiguration(QString configurationID, KisPropertiesConfigurationSP config);
 
+    static bool looksLikeGif(const QString &videoType);
+
 private:
     KisImageSP m_image;
     KisDocument *m_doc;
 
+#ifdef Q_OS_ANDROID
+    QString m_videoFileName;
+    QVariantMap m_videoFormatPreferences;
+#else
     QString m_customFFMpegOptionsString;
     QString ffmpegVersion = "None";
 
@@ -115,6 +136,7 @@ private:
     QMap<QString, QStringList> ffmpegEncoderTypes; // Maps supported output format to available list of encoder(s)
 
     bool m_wantsRenderWithHDR = false;
+#endif
 
     WdgAnimationRenderer *m_page {0};
 };
diff --git a/libs/ui/animation/KisLibavEncoderContext.h b/libs/ui/animation/KisLibavEncoderContext.h
index 01536d4c7c8..67086591d16 100644
--- a/libs/ui/animation/KisLibavEncoderContext.h
+++ b/libs/ui/animation/KisLibavEncoderContext.h
@@ -54,6 +54,25 @@ public:
         return true;
     }
 
+    int getSwsFlags(const QString &scaleFilter) const
+    {
+        // These are the values provided by KisDlgAnimationRenderer.
+        if (scaleFilter.isEmpty() || scaleFilter == QStringLiteral("bilinear")) {
+            return SWS_FAST_BILINEAR;
+        } else if (scaleFilter == QStringLiteral("bicubic")) {
+            return SWS_BICUBIC;
+        } else if (scaleFilter == QStringLiteral("lanczos")) {
+            return SWS_LANCZOS;
+        } else if (scaleFilter == QStringLiteral("neighbor")) {
+            return 0;
+        } else if (scaleFilter == QStringLiteral("spline")) {
+            return SWS_SPLINE;
+        } else {
+            warnFile.nospace() << "Unhandled scale filter '" << scaleFilter << "'";
+            return SWS_FAST_BILINEAR;
+        }
+    }
+
     SwsContext *getSwsContextFor(int inputWidth,
                                  int inputHeight,
                                  AVPixelFormat inputFormat,
diff --git a/libs/ui/animation/KisLibavMediaEncoderRunnable.cpp b/libs/ui/animation/KisLibavMediaEncoderRunnable.cpp
index 1dbf3e9578b..eee0053c7ff 100644
--- a/libs/ui/animation/KisLibavMediaEncoderRunnable.cpp
+++ b/libs/ui/animation/KisLibavMediaEncoderRunnable.cpp
@@ -664,6 +664,7 @@ public:
 
         m_frame->pts = 0;
         Frame inputFrame;
+        int swsFlags = getSwsFlags(settings.scaleFilter);
         while (m_runnable->nextFrame(inputFrame)) {
             if (isCancelled()) {
                 return EncodeResult::Cancelled;
@@ -701,7 +702,7 @@ public:
                                                       outputWidth,
                                                       outputHeight,
                                                       AVPixelFormat(m_frame->format),
-                                                      SWS_FAST_BILINEAR);
+                                                      swsFlags);
             if (!swsContext) {
                 setInternalErrorMessage(QStringLiteral("sws_getCachedContext failed"));
                 return EncodeResult::Failed;
diff --git a/libs/ui/animation/KisMediaEncoderWrapper.cpp b/libs/ui/animation/KisMediaEncoderWrapper.cpp
index 9d0c84c3cf8..599ed54f2d5 100644
--- a/libs/ui/animation/KisMediaEncoderWrapper.cpp
+++ b/libs/ui/animation/KisMediaEncoderWrapper.cpp
@@ -2,12 +2,15 @@
  *  SPDX-License-Identifier: GPL-3.0-or-later
  */
 #include <QAtomicInt>
+#include <QCoreApplication>
 #include <QImage>
 #include <QImageReader>
+#include <QProgressDialog>
 #include <QRunnable>
 #include <QThreadPool>
 
 #include <functional>
+#include <memory>
 
 #include <klocalizedstring.h>
 
@@ -160,6 +163,84 @@ KisMediaEncoderWrapper::~KisMediaEncoderWrapper()
     reset();
 }
 
+KisImportExportErrorCode KisMediaEncoderWrapper::start(const KisMediaEncoderWrapperSettings &settings, bool batchMode)
+{
+    reset();
+
+    KisMediaEncoderRunnable *runnable = makeSupportedRunnable(settings);
+    ImportExportCodes::ErrorCodeID resultCode = ImportExportCodes::InternalError;
+    if (runnable) {
+        connect(
+            runnable,
+            &KisMediaEncoderRunnable::sigCompleted,
+            this,
+            [&resultCode] {
+                resultCode = ImportExportCodes::OK;
+            },
+            Qt::DirectConnection);
+        connect(
+            runnable,
+            &KisMediaEncoderRunnable::sigCancelled,
+            this,
+            [&resultCode] {
+                resultCode = ImportExportCodes::Cancelled;
+            },
+            Qt::DirectConnection);
+        connect(
+            runnable,
+            &KisMediaEncoderRunnable::sigFailed,
+            this,
+            [&resultCode](const QString &errorMessage) {
+                warnFile << "Media encoder export error:" << errorMessage;
+                resultCode = ImportExportCodes::OK;
+            },
+            Qt::DirectConnection);
+        connect(this,
+                &KisMediaEncoderWrapper::sigCancelRequested,
+                runnable,
+                &KisMediaEncoderRunnable::slotHandleCancelRequested,
+                Qt::DirectConnection);
+
+        QProgressDialog *progressDlg;
+        if (batchMode) {
+            progressDlg = nullptr;
+        } else {
+            progressDlg = new QProgressDialog;
+            progressDlg->setLabelText(i18n("Rendering animation..."));
+            connect(
+                runnable,
+                &KisMediaEncoderRunnable::sigProgressUpdated,
+                this,
+                [progressDlg, frameCount = settings.inputFiles.size()](int frameNo) {
+                    int progress;
+                    if (frameNo < 0) {
+                        progress = 0;
+                    } else if (frameNo >= frameCount) {
+                        progress = 100;
+                    } else {
+                        progress = qRound(qreal(frameNo) / qreal(frameCount) * 100.0);
+                    }
+                    progressDlg->setValue(progress);
+                    QCoreApplication::processEvents();
+                },
+                Qt::DirectConnection);
+            connect(progressDlg,
+                    &QProgressDialog::canceled,
+                    this,
+                    &KisMediaEncoderWrapper::sigCancelRequested,
+                    Qt::DirectConnection);
+            progressDlg->show();
+            QCoreApplication::processEvents();
+        }
+
+        m_runnable = runnable;
+        runnable->run();
+        delete runnable;
+        delete progressDlg;
+    }
+    return KisImportExportErrorCode(resultCode);
+}
+
 void KisMediaEncoderWrapper::startNonBlocking(const KisMediaEncoderWrapperSettings &settings)
 {
     reset();
diff --git a/libs/ui/animation/KisMediaEncoderWrapper.h b/libs/ui/animation/KisMediaEncoderWrapper.h
index 539eba6c1b0..1c7a481271d 100644
--- a/libs/ui/animation/KisMediaEncoderWrapper.h
+++ b/libs/ui/animation/KisMediaEncoderWrapper.h
@@ -14,6 +14,8 @@
 
 #include <kritaui_export.h>
 
+#include "KisImportExportErrorCode.h"
+
 class QImage;
 class QWidget;
 
@@ -45,6 +47,7 @@ struct KRITAUI_EXPORT KisMediaEncoderWrapperSettings {
     QStringList inputFiles;
     KisMediaEncoderFormat *format;
     QVariantMap formatPreferences;
+    QString scaleFilter;
     QSize outputSize;
     int inputFps;
     int outputFps;
@@ -141,6 +144,7 @@ public:
     explicit KisMediaEncoderWrapper(QObject *parent = nullptr);
     ~KisMediaEncoderWrapper() override;
 
+    KisImportExportErrorCode start(const KisMediaEncoderWrapperSettings &settings, bool batchMode);
     void startNonBlocking(const KisMediaEncoderWrapperSettings &settings);
     void reset();
 
diff --git a/libs/ui/animation/KisVideoSaver.cpp b/libs/ui/animation/KisVideoSaver.cpp
index 9116d127f68..c26cf038506 100644
--- a/libs/ui/animation/KisVideoSaver.cpp
+++ b/libs/ui/animation/KisVideoSaver.cpp
@@ -26,10 +26,16 @@
 #include <KoResourcePaths.h>
 #include "kis_config.h"
 #include "KisAnimationRenderingOptions.h"
-#include "animation/KisFFMpegWrapper.h"
 
 #include "KisPart.h"
 
+#ifdef Q_OS_ANDROID
+#include "animation/KisMediaEncoderWrapper.h"
+#include <QJsonDocument>
+#else
+#include "animation/KisFFMpegWrapper.h"
+#endif
+
 KisAnimationVideoSaver::KisAnimationVideoSaver(KisDocument *doc, bool batchMode)
     : m_image(doc->image())
     , m_doc(doc)
@@ -46,8 +52,60 @@ KisImageSP KisAnimationVideoSaver::image()
     return m_image;
 }
 
-KisImportExportErrorCode KisAnimationVideoSaver::encode(const QString &savedFilesMask, const KisAnimationRenderingOptions &options)
+KisImportExportErrorCode KisAnimationVideoSaver::encode(const QString &framesDirectory,
+                                                        const QString &savedFilesMask,
+                                                        const QStringList &savedFiles,
+                                                        const KisAnimationRenderingOptions &options)
 {
+#ifdef Q_OS_ANDROID
+    Q_UNUSED(savedFilesMask);
+
+    KisMediaEncoderFormat *format = KisMediaEncoderWrapper::getFormatByKey(options.videoFormatKey);
+    KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(format, ImportExportCodes::InternalError);
+
+    QVariantMap formatPreferences;
+    if (!options.videoFormatPreferencesJson.isEmpty()) {
+        formatPreferences = QJsonDocument::fromJson(options.videoFormatPreferencesJson.toUtf8()).toVariant().toMap();
+    }
+
+    KisMediaEncoderWrapperSettings settings = {
+        options.videoFileName,
+        QStringList(),
+        format,
+        formatPreferences,
+        options.scaleFilter,
+        QSize(options.width, options.height),
+        options.frameRate,
+        options.frameRate,
+        0,
+        0,
+    };
+
+    QString inputDir = framesDirectory;
+    if (!inputDir.endsWith(QStringLiteral("/"))) {
+        inputDir.append(QStringLiteral("/"));
+    }
+
+    settings.inputFiles.reserve(savedFiles.size());
+    for (const QString &savedFile : savedFiles) {
+        settings.inputFiles.append(inputDir + savedFile);
+    }
+
+    // This whole batchMode business is either a vestige or was never actually
+    // implemented to completion. It's *supposed* to not show dialogs if Krita
+    // is run in batch mode via the command line, but it's not actually wired up
+    // properly. KisAnimationRender always sets it to false with a TODO to fetch
+    // it correctly and the ffmpeg code here never sets it either, meaning it'll
+    // always be false. For consistency, I've made KisMediaEncoderWrapper behave
+    // the same, even if it's kind of pointless. But if you're working on this
+    // in the future, you should be able to do the same thing for this code path
+    // as you're doing for the ffmpeg one.
+    bool batchMode = false;
+    return KisMediaEncoderWrapper().start(settings, batchMode);
+#else
+    Q_UNUSED(framesDirectory);
+    Q_UNUSED(savedFiles);
+
     if (!QFileInfo(options.ffmpegPath).exists()) {
         m_doc->setErrorMessage(i18n("ffmpeg could not be found at %1", options.ffmpegPath));
         return ImportExportCodes::Failure;
@@ -202,11 +260,17 @@ KisImportExportErrorCode KisAnimationVideoSaver::encode(const QString &savedFile
      
 
     return resultOuter;
+#endif
 }
 
-KisImportExportErrorCode KisAnimationVideoSaver::convert(KisDocument *document, const QString &savedFilesMask, const KisAnimationRenderingOptions &options, bool batchMode)
+KisImportExportErrorCode KisAnimationVideoSaver::convert(KisDocument *document,
+                                                         const QString &framesDirectory,
+                                                         const QString &savedFilesMask,
+                                                         const QStringList &savedFiles,
+                                                         const KisAnimationRenderingOptions &options,
+                                                         bool batchMode)
 {
     KisAnimationVideoSaver videoSaver(document, batchMode);
-    KisImportExportErrorCode res = videoSaver.encode(savedFilesMask, options);
+    KisImportExportErrorCode res = videoSaver.encode(framesDirectory, savedFilesMask, savedFiles, options);
     return res;
 }
diff --git a/libs/ui/animation/KisVideoSaver.h b/libs/ui/animation/KisVideoSaver.h
index 9afea47a751..743dd4f933d 100644
--- a/libs/ui/animation/KisVideoSaver.h
+++ b/libs/ui/animation/KisVideoSaver.h
@@ -24,9 +24,8 @@ public:
     /**
      * @brief KisAnimationVideoSaver
      * This is the object that takes an animation document and config and tells ffmpeg
-     * to render it via KisFFMpegWrapper.
+     * to render it via KisFFMpegWrapper or uses KisMediaEncoderWrapper on Android.
      * @param doc the document to use for rendering.
-     * @param ffmpegPath the path to the ffmpeg executable.
      * @param batchMode whether Krita is in batchmode and we can thus not show gui widgets.
      */
     KisAnimationVideoSaver(KisDocument* doc, bool batchMode);
@@ -38,16 +37,17 @@ public:
      */
     KisImageSP image();
 
-    /**
-     * @brief encode the main encoding function.
-     * This in turn calls runFFMpeg, which is a private function inside this class.
-     * @param filename the filename to which to render the animation.
-     * @param configuration the configuration
-     * @return whether it is successful or had another failure.
-     */
-    KisImportExportErrorCode encode(const QString &savedFilesMask, const KisAnimationRenderingOptions &options);
-
-    static KisImportExportErrorCode convert(KisDocument *document, const QString &savedFilesMask, const KisAnimationRenderingOptions &options, bool batchMode);
+    KisImportExportErrorCode encode(const QString &framesDirectory,
+                                    const QString &savedFilesMask,
+                                    const QStringList &savedFiles,
+                                    const KisAnimationRenderingOptions &options);
+
+    static KisImportExportErrorCode convert(KisDocument *document,
+                                            const QString &framesDirectory,
+                                            const QString &savedFilesMask,
+                                            const QStringList &savedFiles,
+                                            const KisAnimationRenderingOptions &options,
+                                            bool batchMode);
 
 private:
     KisImageSP m_image;
diff --git a/libs/ui/animation/wdg_animationrenderer.ui b/libs/ui/animation/wdg_animationrenderer.ui
index 278cb14e97e..45322d93374 100644
--- a/libs/ui/animation/wdg_animationrenderer.ui
+++ b/libs/ui/animation/wdg_animationrenderer.ui
@@ -9,7 +9,7 @@
   <property name="windowTitle">
    <string>Animation Renderer Image</string>
   </property>
-  <layout class="QVBoxLayout" name="verticalLayout_3">
+  <layout class="QVBoxLayout" name="verticalLayout_3" stretch="0,0,0,1,0">
    <item>
     <widget class="QGroupBox" name="grpGeneralOptions">
      <property name="title">
@@ -73,9 +73,6 @@
           <property name="toolTip">
            <string>Select the frame export options</string>
           </property>
-          <property name="text">
-           <string>...</string>
-          </property>
          </widget>
         </item>
        </layout>
@@ -209,15 +206,12 @@
           <property name="toolTip">
            <string>Select the FFmpeg render options.</string>
           </property>
-          <property name="text">
-           <string>...</string>
-          </property>
          </widget>
         </item>
        </layout>
       </item>
       <item row="5" column="0">
-       <widget class="QLabel" name="label_7">
+       <widget class="QLabel" name="lblVideoFilenameTitle">
         <property name="text">
          <string>Video location:</string>
         </property>
@@ -234,7 +228,7 @@
        </widget>
       </item>
       <item row="7" column="0">
-       <widget class="QLabel" name="label_9">
+       <widget class="QLabel" name="lblFFMpegLocationTitle">
         <property name="text">
          <string>FFmpeg location:</string>
         </property>
@@ -244,7 +238,7 @@
        <widget class="KisFileNameRequester" name="ffmpegLocation" native="true"/>
       </item>
       <item row="8" column="0">
-       <widget class="QLabel" name="label_10">
+       <widget class="QLabel" name="lblFFMpegVersionTitle">
         <property name="text">
          <string>FFmpeg version:</string>
         </property>
@@ -273,6 +267,19 @@
      </property>
     </widget>
    </item>
+   <item>
+    <spacer name="bottomSpacer">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>0</width>
+       <height>0</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
   </layout>
  </widget>
  <customwidgets>
diff --git a/plugins/dockers/compositiondocker/compositiondocker_dock.cpp b/plugins/dockers/compositiondocker/compositiondocker_dock.cpp
index c7ceeff900a..856d68d89c6 100644
--- a/plugins/dockers/compositiondocker/compositiondocker_dock.cpp
+++ b/plugins/dockers/compositiondocker/compositiondocker_dock.cpp
@@ -45,6 +45,10 @@
 #include <kis_time_span.h>
 #include <KisMimeDatabase.h>
 
+#ifdef Q_OS_ANDROID
+#include <animation/KisAndroidMediaEncoderRunnable.h>
+#endif
+
 
 #include "compositionmodel.h"
 
@@ -338,6 +342,21 @@ void CompositionDockerDock::exportAnimationClicked()
     KisAnimationRenderingOptions exportOptions;
     exportOptions.fromProperties(settings);
 
+#ifdef Q_OS_ANDROID
+    KisMediaEncoderFormat *format = KisMediaEncoderWrapper::getFormatByKey(exportOptions.videoFormatKey);
+    // If there's no format configured, try to grab the first available one.
+    if (!format) {
+        const QVector<KisMediaEncoderFormat *> &supportedFormats = KisMediaEncoderWrapper::getSupportedFormats();
+        if (!supportedFormats.isEmpty()) {
+            format = supportedFormats.first();
+        }
+    }
+    // If there's still none, bail out.
+    if (!format) {
+        return;
+    }
+#endif
+
     if (m_canvas &&
         m_canvas->viewManager() &&
         m_canvas->viewManager()->image() &&
@@ -367,7 +386,11 @@ void CompositionDockerDock::exportAnimationClicked()
         KisLayerCompositionSP currentComposition = toQShared(new KisLayerComposition(image, "temp"));
         currentComposition->store();
 
+#ifdef Q_OS_ANDROID
+        const QString videoExtension = format->extension();
+#else
         const QString videoExtension = KisMimeDatabase::suffixesForMimeType(exportOptions.videoMimeType).first();
+#endif
 
         Q_FOREACH (KisLayerCompositionSP composition, image->compositions()) {
             if(!composition->isExportEnabled())
diff --git a/plugins/dockers/recorder/recorder_export.cpp b/plugins/dockers/recorder/recorder_export.cpp
index 302f96d54fe..9210446abed 100644
--- a/plugins/dockers/recorder/recorder_export.cpp
+++ b/plugins/dockers/recorder/recorder_export.cpp
@@ -300,6 +300,7 @@ public:
             settings->inputFilePaths,
             KisMediaEncoderWrapper::getFormatByKey(settings->selectedFormat),
             settings->formatPreferences.value(settings->selectedFormat).toMap(),
+            QString(),
             settings->resize ? settings->size : settings->imageSize,
             settings->inputFps,
             settings->fps,
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.