diff --git a/plugins/dockers/CMakeLists.txt b/plugins/dockers/CMakeLists.txt --- a/plugins/dockers/CMakeLists.txt +++ b/plugins/dockers/CMakeLists.txt @@ -24,6 +24,7 @@ add_subdirectory(svgcollectiondocker) add_subdirectory(histogram) add_subdirectory(gamutmask) +add_subdirectory(recorder) if(NOT APPLE AND HAVE_QT_QUICK) add_subdirectory(touchdocker) option(ENABLE_CPU_THROTTLE "Build the CPU Throttle Docker" OFF) diff --git a/plugins/dockers/recorder/CMakeLists.txt b/plugins/dockers/recorder/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/plugins/dockers/recorder/CMakeLists.txt @@ -0,0 +1,13 @@ +include_directories(/usr/local/include/gstreamer-1.0 + /usr/include/glib-2.0 + /usr/lib/x86_64-linux-gnu/glib-2.0/include) + +set(KRITA_RECORDERDOCKER_SOURCES recorderdocker.cpp recorderdocker_dock.cpp encoder.cpp) +add_library(kritarecorderdocker MODULE ${KRITA_RECORDERDOCKER_SOURCES}) +target_link_libraries(kritarecorderdocker kritaui) +target_link_libraries(kritarecorderdocker gstreamer-1.0) +target_link_libraries(kritarecorderdocker glib-2.0) +target_link_libraries(kritarecorderdocker gobject-2.0) +target_link_libraries(kritarecorderdocker gstapp-1.0) +target_link_libraries(kritarecorderdocker gstbase-1.0) +install(TARGETS kritarecorderdocker DESTINATION ${KRITA_PLUGIN_INSTALL_DIR}) diff --git a/plugins/dockers/recorder/encoder.h b/plugins/dockers/recorder/encoder.h new file mode 100644 --- /dev/null +++ b/plugins/dockers/recorder/encoder.h @@ -0,0 +1,43 @@ +#ifndef ENCODER_H +#define ENCODER_H + +#include +#include +#include +#include +#include +#include + +class Encoder +{ + GstPipeline *m_pipeline; + GstAppSrc *m_src; + GstElement *m_filter1; + GstElement *m_encoder; + GstElement *m_videoconvert; + GstElement *m_queue; + GstElement *m_webmmux; + GstElement *m_sink; + int m_width; + int m_height; + GstClockTime m_timestamp; + +public: + Encoder() + :m_pipeline(nullptr), + m_src(nullptr), + m_filter1(nullptr), + m_encoder(nullptr), + m_videoconvert(nullptr), + m_webmmux(nullptr), + m_sink(nullptr) + {} + + void init(const char *filename, int width, int height); + + void pushFrame( gpointer data, gsize size); + + void finish(); +}; + +#endif \ No newline at end of file diff --git a/plugins/dockers/recorder/encoder.cpp b/plugins/dockers/recorder/encoder.cpp new file mode 100644 --- /dev/null +++ b/plugins/dockers/recorder/encoder.cpp @@ -0,0 +1,92 @@ +#include "encoder.h" +#include + +void Encoder::init(const char* filename, int width, int height) +{ + GstStateChangeReturn state_ret; + gst_init(NULL, NULL); + + m_width = width; + m_height = height; + + m_pipeline = (GstPipeline*)gst_pipeline_new("mypipeline"); + m_src = (GstAppSrc*)gst_element_factory_make("appsrc", "mysrc"); + m_filter1 = gst_element_factory_make ("capsfilter", "myfilter1"); + m_videoconvert = gst_element_factory_make ("videoconvert", "vc"); + m_encoder = gst_element_factory_make ("vp9enc", "my9enc"); + m_queue = gst_element_factory_make("queue", "qu"); + m_webmmux = gst_element_factory_make("webmmux", "mymux"); + m_sink = gst_element_factory_make ("filesink" , NULL); + m_timestamp = 0; + if( !m_pipeline || + !m_src || !m_filter1 || + !m_encoder || /*!m_videoconvert ||*/ !m_webmmux || /*!m_queue ||*/ + !m_sink ) { + printf("Error creating pipeline elements!\n"); + exit(2); + } + + gst_bin_add_many( + GST_BIN(m_pipeline), + (GstElement*)m_src, + m_filter1, + m_videoconvert, + m_encoder, + m_queue, + m_webmmux, + m_sink, + NULL); + + qDebug() << "width " << m_width << " height " << m_height; + + g_object_set (m_src, "format", GST_FORMAT_TIME, NULL); + g_object_set( m_src, "is-live", true, NULL); + GstCaps *filtercaps1 = gst_caps_new_simple ("video/x-raw", + "format", G_TYPE_STRING, "RGBA", + "width", G_TYPE_INT, m_width, + "height", G_TYPE_INT, m_height, + "framerate", GST_TYPE_FRACTION, 4, 1, + NULL); + g_object_set (G_OBJECT (m_filter1), "caps", filtercaps1, NULL); + g_object_set (G_OBJECT (m_sink), "location", filename, NULL); + + g_assert( gst_element_link_many( + (GstElement*)m_src, + m_filter1, + m_videoconvert, + m_encoder, + m_queue, + m_webmmux, + m_sink, + NULL ) ); + + state_ret = gst_element_set_state((GstElement*)m_pipeline, GST_STATE_PLAYING); + g_assert(state_ret == GST_STATE_CHANGE_ASYNC); +} + +void Encoder::pushFrame(gpointer data, gsize size) +{ + GstBuffer *buffer = gst_buffer_new_wrapped(data, size); //Actual databuffer + GstFlowReturn ret; //Return value + //Set frame timestamp + GST_BUFFER_PTS (buffer) = m_timestamp; + GST_BUFFER_DTS (buffer) = m_timestamp; + GST_BUFFER_DURATION (buffer) = gst_util_uint64_scale_int ( GST_SECOND, 1,4); + m_timestamp += GST_BUFFER_DURATION (buffer); + ret = gst_app_src_push_buffer( m_src, buffer); //Push data into pipeline + + g_assert(ret == GST_FLOW_OK); + qDebug() << "push frame"; +} + +void Encoder::finish() +{ + qDebug() << "finishe called"; + //Declare end of stream + gst_app_src_end_of_stream (GST_APP_SRC (m_src)); + // Wait for EOS message + GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(m_pipeline)); + gst_bus_poll(bus, GST_MESSAGE_EOS, GST_CLOCK_TIME_NONE); + + qDebug() << "finished"; +} \ No newline at end of file diff --git a/plugins/dockers/recorder/krita_recorderdocker.json b/plugins/dockers/recorder/krita_recorderdocker.json new file mode 100644 --- /dev/null +++ b/plugins/dockers/recorder/krita_recorderdocker.json @@ -0,0 +1,9 @@ +{ + "Id": "Recorder Docker", + "Type": "Service", + "X-KDE-Library": "kritarecorderdocker", + "X-KDE-ServiceTypes": [ + "Krita/Dock" + ], + "X-Krita-Version": "28" +} diff --git a/plugins/dockers/recorder/recorderdocker.h b/plugins/dockers/recorder/recorderdocker.h new file mode 100644 --- /dev/null +++ b/plugins/dockers/recorder/recorderdocker.h @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2009 Cyrille Berger + * + * This library is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; version 2.1 of the License. + * + * This library 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef _RECORDER_DOCKER_H_ +#define _RECORDER_DOCKER_H_ + +#include +#include + +class KisViewManager; + +/** + * Template of view plugin + */ +class RecorderDockerPlugin : public QObject +{ + Q_OBJECT + public: + RecorderDockerPlugin(QObject *parent, const QVariantList &); + ~RecorderDockerPlugin() override; + private: + KisViewManager* m_view; +}; + +#endif diff --git a/plugins/dockers/recorder/recorderdocker.cpp b/plugins/dockers/recorder/recorderdocker.cpp new file mode 100644 --- /dev/null +++ b/plugins/dockers/recorder/recorderdocker.cpp @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2009 Cyrille Berger + * + * This library is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; version 2.1 of the License. + * + * This library 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "recorderdocker.h" + +#include + +#include + + +#include +#include +#include + +#include + +#include "kis_config.h" +#include "kis_cursor.h" +#include "kis_global.h" +#include "kis_types.h" +#include "KisViewManager.h" + +#include "recorderdocker_dock.h" +#include + +K_PLUGIN_FACTORY_WITH_JSON(RecorderDockerPluginFactory, "krita_recorderdocker.json", registerPlugin();) + +class RecorderDockerDockFactory : public KoDockFactoryBase { +public: + RecorderDockerDockFactory() + { + } + + QString id() const override + { + return QString( "RecorderDocker" ); + } + + virtual Qt::DockWidgetArea defaultDockWidgetArea() const + { + return Qt::RightDockWidgetArea; + } + + QDockWidget* createDockWidget() override + { + RecorderDockerDock * dockWidget = new RecorderDockerDock(); + dockWidget->setObjectName(id()); + + return dockWidget; + } + + DockPosition defaultDockPosition() const override + { + return DockMinimized; + } +private: + + +}; + + +RecorderDockerPlugin::RecorderDockerPlugin(QObject *parent, const QVariantList &) + : QObject(parent) +{ + KoDockRegistry::instance()->add(new RecorderDockerDockFactory()); +} + +RecorderDockerPlugin::~RecorderDockerPlugin() +{ + m_view = 0; +} + +#include "recorderdocker.moc" diff --git a/plugins/dockers/recorder/recorderdocker_dock.h b/plugins/dockers/recorder/recorderdocker_dock.h new file mode 100644 --- /dev/null +++ b/plugins/dockers/recorder/recorderdocker_dock.h @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2009 Cyrille Berger + * + * This library is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; version 2.1 of the License. + * + * This library 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef _RECORDER_DOCK_H_ +#define _RECORDER_DOCK_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "kis_idle_watcher.h" +#include "encoder.h" + +class QVBoxLayout; +class RecorderWidget; + +class RecorderDockerDock : public QDockWidget, public KoCanvasObserverBase { + Q_OBJECT +public: + RecorderDockerDock(); + QString observerName() override { return "RecorderDockerDock"; } + void setCanvas(KoCanvasBase *canvas) override; + void unsetCanvas() override; + +private: + QGridLayout *m_layout; + QPointer m_recordingCanvas; + QString m_recordPath; + + QPointer m_canvas; + QLabel *m_recordDirectoryLabel; + QLineEdit *m_recordDirectoryLineEdit; + QPushButton *m_recordDirectoryPushButton; + QLabel *m_imageNameLabel; + QLineEdit *m_imageNameLineEdit; + QPushButton *m_recordToggleButton; + QSpacerItem *m_spacer; + QLabel *m_logLabel; + QLineEdit *m_logLineEdit; + KisIdleWatcher m_imageIdleWatcher; + QMutex m_saveMutex; + QMutex m_eventMutex; + Encoder *m_encoder; + + bool m_recordEnabled; + int m_recordCounter; + void enableRecord(bool &enabled, const QString &path); + +private Q_SLOTS: + void onRecordButtonToggled(bool enabled); + void onSelectRecordFolderButtonClicked(); + void startUpdateCanvasProjection(); + void generateThumbnail(); +}; + + +#endif diff --git a/plugins/dockers/recorder/recorderdocker_dock.cpp b/plugins/dockers/recorder/recorderdocker_dock.cpp new file mode 100644 --- /dev/null +++ b/plugins/dockers/recorder/recorderdocker_dock.cpp @@ -0,0 +1,251 @@ +/* + * Copyright (c) 2009 Cyrille Berger + * + * This library is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; version 2.1 of the License. + * + * This library 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "recorderdocker_dock.h" + +#include +#include +#include +#include + +#include "kis_canvas2.h" +#include +#include +#include "kis_image.h" +#include "kis_paint_device.h" +#include "kis_signal_compressor.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "encoder.h" + +RecorderDockerDock::RecorderDockerDock( ) + : QDockWidget(i18n("Recorder")) + , m_canvas(0) + , m_imageIdleWatcher(1000) + , m_recordEnabled(false) + , m_recordCounter(0) + , m_encoder(nullptr) +{ + QWidget *page = new QWidget(this); + m_layout = new QGridLayout(page); + m_recordDirectoryLabel = new QLabel(this); + m_recordDirectoryLabel->setText("Directory:"); + + m_layout->addWidget(m_recordDirectoryLabel, 0, 0,1,2); + + m_recordDirectoryLineEdit = new QLineEdit(this); + m_recordDirectoryLineEdit->setText(QDir::homePath()); + m_recordDirectoryLineEdit->setReadOnly(true); + m_layout->addWidget(m_recordDirectoryLineEdit, 1, 0); + + m_recordDirectoryPushButton = new QPushButton(this); + m_recordDirectoryPushButton->setIcon(KisIconUtils::loadIcon("folder")); + m_recordDirectoryPushButton->setToolTip(i18n("Record Video")); + + m_layout->addWidget(m_recordDirectoryPushButton, 1, 1); + + m_imageNameLabel = new QLabel(this); + m_imageNameLabel->setText("Video Name:"); + + m_layout->addWidget(m_imageNameLabel, 2,0,1,2); + + m_imageNameLineEdit = new QLineEdit(this); + m_imageNameLineEdit->setText("image"); + + QRegExp rx("[0-9a-zA-z_]+"); + QValidator *validator = new QRegExpValidator(rx, this); + m_imageNameLineEdit->setValidator(validator); + + m_layout->addWidget(m_imageNameLineEdit, 3, 0); + + m_recordToggleButton = new QPushButton(this); + m_recordToggleButton->setCheckable(true); + m_recordToggleButton->setIcon(KisIconUtils::loadIcon("media-record")); + m_recordToggleButton->setToolTip(i18n("Record Video")); + m_layout->addWidget(m_recordToggleButton, 3, 1); + + m_logLabel = new QLabel(this); + m_logLabel->setText("Recent Save:"); + m_layout->addWidget(m_logLabel, 4,0,1,2); + m_logLineEdit = new QLineEdit(this); + m_logLineEdit->setReadOnly(true); + m_layout->addWidget(m_logLineEdit, 5,0,1,2); + + m_spacer = new QSpacerItem(1, 1, QSizePolicy::Minimum,QSizePolicy::Expanding); + m_layout->addItem(m_spacer, 6, 0,1,2); + connect(m_recordDirectoryPushButton, SIGNAL(clicked()), this, SLOT(onSelectRecordFolderButtonClicked())); + connect(m_recordToggleButton, SIGNAL(toggled(bool)), this, SLOT(onRecordButtonToggled(bool))); + setWidget(page); +} + +void RecorderDockerDock::setCanvas(KoCanvasBase * canvas) +{ + if(m_canvas == canvas) + return; + + setEnabled(canvas != 0); + + if (m_canvas) { + m_canvas->disconnectCanvasObserver(this); + m_canvas->image()->disconnect(this); + } + + m_canvas = dynamic_cast(canvas); + + if (m_canvas) { + m_imageIdleWatcher.setTrackedImage(m_canvas->image()); + + connect(&m_imageIdleWatcher, &KisIdleWatcher::startedIdleMode, this, &RecorderDockerDock::generateThumbnail); + + connect(m_canvas->image(), SIGNAL(sigImageUpdated(QRect)),SLOT(startUpdateCanvasProjection())); + connect(m_canvas->image(), SIGNAL(sigSizeChanged(QPointF, QPointF)),SLOT(startUpdateCanvasProjection())); + } +} + +void RecorderDockerDock::startUpdateCanvasProjection() +{ + m_imageIdleWatcher.startCountdown(); +} + +void RecorderDockerDock::unsetCanvas() +{ + setEnabled(false); + m_canvas = 0; +} + +void RecorderDockerDock::onRecordButtonToggled(bool enabled) +{ + bool enabled2 = enabled; + enableRecord(enabled2, m_recordDirectoryLineEdit->text() % "/" % m_imageNameLineEdit->text()); + + if (enabled && !enabled2) + { + disconnect(m_recordToggleButton, SIGNAL(toggle(bool)), this, SLOT(onRecordButtonToggled(bool))); + m_recordToggleButton->setChecked(false); + + connect(m_recordToggleButton, SIGNAL(toggle(bool)), this, SLOT(onRecordButtonToggled(bool))); + } +} + +void RecorderDockerDock::onSelectRecordFolderButtonClicked() +{ + QFileDialog dialog(this); + dialog.setFileMode(QFileDialog::DirectoryOnly); + QString folder = dialog.getExistingDirectory(this, tr("Select Output Folder"), m_recordDirectoryLineEdit->text(), QFileDialog::ShowDirsOnly); + m_recordDirectoryLineEdit->setText(folder); +} + +void RecorderDockerDock::enableRecord(bool &enabled, const QString &path) +{ + m_recordEnabled = enabled; + if (m_recordEnabled) + { + m_recordPath = path; + + QUrl fileUrl(m_recordPath); + + QString filename = fileUrl.fileName(); + QString dirPath = fileUrl.adjusted(QUrl::RemoveFilename).path(); + + QDir dir(dirPath); + + if (!dir.exists()) + { + if (!dir.mkpath(dirPath)) + { + enabled = m_recordEnabled = false; + return; + } + } + + QFileInfoList images = dir.entryInfoList({filename % "_*.webm"}); + + QRegularExpression namePattern("^"%filename%"_([0-9]{7}).webm$"); + m_recordCounter = -1; + foreach(auto info, images) + { + QRegularExpressionMatch match = namePattern.match(info.fileName()); + if (match.hasMatch()) + { + QString count = match.captured(1); + int numCount = count.toInt(); + + if (m_recordCounter < numCount) + { + m_recordCounter = numCount; + } + } + } + + if (m_canvas) + { + m_recordingCanvas = m_canvas; + + QString finalFileName = QString(m_recordPath % "_%1.webm").arg(++m_recordCounter, 7, 10, QChar('0')); + m_encoder = new Encoder(); + m_encoder->init(finalFileName.toStdString().c_str(), m_canvas->image()->width(), m_canvas->image()->height()); + startUpdateCanvasProjection(); + } + else + { + enabled = m_recordEnabled = false; + return; + } + } + else + { + if (m_encoder) + { + m_encoder->finish(); + m_encoder = nullptr; + } + } +} + +void RecorderDockerDock::generateThumbnail() +{ + if (m_recordEnabled) + { + if (m_canvas && m_recordingCanvas == m_canvas) + { + disconnect(&m_imageIdleWatcher, &KisIdleWatcher::startedIdleMode, this, &RecorderDockerDock::generateThumbnail); + if (m_encoder) + { + KisImageSP image = m_canvas->image(); + KisPaintDeviceSP dev = image->projection(); + + gpointer data; + gsize size = image->width() * image->height() * dev->pixelSize(); + data = g_malloc(size); + dev->readBytes((quint8*)data, 0, 0, image->width(), image->height()); + m_encoder->pushFrame(data, size); + } + + connect(&m_imageIdleWatcher, &KisIdleWatcher::startedIdleMode, this, &RecorderDockerDock::generateThumbnail); + } + } +}