diff options
author | Mavridis Philippe <mavridisf@gmail.com> | 2023-11-25 21:03:10 +0200 |
---|---|---|
committer | Mavridis Philippe <mavridisf@gmail.com> | 2023-11-25 22:02:34 +0200 |
commit | 37550ddc6ab16b457c344ccb0aa0c0e2484a3991 (patch) | |
tree | 5fb7f8df0827b3adf87291f012998147e8c93b13 | |
parent | b69aea1fa0e6e920a0d08828ec0e2702dd22b7d0 (diff) | |
download | kaffeine-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.cpp | 161 | ||||
-rw-r--r-- | kaffeine/src/player-parts/libmpv-part/libmpv_part.h | 2 | ||||
-rw-r--r-- | kaffeine/src/player-parts/libmpv-part/libmpv_part.rc | 9 |
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"/> |