File indexing completed on 2024-12-08 04:27:17

0001 /*
0002     SPDX-FileCopyrightText: 2022 Meltytech, LLC
0003     SPDX-FileCopyrightText: 2022 Julius Künzel <jk.kdedev@smartlab.uber.space>
0004 
0005     SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0006 */
0007 
0008 #include "glaxnimatelauncher.h"
0009 #include "bin/projectclip.h"
0010 #include "bin/projectitemmodel.h"
0011 #include "core.h"
0012 #include "dialogs/kdenlivesettingsdialog.h"
0013 #include "doc/kthumb.h"
0014 #include "kdenlivesettings.h"
0015 #include "mainwindow.h"
0016 #include "timeline2/view/timelinewidget.h"
0017 
0018 #include <KConfigDialog>
0019 #include <KMessageBox>
0020 #include <KUrlRequesterDialog>
0021 #include <QLocalServer>
0022 #include <QLocalSocket>
0023 #include <QSharedMemory>
0024 
0025 bool GlaxnimateLauncher::checkInstalled()
0026 {
0027     if (!KdenliveSettings::glaxnimatePath().isEmpty() && QFile(KdenliveSettings::glaxnimatePath()).exists()) {
0028         return true;
0029     }
0030     QUrl url = KUrlRequesterDialog::getUrl(QUrl(), nullptr, i18n("Enter path to the Glaxnimate application"));
0031     if (url.isEmpty() || !QFile(url.toLocalFile()).exists()) {
0032         KMessageBox::error(QApplication::activeWindow(), i18n("You need enter a valid path to be able to edit Lottie animations."));
0033         return false;
0034     }
0035     KdenliveSettings::setGlaxnimatePath(url.toLocalFile());
0036     KdenliveSettingsDialog *d = static_cast<KdenliveSettingsDialog *>(KConfigDialog::exists(QStringLiteral("settings")));
0037     if (d) {
0038         d->updateExternalApps();
0039     }
0040     return true;
0041 }
0042 
0043 GlaxnimateLauncher &GlaxnimateLauncher::instance()
0044 {
0045     static GlaxnimateLauncher instance;
0046     return instance;
0047 }
0048 
0049 void GlaxnimateLauncher::reset()
0050 {
0051     if (m_stream && m_socket && m_stream && QLocalSocket::ConnectedState == m_socket->state()) {
0052         *m_stream << QString("clear");
0053         m_socket->flush();
0054     }
0055     m_parent.reset();
0056 }
0057 
0058 void GlaxnimateLauncher::openFile(const QString &filename)
0059 {
0060     QString error = pCore->openExternalApp(KdenliveSettings::glaxnimatePath(), {filename});
0061     if (!error.isEmpty()) {
0062         KMessageBox::detailedError(QApplication::activeWindow(), i18n("Failed to launch Glaxnimate application"), error);
0063         return;
0064     }
0065 }
0066 
0067 void GlaxnimateLauncher::openClip(int clipId)
0068 {
0069     if (!checkInstalled()) {
0070         return;
0071     }
0072     if ((m_server && m_socket && m_stream && QLocalSocket::ConnectedState == m_socket->state()) || (m_parent && m_parent->m_clipId != -1)) {
0073         // There is already an open connection, it is only supported to send the background to one Glaxnimate instance at the time
0074         // Open the clip without sending the background
0075         openFile(pCore->projectItemModel()->getClipByBinID(pCore->window()->getCurrentTimeline()->model()->getClipBinId(clipId))->clipUrl());
0076         return;
0077     }
0078     m_parent.reset(new ParentResources);
0079 
0080     m_parent->m_binClip = pCore->projectItemModel()->getClipByBinID(pCore->window()->getCurrentTimeline()->model()->getClipBinId(clipId));
0081     if (m_parent->m_binClip->clipType() != ClipType::Animation) {
0082         pCore->displayMessage(i18n("Item is not an animation clip"), ErrorMessage, 500);
0083         return;
0084     }
0085 
0086     QString filename = m_parent->m_binClip->clipUrl();
0087     m_parent->m_clipId = clipId;
0088     m_server.reset(new QLocalServer);
0089     connect(m_server.get(), &QLocalServer::newConnection, this, &GlaxnimateLauncher::onConnect);
0090     QString name = QString("kdenlive-%1").arg(QCoreApplication::applicationPid());
0091     QStringList args = {"--ipc", name, filename};
0092     /*QProcess childProcess;
0093     QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
0094     env.remove("LC_ALL");
0095     childProcess.setProcessEnvironment(env);*/
0096     QString error;
0097     if (!m_server->listen(name)) {
0098         qDebug() << "failed to start the IPC server:" << m_server->errorString();
0099         m_server.reset();
0100         args.clear();
0101         args << filename;
0102         qDebug() << "Run without --ipc";
0103         error = pCore->openExternalApp(KdenliveSettings::glaxnimatePath(), args);
0104         if (error.isEmpty()) {
0105             m_sharedMemory.reset(new QSharedMemory(name));
0106             return;
0107         }
0108     } else {
0109         if (pCore->openExternalApp(KdenliveSettings::glaxnimatePath(), args).isEmpty()) {
0110             m_sharedMemory.reset(new QSharedMemory(name));
0111             return;
0112         } else {
0113             // This glaxnimate executable may not support --ipc
0114             // XXX startDetached is not failing in this case, need something better
0115             qDebug() << "Failed to start glaxnimate with the --ipc, trying without now";
0116             m_server.reset();
0117             args.clear();
0118             args << filename;
0119             qDebug() << "Run without --ipc";
0120             error = pCore->openExternalApp(KdenliveSettings::glaxnimatePath(), args);
0121         }
0122     }
0123     if (!error.isEmpty()) {
0124         KMessageBox::detailedError(QApplication::activeWindow(), i18n("Failed to launch Glaxnimate application"), error);
0125         return;
0126     }
0127 }
0128 
0129 void GlaxnimateLauncher::onConnect()
0130 {
0131     m_socket = m_server->nextPendingConnection();
0132     connect(m_socket, &QLocalSocket::readyRead, this, &GlaxnimateLauncher::onReadyRead);
0133     connect(m_socket, &QLocalSocket::errorOccurred, this, &GlaxnimateLauncher::onSocketError);
0134     m_stream.reset(new QDataStream(m_socket));
0135     m_stream->setVersion(QDataStream::Qt_5_15);
0136     *m_stream << QString("hello");
0137     m_socket->flush();
0138     m_server->close();
0139     m_isProtocolValid = false;
0140 }
0141 
0142 void GlaxnimateLauncher::onReadyRead()
0143 {
0144     if (!m_isProtocolValid) {
0145         QString message;
0146         *m_stream >> message;
0147         qDebug() << message;
0148         if (message.startsWith("version ") && message != "version 1") {
0149             *m_stream << QString("bye");
0150             m_socket->flush();
0151             m_server->close();
0152         } else {
0153             m_isProtocolValid = true;
0154         }
0155     } else {
0156         qreal time = -1.0;
0157         for (int i = 0; i < 1000 && !m_stream->atEnd(); i++) {
0158             *m_stream >> time;
0159         }
0160 
0161         // Only if the frame number is different
0162         int frameNum = pCore->window()->getCurrentTimeline()->model()->getClipPosition(m_parent->m_clipId) + time -
0163                        pCore->window()->getCurrentTimeline()->model()->getClipIn(m_parent->m_clipId);
0164         if (frameNum != m_parent->m_frameNum) {
0165             qDebug() << "glaxnimate time =" << time << "=> Kdenlive frameNum =" << frameNum;
0166 
0167             // Get the image from MLT
0168             pCore->window()->getCurrentTimeline()->model()->producer().get()->seek(frameNum);
0169             QList<int> clips = m_parent->m_binClip->timelineInstances();
0170             // Temporarily hide this title clip in timeline so that it does not appear when requesting background frame
0171             pCore->temporaryUnplug(clips, true);
0172             std::unique_ptr<Mlt::Frame> frame(pCore->window()->getCurrentTimeline()->model()->producer().get()->get_frame());
0173             QImage temp = KThumb::getFrame(frame.get(), pCore->getCurrentFrameSize().width(), pCore->getCurrentFrameSize().height());
0174             pCore->temporaryUnplug(clips, false);
0175             if (copyToShared(temp)) {
0176                 m_parent->m_frameNum = frameNum;
0177             }
0178         }
0179     }
0180 }
0181 
0182 void GlaxnimateLauncher::onSocketError(QLocalSocket::LocalSocketError socketError)
0183 {
0184     switch (socketError) {
0185     case QLocalSocket::PeerClosedError:
0186         qDebug() << "Glaxnimate closed the connection";
0187         m_parent->m_clipId = -1;
0188         m_stream.reset();
0189         m_sharedMemory.reset();
0190         break;
0191     default:
0192         qDebug() << "Glaxnimate IPC error:" << m_socket->errorString();
0193     }
0194 }
0195 
0196 bool GlaxnimateLauncher::copyToShared(const QImage &image)
0197 {
0198     if (!m_sharedMemory) {
0199         return false;
0200     }
0201     qint32 sizeInBytes = image.sizeInBytes() + 4 * sizeof(qint32);
0202     if (sizeInBytes > m_sharedMemory->size()) {
0203         if (m_sharedMemory->isAttached()) {
0204             m_sharedMemory->lock();
0205             m_sharedMemory->detach();
0206             m_sharedMemory->unlock();
0207         }
0208         // over-allocate to avoid recreating
0209         if (!m_sharedMemory->create(sizeInBytes)) {
0210             qDebug() << m_sharedMemory->errorString();
0211             return false;
0212         }
0213     }
0214     if (m_sharedMemory->isAttached()) {
0215         m_sharedMemory->lock();
0216 
0217         uchar *to = (uchar *)m_sharedMemory->data();
0218         // Write the width of the image and move the pointer forward
0219         qint32 width = image.width();
0220         ::memcpy(to, &width, sizeof(width));
0221         to += sizeof(width);
0222 
0223         // Write the height of the image and move the pointer forward
0224         qint32 height = image.height();
0225         ::memcpy(to, &height, sizeof(height));
0226         to += sizeof(height);
0227 
0228         // Write the image format of the image and move the pointer forward
0229         qint32 imageFormat = image.format();
0230         ::memcpy(to, &imageFormat, sizeof(imageFormat));
0231         to += sizeof(imageFormat);
0232 
0233         // Write the bytes per line of the image and move the pointer forward
0234         qint32 bytesPerLine = image.bytesPerLine();
0235         ::memcpy(to, &bytesPerLine, sizeof(bytesPerLine));
0236         to += sizeof(bytesPerLine);
0237 
0238         // Write the raw data of the image and move the pointer forward
0239         ::memcpy(to, image.constBits(), image.sizeInBytes());
0240 
0241         m_sharedMemory->unlock();
0242         if (m_stream && m_socket) {
0243             *m_stream << QString("redraw");
0244             m_socket->flush();
0245         }
0246         return true;
0247     }
0248     return false;
0249 }