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