Commit a17370b8 authored by Dmitry Kazakov's avatar Dmitry Kazakov
Browse files

Use direct ffmpeg binary calls to convert Krita animation

We should drop the avTranscoder dep in the end. It doesn't support
image filtering in a proper way.

This patch also introduces an environment variable, add this line
if you want the set of PNG images be kept undeleted after the
encoding:

KRITA_KEEP_FRAMES=1 krita
parent bae1611c
......@@ -319,3 +319,8 @@ KisImportExportFilter::ConversionStatus KisAnimationExportSaver::saveFrameCallba
return status;
}
QString KisAnimationExportSaver::savedFilesMask() const
{
return m_d->filenamePrefix + "%04d" + m_d->filenameSuffix;
}
......@@ -80,6 +80,8 @@ public:
KisImportExportFilter::ConversionStatus exportAnimation();
QString savedFilesMask() const;
private:
KisImportExportFilter::ConversionStatus saveFrameCallback(int time, KisPaintDeviceSP frame);
......
......@@ -201,7 +201,7 @@ KisImportExportFilter::ConversionStatus KisPNGExport::convert(const QByteArray&
}
else {
options.alpha = isThereAlpha;
options.alpha = true;
options.interlace = false;
options.compression = 9;
options.tryToSaveAsIndexed = false;
......
......@@ -84,7 +84,7 @@ KisImportExportFilter::ConversionStatus KisVideoExport::convert(const QByteArray
askForOptions = true;
}
VideoSaver::AdditionalOptions additionalOptions;
QStringList additionalOptionsList;
askForOptions &=
!qApp->applicationName().toLower().contains("test") &
......@@ -97,14 +97,14 @@ KisImportExportFilter::ConversionStatus KisVideoExport::convert(const QByteArray
dlg.setCodec(codecIndex);
if (dlg.exec() == QDialog::Accepted) {
additionalOptions = dlg.getOptions();
additionalOptionsList = dlg.customUserOptions();
} else {
return KisImportExportFilter::UserCancelled;
}
}
VideoSaver kpc(input, getBatchMode());
KisImageBuilder_Result res = kpc.encode(filename, additionalOptions);
KisImageBuilder_Result res = kpc.encode(filename, additionalOptionsList);
if (res == KisImageBuilder_RESULT_OK) {
return KisImportExportFilter::OK;
......
......@@ -144,35 +144,33 @@ void VideoExportOptionsDialog::setCodec(CodecIndex index)
ui->cmbCodec->setCurrentIndex(int(index));
}
VideoSaver::AdditionalOptions VideoExportOptionsDialog::getOptions() const
QStringList VideoExportOptionsDialog::customUserOptions() const
{
VideoSaver::AdditionalOptions options;
QStringList options;
if (ui->cmbCodec->currentIndex() == int(CODEC_H264)) {
options["crf"] = QString::number(ui->intConstantRateFactor->value());
options << "-crf" << QString::number(ui->intConstantRateFactor->value());
const int presetIndex = ui->cmbPreset->currentIndex();
options["preset"] = m_d->presets[presetIndex].id();
options << "-preset" << m_d->presets[presetIndex].id();
const int profileIndex = ui->cmbProfile->currentIndex();
options["profile"] = m_d->profiles[profileIndex].id();
options << "-profile" << m_d->profiles[profileIndex].id();
if (m_d->profiles[profileIndex].id() == "high422") {
options["pix_fmt"] = "yuv422p";
options << "-pix_fmt" << "yuv422p";
} else if (m_d->profiles[profileIndex].id() == "high444") {
options["pix_fmt"] = "yuv444p";
options << "-pix_fmt" << "yuv444p";
} else {
options["pix_fmt"] = "yuv420p";
options << "-pix_fmt" << "yuv420p";
}
// Disabled! see the comment in c-tor!
//const int tuneIndex = ui->cmbTune->currentIndex();
//options["tune"] = m_d->tunes[tuneIndex].id();
//options << "-tune" << m_d->tunes[tuneIndex].id();
} else if (ui->cmbCodec->currentIndex() == int(CODEC_THEORA)) {
const qint64 bitRate = ui->intBitrate->value() * 1024;
options["bit_rate"] = bitRate;
options << "-b" << QString::number(ui->intBitrate->value()) + "k";
}
return options;
......
......@@ -44,7 +44,7 @@ public:
void setCodec(CodecIndex index);
VideoSaver::AdditionalOptions getOptions() const;
QStringList customUserOptions() const;
private Q_SLOTS:
void slotAccepted();
......
......@@ -34,159 +34,247 @@
#include "kis_animation_exporter.h"
#include <AvTranscoder/transcoder/Transcoder.hpp>
#include <AvTranscoder/file/OutputFile.hpp>
#include <AvTranscoder/progress/ConsoleProgress.hpp>
#include <AvTranscoder/decoder/VideoGenerator.hpp>
#include <AvTranscoder/data/decoded/VideoFrame.hpp>
#include <QFileSystemWatcher>
#include <QProcess>
#include <QProgressDialog>
#include <QEventLoop>
#include <QTemporaryFile>
#include <QTemporaryDir>
#include "KisPart.h"
class KisFFMpegProgressWatcher : public QObject {
Q_OBJECT
public:
KisFFMpegProgressWatcher(QFile &progressFile, int totalFrames)
: m_progressFile(progressFile),
m_totalFrames(totalFrames)
{
connect(&m_progressWatcher, SIGNAL(fileChanged(QString)), SLOT(slotFileChanged()));
VideoSaver::VideoSaver(KisDocument *doc, bool batchMode)
: m_image(doc->image())
, m_doc(doc)
, m_batchMode(batchMode)
, m_stop(false)
{
}
m_progressWatcher.addPath(m_progressFile.fileName());
}
VideoSaver::~VideoSaver()
{
}
private Q_SLOTS:
void slotFileChanged() {
//qDebug() << "=== progress changed ===";
KisImageWSP VideoSaver::image()
{
return m_image;
}
int currentFrame = -1;
bool isEnded = false;
struct FrameUploader {
FrameUploader(avtranscoder::StreamTranscoder &_transcoder,
const avtranscoder::VideoFrameDesc& frameDesc,
const QRect &_bounds,
const KoColorSpace *_dstCS)
: transcoder(_transcoder),
frameData(frameDesc),
dstCS(_dstCS),
bounds(_bounds)
{
generator =
dynamic_cast<avtranscoder::VideoGenerator*>(transcoder.getCurrentDecoder());
KIS_ASSERT_RECOVER_NOOP(generator);
buffer.resize(dstCS->pixelSize() * frameDesc._width * frameDesc._height);
}
while(!m_progressFile.atEnd()) {
QString line = QString(m_progressFile.readLine()).remove(QChar('\n'));
QStringList var = line.split("=");
KisImportExportFilter::ConversionStatus
operator() (int time, KisPaintDeviceSP dev) {
Q_UNUSED(time);
dev->convertTo(dstCS);
dev->readBytes(buffer.data(), bounds);
frameData.assign(buffer.constData());
generator->setNextFrame(frameData);
transcoder.processFrame();
if (var[0] == "frame") {
currentFrame = var[1].toInt();
} else if (var[0] == "progress") {
isEnded = var[1] == "end";
}
//qDebug() << var;
}
return KisImportExportFilter::OK;
if (isEnded) {
emit sigProgressChanged(100);
emit sigProcessingFinished();
} else {
emit sigProgressChanged(100 * currentFrame / m_totalFrames);
}
}
avtranscoder::StreamTranscoder &transcoder;
avtranscoder::VideoGenerator *generator;
avtranscoder::VideoFrame frameData;
const KoColorSpace *dstCS;
QRect bounds;
QVector<quint8> buffer;
Q_SIGNALS:
void sigProgressChanged(int percent);
void sigProcessingFinished();
private:
QFileSystemWatcher m_progressWatcher;
QFile &m_progressFile;
int m_totalFrames;
};
KisImageBuilder_Result VideoSaver::encode(const QString &filename, const QMap<QString, QString> &additionalOptions)
class KisFFMpegRunner
{
if (qEnvironmentVariableIsSet("KRITA_DEBUG_FFMPEG")) {
avtranscoder::Logger::setLogLevel(AV_LOG_DEBUG);
} else {
avtranscoder::Logger::setLogLevel(AV_LOG_QUIET);
public:
KisFFMpegRunner()
: m_cancelled(false) {}
public:
KisImageBuilder_Result runFFMpeg(const QStringList &specialArgs,
const QString &actionName,
const QString &logPath,
int totalFrames) {
QTemporaryFile progressFile("KritaFFmpegProgress.XXXXXX");
progressFile.open();
m_process.setStandardOutputFile(logPath);
m_process.setProcessChannelMode(QProcess::MergedChannels);
QStringList args;
args << "-v" << "debug"
<< "-nostdin"
<< specialArgs;
m_cancelled = false;
m_process.start("ffmpeg", args);
return waitForFFMpegProcess(actionName, progressFile, m_process, totalFrames);
}
const QFileInfo fileInfo(filename);
const QString suffix = fileInfo.suffix().toLower();
using namespace avtranscoder::constants;
avtranscoder::ProfileLoader::Profile videoProfile;
if (suffix == "gif") {
videoProfile[avProfileIdentificator] = "gif";
videoProfile[avProfileIdentificatorHuman] = "GIF format codec";
videoProfile[avProfileType] = avProfileTypeVideo;
videoProfile[avProfilePixelFormat] = "bgr8";
videoProfile[avProfileCodec] = "gif";
videoProfile["gifflags"] = "-transdiff";
} else if (suffix == "mkv" || suffix == "mp4") {
avtranscoder::ProfileLoader loader(true);
videoProfile = loader.getProfile("h264-hq");
} else if (suffix == "ogv") {
videoProfile[avProfileIdentificator] = "theora";
videoProfile[avProfileIdentificatorHuman] = "Xiph.Org Theora";
videoProfile[avProfileType] = avProfileTypeVideo;
videoProfile[avProfilePixelFormat] = "yuv422p";
videoProfile[avProfileCodec] = "theora";
void cancel() {
m_cancelled = true;
m_process.kill();
}
for (auto it = additionalOptions.constBegin();
it != additionalOptions.constEnd(); ++it) {
private:
KisImageBuilder_Result waitForFFMpegProcess(const QString &message,
QFile &progressFile,
QProcess &ffmpegProcess,
int totalFrames) {
KisFFMpegProgressWatcher watcher(progressFile, totalFrames);
QProgressDialog progress(message, "", 0, 0, KisPart::instance()->currentMainwindow());
progress.setWindowModality(Qt::ApplicationModal);
progress.setCancelButton(0);
progress.setMinimumDuration(0);
progress.setValue(0);
progress.setRange(0,100);
QEventLoop loop;
loop.connect(&watcher, SIGNAL(sigProcessingFinished()), SLOT(quit()));
loop.connect(&ffmpegProcess, SIGNAL(finished(int, QProcess::ExitStatus)), SLOT(quit()));
loop.connect(&watcher, SIGNAL(sigProgressChanged(int)), &progress, SLOT(setValue(int)));
loop.exec();
// wait for some errorneous case
ffmpegProcess.waitForFinished(5000);
KisImageBuilder_Result retval = KisImageBuilder_RESULT_OK;
if (ffmpegProcess.state() != QProcess::NotRunning) {
// sorry...
ffmpegProcess.kill();
retval = KisImageBuilder_RESULT_FAILURE;
} else if (m_cancelled) {
retval = KisImageBuilder_RESULT_CANCEL;
} else if (ffmpegProcess.exitCode()) {
retval = KisImageBuilder_RESULT_FAILURE;
}
videoProfile[it.key().toStdString()] = it.value().toStdString();
return retval;
}
private:
QProcess m_process;
bool m_cancelled;
};
KisImageBuilder_Result retval= KisImageBuilder_RESULT_OK;
KisImageAnimationInterface *animation = m_image->animationInterface();
const KoColorSpace *dstCS = KoColorSpaceRegistry::instance()->rgb8();
const QRect saveRect = m_image->bounds();
VideoSaver::VideoSaver(KisDocument *doc, bool batchMode)
: m_image(doc->image())
, m_doc(doc)
, m_batchMode(batchMode)
, m_runner(new KisFFMpegRunner)
{
}
VideoSaver::~VideoSaver()
{
}
AVPixelFormat inputPixelFormat = AV_PIX_FMT_BGRA;
KisImageSP VideoSaver::image()
{
return m_image;
}
try {
avtranscoder::preloadCodecsAndFormats();
KisImageBuilder_Result VideoSaver::encode(const QString &filename, const QStringList &additionalOptionsList)
{
KisImageBuilder_Result retval= KisImageBuilder_RESULT_OK;
avtranscoder::OutputFile outputFile(filename.toStdString());
KisImageAnimationInterface *animation = m_image->animationInterface();
const KisTimeRange clipRange = animation->fullClipRange();
const int frameRate = animation->framerate();
avtranscoder::VideoCodec inputCodec(avtranscoder::eCodecTypeDecoder, AV_CODEC_ID_RAWVIDEO);
avtranscoder::VideoFrameDesc imageDesc(saveRect.width(), saveRect.height(), inputPixelFormat);
imageDesc._fps = animation->framerate();
inputCodec.setImageParameters(imageDesc);
const QString resultFile = filename;
const bool removeGeneratedFiles =
!qEnvironmentVariableIsSet("KRITA_KEEP_FRAMES");
avtranscoder::StreamTranscoder transcoder(inputCodec, outputFile, videoProfile);
const QFileInfo info(resultFile);
const QString suffix = info.suffix().toLower();
const QString baseDirectory = info.absolutePath();
const QString frameDirectoryTemplate = baseDirectory + QDir::separator() + "frames.XXXXXX";
QTemporaryDir framesDirImpl(frameDirectoryTemplate);
framesDirImpl.setAutoRemove(removeGeneratedFiles);
avtranscoder::VideoGenerator *generator =
dynamic_cast<avtranscoder::VideoGenerator*>(transcoder.getCurrentDecoder());
KIS_ASSERT_RECOVER_NOOP(generator);
const QDir framesDir(framesDirImpl.path());
{
outputFile.beginWrap();
transcoder.preProcessCodecLatency();
const QString framesPath = framesDir.path();
const QString framesBasePath = framesDir.filePath("frame.png");
const QString palettePath = framesDir.filePath("palette.png");
KisTimeRange clipRange = animation->fullClipRange();
KisAnimationExporter exporter(m_doc, clipRange.start(), clipRange.end());
KisAnimationExportSaver saver(m_doc, framesBasePath, clipRange.start(), clipRange.end());
FrameUploader uploader(transcoder, imageDesc, saveRect, dstCS);
exporter.setSaveFrameCallback(uploader);
KisImportExportFilter::ConversionStatus status =
saver.exportAnimation();
KisImportExportFilter::ConversionStatus status =
exporter.exportAnimation();
if (status == KisImportExportFilter::UserCancelled) {
return KisImageBuilder_RESULT_CANCEL;
} else if (status != KisImportExportFilter::OK) {
return KisImageBuilder_RESULT_FAILURE;
}
if (status == KisImportExportFilter::UserCancelled) {
retval = KisImageBuilder_RESULT_CANCEL;
} else if (status != KisImportExportFilter::OK) {
retval = KisImageBuilder_RESULT_FAILURE;
if (suffix == "gif") {
{
QStringList args;
args << "-r" << QString::number(frameRate)
<< "-i" << saver.savedFilesMask()
<< "-vf" << "palettegen"
<< "-y" << palettePath;
KisImageBuilder_Result result =
m_runner->runFFMpeg(args, i18n("Fetching palette..."),
framesDir.filePath("log_palettegen.log"),
clipRange.duration());
if (result) {
return result;
}
outputFile.endWrap();
}
} catch(std::exception& e) {
std::cerr << "ERROR: during process, an error occured: " << e.what() << std::endl;
}
catch(...) {
std::cerr << "ERROR: during process, an unknown error occured" << std::endl;
{
QStringList args;
args << "-r" << QString::number(frameRate)
<< "-i" << saver.savedFilesMask()
<< "-i" << palettePath
<< "-lavfi" << "[0:v][1:v] paletteuse"
<< additionalOptionsList
<< "-y" << resultFile;
KisImageBuilder_Result result =
m_runner->runFFMpeg(args, i18n("Encoding frames..."),
framesDir.filePath("log_paletteuse.log"),
clipRange.duration());
if (result) {
return result;
}
}
} else {
QStringList args;
args << "-r" << QString::number(frameRate)
<< "-i" << saver.savedFilesMask()
<< additionalOptionsList
<< "-y" << resultFile;
KisImageBuilder_Result result =
m_runner->runFFMpeg(args, i18n("Encoding frames..."),
framesDir.filePath("log_encode.log"),
clipRange.duration());
if (result) {
return result;
}
}
return retval;
......@@ -194,5 +282,7 @@ KisImageBuilder_Result VideoSaver::encode(const QString &filename, const QMap<QS
void VideoSaver::cancel()
{
m_stop = true;
m_runner->cancel();
}
#include "video_saver.moc"
......@@ -27,6 +27,8 @@
#include "KisImageBuilderResult.h"
#include "kritavideoexport_export.h"
class KisFFMpegRunner;
/* The KisImageBuilder_Result definitions come from kis_png_converter.h here */
class KisDocument;
......@@ -34,14 +36,12 @@ class KisDocument;
class KRITAVIDEOEXPORT_EXPORT VideoSaver : public QObject {
Q_OBJECT
public:
typedef QMap<QString, QString> AdditionalOptions;
public:
VideoSaver(KisDocument* doc, bool batchMode);
virtual ~VideoSaver();
KisImageWSP image();
KisImageBuilder_Result encode(const QString &filename, const AdditionalOptions &additionalOptions = AdditionalOptions());
KisImageSP image();
KisImageBuilder_Result encode(const QString &filename, const QStringList &additionalOptionsList = QStringList());
private Q_SLOTS:
void cancel();
......@@ -50,7 +50,7 @@ private:
KisImageSP m_image;
KisDocument* m_doc;
bool m_batchMode;
bool m_stop;
QScopedPointer<KisFFMpegRunner> m_runner;
};
#endif
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment