216 lines
7.2 KiB
C++
216 lines
7.2 KiB
C++
/*
|
|
* Copyright (c) 2025 Meltytech, LLC
|
|
*
|
|
* 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 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "htmlgeneratorjob.h"
|
|
|
|
#include "Logger.h"
|
|
#include "mainwindow.h"
|
|
#include "mltcontroller.h"
|
|
#include "settings.h"
|
|
#include "shotcut_mlt_properties.h"
|
|
#include "widgets/htmlgeneratorwidget.h"
|
|
|
|
#include <QAction>
|
|
#include <QApplication>
|
|
#include <QDir>
|
|
#include <QFileInfo>
|
|
#include <QRegularExpression>
|
|
#include <QStandardPaths>
|
|
#include <QTemporaryFile>
|
|
|
|
static double fps()
|
|
{
|
|
return std::min(15.0, MLT.profile().fps());
|
|
}
|
|
|
|
HtmlGeneratorJob::HtmlGeneratorJob(const QString &name,
|
|
const QString &html,
|
|
const QString &outputPath,
|
|
int duration,
|
|
QThread::Priority priority)
|
|
: AbstractJob(name, priority)
|
|
, m_html(html)
|
|
, m_outputPath(outputPath)
|
|
, m_duration(duration)
|
|
, m_generator(nullptr)
|
|
, m_isGeneratingFrames(true)
|
|
, m_previousPercent(0)
|
|
{
|
|
setTarget(outputPath);
|
|
|
|
// Create temporary directory for animation frames
|
|
const auto outDir = QFileInfo(m_outputPath).dir();
|
|
m_tempDir.reset(new QTemporaryDir(outDir.filePath("shotcut-htmlgen-XXXXXX")));
|
|
if (!m_tempDir->isValid()) {
|
|
LOG_ERROR() << "Failed to create temp directory for HTML animation frames:"
|
|
<< m_tempDir->path();
|
|
}
|
|
|
|
auto *action = new QAction(tr("Open"), this);
|
|
action->setData("Open");
|
|
connect(action, &QAction::triggered, this, &HtmlGeneratorJob::onOpenTriggered);
|
|
m_successActions << action;
|
|
}
|
|
|
|
void HtmlGeneratorJob::start()
|
|
{
|
|
if (!m_tempDir || !m_tempDir->isValid()) {
|
|
LOG_ERROR() << "Invalid temp directory";
|
|
return;
|
|
}
|
|
|
|
// Create HTML file
|
|
m_htmlFilePath = m_tempDir->path() + "/animation.html";
|
|
QFile htmlFile(m_htmlFilePath);
|
|
if (!htmlFile.open(QIODevice::WriteOnly)) {
|
|
LOG_ERROR() << "Failed to create HTML file:" << htmlFile.errorString();
|
|
return;
|
|
}
|
|
htmlFile.write(m_html.toUtf8());
|
|
htmlFile.flush();
|
|
htmlFile.close();
|
|
|
|
// Create HTML generator for animation
|
|
m_generator = new HtmlGenerator(this);
|
|
|
|
// Set animation parameters: fps, duration in milliseconds
|
|
m_generator->setAnimationParameters(fps(), m_duration);
|
|
|
|
connect(m_generator,
|
|
&HtmlGenerator::imageReady,
|
|
this,
|
|
&HtmlGeneratorJob::onAnimationFramesReady);
|
|
connect(m_generator,
|
|
&HtmlGenerator::progressUpdate,
|
|
this,
|
|
&HtmlGeneratorJob::onHtmlGeneratorProgress);
|
|
|
|
const QString url("file://" + m_htmlFilePath);
|
|
const QSize size(qRound(MLT.profile().width() * MLT.profile().sar()), MLT.profile().height());
|
|
|
|
// Start the animation capture
|
|
AbstractJob::start();
|
|
m_isGeneratingFrames = true;
|
|
|
|
m_generator->launchBrowser(Settings.chromiumPath(), url, size, m_tempDir->path());
|
|
|
|
LOG_DEBUG() << "Started HTML animation generation:" << url;
|
|
}
|
|
|
|
void HtmlGeneratorJob::onAnimationFramesReady()
|
|
{
|
|
LOG_DEBUG() << "Animation frames ready, starting FFmpeg conversion";
|
|
m_isGeneratingFrames = false;
|
|
emit progressUpdated(m_item, 80); // Frames generation is done
|
|
|
|
// Clean up the generator
|
|
if (m_generator) {
|
|
m_generator->deleteLater();
|
|
m_generator = nullptr;
|
|
}
|
|
|
|
// Now start FFmpeg process
|
|
const auto shotcutPath = qApp->applicationDirPath();
|
|
const QFileInfo ffmpegPath(shotcutPath, "ffmpeg");
|
|
|
|
QStringList args;
|
|
args << "-r" << QString::number(fps()) << "-i" << m_tempDir->path() + "/frame_%04d.png"
|
|
<< "-codec:v"
|
|
<< "utvideo"
|
|
<< "-pix_fmt"
|
|
<< "gbrap"
|
|
<< "-y" << m_outputPath;
|
|
|
|
setReadChannel(QProcess::StandardError);
|
|
LOG_DEBUG() << ffmpegPath.absoluteFilePath() + " " + args.join(' ');
|
|
|
|
// Start FFmpeg process (this will call AbstractJob::start with program and args)
|
|
AbstractJob::start(ffmpegPath.absoluteFilePath(), args);
|
|
}
|
|
|
|
void HtmlGeneratorJob::onReadyRead()
|
|
{
|
|
if (m_isGeneratingFrames) {
|
|
return; // Don't process FFmpeg output during frame generation
|
|
}
|
|
|
|
// Process FFmpeg output for progress reporting
|
|
QString msg;
|
|
do {
|
|
msg = readLine();
|
|
if (!msg.startsWith("frame=") && (!msg.trimmed().isEmpty())) {
|
|
appendToLog(msg);
|
|
}
|
|
|
|
// Look for progress information in FFmpeg output
|
|
if (msg.contains("frame=")) {
|
|
// Extract frame number from FFmpeg output
|
|
static QRegularExpression frameRegex("frame=\\s*(\\d+)");
|
|
const auto match = frameRegex.match(msg);
|
|
if (match.hasMatch()) {
|
|
int currentFrame = match.captured(1).toInt();
|
|
int totalFrames = qRound((m_duration / 1000.0) * fps());
|
|
int percent = 80 + qRound((currentFrame * 80.0) / totalFrames); // 80-100%
|
|
if (percent != m_previousPercent) {
|
|
emit progressUpdated(m_item, percent);
|
|
m_previousPercent = percent;
|
|
}
|
|
}
|
|
}
|
|
} while (!msg.isEmpty());
|
|
}
|
|
|
|
void HtmlGeneratorJob::onHtmlGeneratorProgress(float progress)
|
|
{
|
|
if (m_isGeneratingFrames) {
|
|
// Map 0.0-1.0 to 0-80%
|
|
const auto percent = qRound(progress * 80.0);
|
|
emit progressUpdated(m_item, percent);
|
|
}
|
|
}
|
|
|
|
void HtmlGeneratorJob::onFinished(int exitCode, QProcess::ExitStatus exitStatus)
|
|
{
|
|
AbstractJob::onFinished(exitCode, exitStatus);
|
|
|
|
if (exitCode == 0 && QFileInfo::exists(m_outputPath)) {
|
|
// Automatically open the captured file
|
|
QTimer::singleShot(0, this, SLOT(onOpenTriggered()));
|
|
}
|
|
}
|
|
|
|
void HtmlGeneratorJob::onOpenTriggered()
|
|
{
|
|
auto p = new Mlt::Producer(MLT.profile(), m_outputPath.toUtf8().constData());
|
|
p->set(kPrivateProducerProperty, property(kPrivateProducerProperty).toString().toLatin1());
|
|
p->set(HtmlGeneratorWidget::kColorProperty,
|
|
property(HtmlGeneratorWidget::kColorProperty).toByteArray());
|
|
p->set(HtmlGeneratorWidget::kCssProperty,
|
|
property(HtmlGeneratorWidget::kCssProperty).toByteArray());
|
|
p->set(HtmlGeneratorWidget::kBodyProperty,
|
|
property(HtmlGeneratorWidget::kBodyProperty).toByteArray());
|
|
p->set(HtmlGeneratorWidget::kJavaScriptProperty,
|
|
property(HtmlGeneratorWidget::kJavaScriptProperty).toByteArray());
|
|
p->set(HtmlGeneratorWidget::kLine1Property,
|
|
property(HtmlGeneratorWidget::kLine1Property).toByteArray());
|
|
p->set(HtmlGeneratorWidget::kLine2Property,
|
|
property(HtmlGeneratorWidget::kLine2Property).toByteArray());
|
|
p->set(HtmlGeneratorWidget::kLine3Property,
|
|
property(HtmlGeneratorWidget::kLine3Property).toByteArray());
|
|
MAIN.open(p, false);
|
|
}
|