summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMavridis Philippe <mavridisf@gmail.com>2023-11-25 21:03:10 +0200
committerMavridis Philippe <mavridisf@gmail.com>2023-11-25 22:02:34 +0200
commit37550ddc6ab16b457c344ccb0aa0c0e2484a3991 (patch)
tree5fb7f8df0827b3adf87291f012998147e8c93b13
parentb69aea1fa0e6e920a0d08828ec0e2702dd22b7d0 (diff)
downloadkaffeine-37550ddc6ab16b457c344ccb0aa0c0e2484a3991.tar.gz
kaffeine-37550ddc6ab16b457c344ccb0aa0c0e2484a3991.zip
mpv: Add screenshot action
When activated, it gets from MPV the currently playing frame as raw image data held in memory, which is then used to construct a TQImage, which is passed via DCOP to a newly launched instance of KSnapshot so that the user can choose what to do next like they would do with an ordinary screenshot. If KSnapshot is not available, a save dialog is shown to choose a local or remote location for writing the image, in all the formats that KImageIO supports writing to. Signed-off-by: Mavridis Philippe <mavridisf@gmail.com>
-rw-r--r--kaffeine/src/player-parts/libmpv-part/libmpv_part.cpp161
-rw-r--r--kaffeine/src/player-parts/libmpv-part/libmpv_part.h2
-rw-r--r--kaffeine/src/player-parts/libmpv-part/libmpv_part.rc9
3 files changed, 172 insertions, 0 deletions
diff --git a/kaffeine/src/player-parts/libmpv-part/libmpv_part.cpp b/kaffeine/src/player-parts/libmpv-part/libmpv_part.cpp
index 44655c6..2c0c1f2 100644
--- a/kaffeine/src/player-parts/libmpv-part/libmpv_part.cpp
+++ b/kaffeine/src/player-parts/libmpv-part/libmpv_part.cpp
@@ -35,6 +35,7 @@
#include <tdeparts/genericfactory.h>
#include <tdeglobalsettings.h>
#include <tdeio/netaccess.h>
+#include <tdeapplication.h>
#include <kstandarddirs.h>
#include <tdemessagebox.h>
#include <tdefiledialog.h>
@@ -45,6 +46,9 @@
#include <tdetoolbar.h>
#include <tdeglobal.h>
#include <kiconloader.h>
+#include <dcopclient.h>
+#include <tdetempfile.h>
+#include <kimageio.h>
#include <kdebug.h>
// Kaffeine
@@ -180,6 +184,8 @@ void MpvPart::initActions()
new KWidgetAction(m_volume, i18n("Volume"), 0, 0, 0, actionCollection(), "player_volume");
/*** Stream recording toolbar ***/
+ new TDEAction(i18n("Take Screenshot"), "ksnapshot", Key_S, this, SLOT(slotShot()), actionCollection(), "record_shot");
+
m_recordAction = new TDEToggleAction(i18n("&Record stream"), "player_record", Key_R, this, SLOT(slotToggleRecording()), actionCollection(), "record_toggle");
new TDEAction(i18n("Set recording file"), "document-open", 0, this, SLOT(slotSetRecordingFile()), actionCollection(), "record_open");
m_recordFile = new TQLabel(0);
@@ -753,4 +759,159 @@ void MpvPart::hideContextMenu() {
}
}
+#define THROW_ERROR(msg, details) \
+ kdError() << "mpv: " << msg << " (" << details << ")" << endl; \
+ KMessageBox::detailedError(nullptr, i18n(msg), details); \
+
+#define THROW_ERROR_FREE(msg, details) \
+ THROW_ERROR(msg, details) \
+ mpv_free_node_contents(&result);
+
+void MpvPart::slotShot() {
+ mpv_node result;
+ int w, h, stride;
+ char *format;
+ mpv_byte_array *data;
+
+ // Get screenshot as raw data in memory
+ const char *args[] = {"screenshot-raw", nullptr};
+ if (!mpv_command_ret(m_mpv, args, &result) == MPV_ERROR_SUCCESS) {
+ THROW_ERROR("Internal error", "screenshot-raw command failed")
+ return;
+ }
+
+ if (result.format != MPV_FORMAT_NODE_MAP) {
+ THROW_ERROR("Internal error", "screenshot-raw command returned wrong data format")
+ return;
+ }
+
+ // Validate the result returned by `screenshot-raw`
+#define CHECK_ARG(index, key) \
+ if (qstrcmp(result.u.list->keys[index], key) != 0) { \
+ TQString error = TQString("screenshot-raw returned wrong argument at #%1: %2") \
+ .arg(index).arg(key); \
+ THROW_ERROR_FREE("Internal error", error) \
+ return; \
+ }
+
+CHECK_ARG(0, "w")
+w = result.u.list->values[0].u.int64;
+
+CHECK_ARG(1, "h")
+h = result.u.list->values[1].u.int64;
+
+CHECK_ARG(2, "stride")
+stride = result.u.list->values[2].u.int64;
+
+CHECK_ARG(3, "format")
+format = result.u.list->values[3].u.string;
+
+CHECK_ARG(4, "data")
+data = result.u.list->values[4].u.ba;
+
+#undef CHECK_ARG
+
+ // Validate image format (we can handle only bgr0, which is the default anyway)
+ kdDebug() << "mpv: screenshot-raw image format: " << format << endl;
+ if (qstrcmp(format, "bgr0") != 0) {
+ THROW_ERROR_FREE("Internal error", "mpv: screenshot-raw returned image format other than bgr0")
+ return;
+ }
+
+ // An empty image is useless but not a fatal error
+ if (w <= 0 || h <= 0) {
+ kdWarning() << "mpv: screenshot-raw returned empty image" << endl;
+ }
+
+ // Create a TQDataStream to easily read the raw image data
+ TQByteArray ba;
+ ba.setRawData(static_cast<char *>(data->data), data->size);
+ TQDataStream s(ba, IO_ReadOnly);
+ s.setByteOrder(TQDataStream::BigEndian);
+
+ // Create image from raw B8G8R8X8 data
+ TQImage img(w, h, 32);
+ for (uint y = 0; y < h; ++y) {
+ TQRgb *scanline = (TQRgb *)img.scanLine(y);
+ for (uint x = 0; x < w; x++) {
+ uchar r, g, b, z;
+ s >> b >> g >> r >> z;
+ scanline[x] = tqRgb(r, g, b);
+ }
+ }
+
+
+ // Try to launch ksnapshot
+ TQString ksError;
+ TQCString ksDcop;
+
+ int ksSuccess = kapp->startServiceByDesktopName("ksnapshot", TQString::null,
+ &ksError, &ksDcop);
+ if (ksSuccess == 0) {
+ // Serialize the image to pass via dcop
+ TQByteArray pix;
+ TQDataStream ds(pix, IO_WriteOnly);
+ ds << img;
+
+ // Send the serialized image to the newly initialized ksnapshot instance
+ if (!kapp->dcopClient()->send(ksDcop, "interface", "setPixmap(TQPixmap)", pix)) {
+ THROW_ERROR("Unable to launch KSnapshot!", i18n("You seem to be using an incompatible version of KSnapshot."));
+ return;
+ }
+ }
+ else {
+ TQStringList mimeTypes = KImageIO::mimeTypes(KImageIO::Writing);
+ if (mimeTypes.isEmpty()) {
+ THROW_ERROR("No image formats supported!", "KImageIO does not support any image format for export.");
+ return;
+ }
+
+ KFileDialog *saveDlg = new KFileDialog(TQString::null, TQString::null,
+ widget(), "screenshotSaver", false);
+ saveDlg->setOperationMode(KFileDialog::Saving);
+ saveDlg->setMimeFilter(mimeTypes);
+
+ if (!saveDlg->exec()) {
+ return;
+ }
+
+ KURL saveURL = saveDlg->selectedURL();
+ if (saveURL.isEmpty()) return;
+
+ TQString type = KImageIO::type(saveURL.url());
+ if (!KImageIO::canWrite(type)) {
+ THROW_ERROR("Cannot write image", "Format " + type + " is unsupported for export.")
+ return;
+ }
+
+ if (saveURL.isLocalFile()) {
+ if (!img.save(saveURL.path(), type.latin1())) {
+ THROW_ERROR("I/O Error", "Could not save file!")
+ return;
+ }
+ }
+ else {
+ KTempFile tempFile;
+ tempFile.setAutoDelete(true);
+ TQString tempFileName = tempFile.name();
+ if (tempFileName.isEmpty()) {
+ THROW_ERROR("I/O Error", "Could not create temporary file!")
+ return;
+ }
+
+ if (!img.save(tempFileName, type.latin1())) {
+ THROW_ERROR("I/O Error", "Could not write to temporary file!")
+ return;
+ }
+
+ if (!TDEIO::NetAccess::upload(tempFileName, saveURL, widget())) {
+ THROW_ERROR("Upload error", "Could not upload image.")
+ return;
+ }
+ }
+ }
+}
+#undef THROW_ERROR
+#undef THROW_ERROR_FREE
+
#include "libmpv_part.moc" \ No newline at end of file
diff --git a/kaffeine/src/player-parts/libmpv-part/libmpv_part.h b/kaffeine/src/player-parts/libmpv-part/libmpv_part.h
index 3350d78..8658778 100644
--- a/kaffeine/src/player-parts/libmpv-part/libmpv_part.h
+++ b/kaffeine/src/player-parts/libmpv-part/libmpv_part.h
@@ -102,6 +102,8 @@ class MpvPart : public KaffeinePart
void slotNext();
void slotSetPosition(uint); /* percent */
+ void slotShot();
+
void slotToggleRecording();
void startRecording();
void stopRecording();
diff --git a/kaffeine/src/player-parts/libmpv-part/libmpv_part.rc b/kaffeine/src/player-parts/libmpv-part/libmpv_part.rc
index 74f2893..48b2983 100644
--- a/kaffeine/src/player-parts/libmpv-part/libmpv_part.rc
+++ b/kaffeine/src/player-parts/libmpv-part/libmpv_part.rc
@@ -12,7 +12,9 @@
<Action name="player_previous"/>
<Action name="player_next"/>
<Separator/>
+ <Action name="record_shot"/>
<Action name="record_toggle"/>
+ <Action name="record_open"/>
</Menu>
</MenuBar>
@@ -34,6 +36,8 @@
<Separator/>
</ToolBar>
<ToolBar name="record" hidden="true"><text>Stream Recorder Toolbar</text>
+ <Action name="record_shot"/>
+ <Separator/>
<Action name="record_toggle"/>
<Action name="record_open"/>
<Action name="record_file"/>
@@ -72,6 +76,7 @@
<Action name="player_mute"/>
<Action name="player_volume"/>
+ <Action name="record_shot"/>
<Action name="record_toggle"/>
<Action name="record_open"/>
<Action name="record_file"/>
@@ -99,6 +104,7 @@
<Action name="player_mute"/>
<Action name="player_volume"/>
+ <Action name="record_shot"/>
<Action name="record_toggle"/>
<Action name="record_open"/>
<Action name="record_file"/>
@@ -124,6 +130,8 @@
<Action name="player_mute"/>
<Action name="player_volume"/>
+
+ <Action name="record_shot"/>
</enable>
</State>
@@ -143,6 +151,7 @@
<Action name="player_mute"/>
<Action name="player_volume"/>
+ <Action name="record_shot"/>
<Action name="record_toggle"/>
<Action name="record_open"/>
<Action name="record_file"/>