/*
* 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 .
*/
#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
#include
#include
#include
#include
#include
#include
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);
}