You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
kaffeine/kaffeine/src/player-parts/libmpv-part/libmpv_part.cpp

530 lines
17 KiB

/*
* Kaffeine libmpv part
* Copyright (C) 2023 Mavridis Philippe <mavridisf@gmail.com>
*
* Based on Kaffeine dummy part
* Copyright (C) 2004-2005 Jürgen Kofler <kaffeine@gmx.net>
*
* 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 2 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
// TQt
#include <tqtooltip.h>
#include <tqlabel.h>
#include <tqslider.h>
#include <tqpushbutton.h>
#include <tqfile.h>
// TDE
#include <tdeparts/genericfactory.h>
#include <tdeglobalsettings.h>
#include <tdeio/netaccess.h>
#include <tdemessagebox.h>
#include <tdefiledialog.h>
#include <tdeaction.h>
#include <kmimetype.h>
#include <tdemessagebox.h>
#include <kxmlguifactory.h>
#include <tdetoolbar.h>
#include <kdebug.h>
// Kaffeine
#include "playlistimport.h"
// Part
#include "libmpv_part.h"
#include "libmpv_event.h"
typedef KParts::GenericFactory<MpvPart> MpvPartFactory;
K_EXPORT_COMPONENT_FACTORY (libmpvpart, MpvPartFactory);
MpvPart::MpvPart(TQWidget* parentWidget, const char* widgetName, TQObject* parent, const char* name, const TQStringList& /*args*/)
: KaffeinePart(parent, name ? name : "MpvPart"),
m_current(0),
m_seeking(false),
m_recordFilePath()
{
// Create an instance of this class
setInstance(MpvPartFactory::instance());
// Create container widget
m_player = new TQWidget(parentWidget);
m_player->setBackgroundColor(TQt::black);
m_player->setFocusPolicy(TQ_ClickFocus);
setWidget(m_player);
// Initialize and embed mpv
if (!initMpv()) {
kdError() << "libmpvpart: initialization of mpv failed!" << endl;
emit canceled(i18n("MPV initialization failed!"));
return;
}
// Initialize GUI
setXMLFile("libmpv_part.rc");
initActions();
emit setStatusBarText(i18n("Ready"));
}
MpvPart::~MpvPart()
{
if (m_mpv) {
mpv_terminate_destroy(m_mpv);
m_mpv = nullptr;
}
if (m_eventThread) {
// the event thread doesn't do I/O, so we just kill it
m_eventThread->terminate();
}
}
TDEAboutData *MpvPart::createAboutData() {
TDEAboutData* aboutData = new TDEAboutData(
"libmpvpart", I18N_NOOP("MpvPart"),
"0.1", "A Kaffeine player part based on libmpv",
TDEAboutData::License_GPL,
"(c) 2023 Trinity Desktop Project", 0);
aboutData->addAuthor("Mavridis Philippe", I18N_NOOP("Developer"), "mavridisf@gmail.com");
aboutData->addAuthor("Jürgen Kofler", I18N_NOOP("Original Kaffeine developer"), "kaffeine@gmx.net");
return aboutData;
}
bool MpvPart::initMpv() {
// Create mpv instance
m_mpv = mpv_create();
if (!m_mpv) {
return false;
}
// Embed mpv into container widget
int64_t wid = static_cast<int64_t>(m_player->winId());
mpv_set_option(m_mpv, "wid", MPV_FORMAT_INT64, &wid);
// Get log messages with minimal level "info"
mpv_request_log_messages(m_mpv, "info");
// Start mpv event thread
m_eventThread = new MpvEventThread(this);
m_eventThread->start(TQThread::LowPriority);
return (mpv_initialize(m_mpv) >= 0);
}
void MpvPart::initActions()
{
/** Playback controls ***/
new TDEAction(i18n("Play"), "media-playback-start", 0, this, SLOT(slotPlay()), actionCollection(), "player_play");
new TDEAction(i18n("Pause"), "media-playback-pause", Key_Space, this, SLOT(slotTogglePause()), actionCollection(), "player_pause");
new TDEAction(i18n("Stop"), "media-playback-stop", Key_Backspace, this, SLOT(slotStop()), actionCollection(), "player_stop");
new TDEAction(i18n("&Previous"), "media-skip-backward", Key_PageUp, this, SLOT(slotPrevious()), actionCollection(), "player_previous");
new TDEAction(i18n("&Next"), "media-skip-forward", Key_PageDown, this, SLOT(slotNext()), actionCollection(), "player_next");
/*** Position toolbar ***/
// Important: we have a max of 1000 instead of 100 for better precision; multiply/divide your percentages by 10
m_position = new TQSlider(0, 1000, 10, 0, TQt::Horizontal, 0);
TQToolTip::add(m_position, i18n("Position"));
m_position->setTracking(false);
m_position->setFocusPolicy(TQ_NoFocus);
m_position->setMinimumWidth(100);
connect(m_position, SIGNAL(sliderPressed()), this, SLOT(slotStartSeeking()));
connect(m_position, SIGNAL(sliderMoved(int)), this, SLOT(slotSetSeekingPos(int)));
connect(m_position, SIGNAL(sliderReleased()), this, SLOT(slotStopSeeking()));
KWidgetAction *posAction = new KWidgetAction(m_position, i18n("Position"), 0, 0, 0, actionCollection(), "player_position");
posAction->setAutoSized(true);
m_playtime = new TQPushButton(0);
TQFontMetrics met(TDEGlobalSettings::generalFont());
m_playtime->setFixedWidth(met.width("-88:88:88") + 6);
m_playtime->setSizePolicy(TQSizePolicy (TQSizePolicy::Fixed, TQSizePolicy::Fixed));
m_playtime->setFocusPolicy(TQ_NoFocus);
new KWidgetAction(m_playtime, i18n("Playtime"), 0, 0, 0, actionCollection(), "player_playtime");
/*** Volume toolbar ***/
new TDEAction(i18n("&Mute"), "player_mute", Key_U, this, SLOT(slotMute()), actionCollection(), "player_mute");
m_volume = new TQSlider(0, 100, 10, 100, TQt::Horizontal, 0);
connect(m_volume, SIGNAL(sliderMoved(int)), this, SLOT(slotSetVolume(int)));
new KWidgetAction(m_volume, i18n("Volume"), 0, 0, 0, actionCollection(), "player_volume");
/*** Stream recording toolbar ***/
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);
new KWidgetAction(m_recordFile, i18n("Recording file"), 0, 0, 0, actionCollection(), "record_file");
updateRecordFileLabel();
resetTime();
stateChanged("disable_all");
}
void MpvPart::showError(TQString text, TQString caption) {
KMessageBox::sorry(0, text, caption);
}
void MpvPart::showDetailedError(TQString text, TQString details, TQString caption) {
KMessageBox::detailedSorry(0, text, details, caption);
}
// Custom events dispatched from mpv event thread are handled here
void MpvPart::customEvent(TQCustomEvent *event) {
if (event->type() == MPVPART_EVENT_PROPERTY_CHANGE) {
MpvPropertyChangeEvent *pe = (MpvPropertyChangeEvent *)event;
if (pe->property() == "time-pos" && pe->format() == MPV_FORMAT_DOUBLE) {
m_time = pe->toTime();
m_playtime->setText(m_time.toString());
}
else if (pe->property() == "percent-pos" && pe->format() == MPV_FORMAT_DOUBLE) {
if (!m_seeking) {
m_percent = pe->toDouble();
m_position->setValue(m_percent * 10);
}
}
else if (pe->property() == "duration" && pe->format() == MPV_FORMAT_DOUBLE) {
MRL mrl = m_playlist[m_current];
TQTime length = TQTime().addMSecs(pe->toDouble() * 1000);
if (!length.isNull()) {
mrl.setLength(length);
emit signalNewMeta(m_mrl);
}
}
}
else if (event->type() == MPVPART_EVENT_EOF) {
resetTime();
MpvEOFEvent *eofe = (MpvEOFEvent *)event;
if (eofe->reason() == MPV_END_FILE_REASON_ERROR) {
KMessageBox::detailedError(nullptr, i18n("Cannot play file."), eofe->error());
}
}
else if (event->type() == MPVPART_EVENT_ERROR) {
slotPause(true);
MpvErrorEvent *ee = (MpvErrorEvent *)event;
if (ee->details().isEmpty()) {
showError(ee->text(), ee->caption());
}
else {
showDetailedError(ee->text(), ee->details(), ee->caption());
}
}
}
bool MpvPart::openURL(const MRL& mrl) {
/* Note: we do not use the internal playlist feature of mpv.
* Instead we rely on the playlist feature of Kaffeine.
*/
if (!m_mpv) return false;
m_mrl = mrl;
m_playlist.clear();
m_current = 0;
/* Playlist handling code taken from gstreamer_part.cpp */
bool playlist = false;
TQString ext = m_mrl.kurl().fileName();
ext = ext.remove( 0 , ext.findRev('.')+1 ).lower();
if ( m_mrl.mime().isNull() ) {
KMimeType::Ptr mime = KMimeType::findByURL( m_mrl.kurl().path() );
m_mrl.setMime( mime->name() );
}
if ( (m_mrl.mime() == "text/plain") || (m_mrl.mime() == "text/xml") || (m_mrl.mime() == "application/x-kaffeine")
|| (m_mrl.mime() == "audio/x-scpls") || (m_mrl.mime() == "audio/x-mpegurl") || (m_mrl.mime() == "audio/mpegurl")
|| (ext == "asx") || (ext == "asf") || (ext == "wvx") || (ext == "wax") ) /* windows meta files */
{
kdDebug() << "libmpvpart: Check for kaffeine/noatun/m3u/pls/asx playlist\n";
TQString localFile;
if ( TDEIO::NetAccess::download(m_mrl.kurl(), localFile, widget()) ) {
TQFile file( localFile );
file.open( IO_ReadOnly );
TQTextStream stream( &file );
TQString firstLine = stream.readLine();
TQString secondLine = stream.readLine();
file.close();
if ( secondLine.contains("kaffeine", false) ) {
kdDebug() << "libmpvpart: Try loading kaffeine playlist\n";
playlist = PlaylistImport::kaffeine( localFile, m_playlist );
}
if ( secondLine.contains("noatun", false) ) {
kdDebug() << "libmpvpart: Try loading noatun playlist\n";
playlist = PlaylistImport::noatun( localFile, m_playlist);
}
if ( firstLine.contains("asx", false) ) {
kdDebug() << "libmpvpart: Try loading asx playlist\n";
playlist = PlaylistImport::asx( localFile, m_playlist );
}
if ( firstLine.contains("[playlist]", false) ) {
kdDebug() << "libmpvpart: Try loading pls playlist\n";
playlist = PlaylistImport::pls( localFile, m_playlist );
}
if (ext == "m3u" || ext == "m3u8") { //indentify by extension
kdDebug() << "libmpvpart: Try loading m3u playlist\n";
playlist = PlaylistImport::m3u( localFile, m_playlist );
}
}
else
kdError() << "libmpvpart: " << TDEIO::NetAccess::lastErrorString() << endl;
}
// check for ram playlist
if ( (ext == "ra") || (ext == "rm") || (ext == "ram") || (ext == "lsc") || (ext == "pl") ) {
kdDebug() << "libmpvpart: Try loading ram playlist\n";
playlist = PlaylistImport::ram( m_mrl, m_playlist, widget() );
}
if (!playlist)
{
kdDebug() << "libmpvpart: Got single track" << endl;
m_playlist.append( m_mrl );
}
slotPlay();
return true;
}
bool MpvPart::closeURL() {
if (!m_mpv) return false;
const char *args[] = {"playlist-remove", "current", nullptr};
mpv_command_async(m_mpv, 0, args);
return true;
}
void MpvPart::slotPlay() {
if (!m_mpv) return;
if (isPaused()) {
int value = 0;
mpv_set_property(m_mpv, "pause", MPV_FORMAT_FLAG, &value);
return;
}
if (m_playlist.count() > 0) {
emit setStatusBarText( i18n("Opening...") );
MRL curMRL = m_playlist[m_current];
const char *args[] = {"loadfile", curMRL.url().local8Bit(), nullptr};
mpv_command_async(m_mpv, 0, args);
// Add subtitles
if ((!curMRL.subtitleFiles().isEmpty()) && (curMRL.currentSubtitle() > -1)) {
kdDebug() << "adding subs" << endl;
const char *args[] = {"sub-add", curMRL.subtitleFiles()[curMRL.currentSubtitle()].local8Bit(), nullptr};
mpv_command_async(m_mpv, 0, args);
}
}
else {
emit signalRequestCurrentTrack();
}
stateChanged(isStream() ? "playing_stream" : "playing_file");
}
void MpvPart::slotPause(bool pause) {
int value = pause ? 1 : 0;
mpv_set_property(m_mpv, "pause", MPV_FORMAT_FLAG, &value);
stateChanged(pause ? "paused" : (isStream() ? "playing_stream" : "playing_file"));
}
void MpvPart::slotTogglePause() {
if (!m_mpv) return;
slotPause(!isPaused());
}
bool MpvPart::isPlaying()
{
if (!m_mpv) return false;
int result;
if (mpv_get_property(m_mpv, "core-idle", MPV_FORMAT_FLAG, &result) < 0) {
return false;
}
return !(bool)result;
}
bool MpvPart::isPaused() {
if (!m_mpv) return false;
int result;
if (mpv_get_property(m_mpv, "pause", MPV_FORMAT_FLAG, &result) < 0) {
return false;
}
return (bool)result;
}
bool MpvPart::isMute() {
if (!m_mpv) return false;
int result;
if (mpv_get_property(m_mpv, "ao-mute", MPV_FORMAT_FLAG, &result) < 0) {
return false;
}
return (bool)result;
}
bool MpvPart::isStream() {
TQString proto = m_mrl.kurl().protocol();
if (proto.startsWith("http") || proto.startsWith("rtsp")) {
return true;
}
return false;
}
void MpvPart::resetTime() {
m_playtime->setText("0:00:00");
m_position->setValue(0);
}
void MpvPart::slotStartSeeking() {
m_seeking = true;
slotPause(true);
}
void MpvPart::slotStopSeeking() {
slotSetPosition(m_percent);
slotPause(false);
m_seeking = false;
}
void MpvPart::slotSetSeekingPos(int pos) {
m_percent = (uint)pos / 10;
}
uint MpvPart::volume() const {
int64_t value = 0;
mpv_get_property(m_mpv, "ao-volume", MPV_FORMAT_INT64, &value);
return value;
}
uint MpvPart::position() const {
return m_percent;
}
void MpvPart::slotSetVolume(uint volume) {
if (!m_mpv) return;
int64_t value = (int64_t)volume;
mpv_set_property(m_mpv, "ao-volume", MPV_FORMAT_INT64, &value);
}
void MpvPart::slotSetPosition(uint position) {
if (!m_mpv) return;
int64_t value = (int64_t)position;
mpv_set_property(m_mpv, "percent-pos", MPV_FORMAT_INT64, &value);
}
void MpvPart::slotPrevious() {
if ( m_current > 0 ) {
m_current--;
slotPlay();
}
else {
emit signalRequestPreviousTrack();
}
}
void MpvPart::slotNext() {
if ((m_playlist.count() > 0) && (m_current < m_playlist.count()-1)) {
m_current++;
slotPlay();
}
else {
emit signalRequestNextTrack();
}
}
void MpvPart::slotStop() {
if (isPlaying()) {
const char *args[] = {"stop", nullptr};
mpv_command_async(m_mpv, 0, args);
stateChanged("not_playing");
}
}
void MpvPart::slotMute() {
int value = isMute() ? 0 : 1;
mpv_set_property(m_mpv, "ao-mute", MPV_FORMAT_FLAG, &value);
}
void MpvPart::slotToggleRecording() {
if (m_recordAction->isChecked()) {
startRecording();
}
else {
stopRecording();
}
}
void MpvPart::startRecording() {
// Ensure we have an out file and start recording
if (m_recordFilePath.isEmpty()) {
slotSetRecordingFile();
}
if (m_recordFilePath.isEmpty()) {
m_recordAction->setChecked(false);
return;
}
mpv_set_property_string(m_mpv, "stream-record", m_recordFilePath.local8Bit());
m_recordAction->setChecked(true);
}
void MpvPart::stopRecording() {
mpv_set_property_string(m_mpv, "stream-record", "");
m_recordAction->setChecked(false);
}
void MpvPart::slotSetRecordingFile() {
if (m_recordAction->isChecked() && !m_recordFilePath.isEmpty()) {
if (!KMessageBox::warningContinueCancel(0,
i18n("Changing the recording file will stop the recording process."),
i18n("Recording in progress")) == KMessageBox::Continue)
{
return;
}
}
m_recordFilePath = KFileDialog::getSaveFileName(
TQString::null,
"*.mkv|" + i18n("Matroska file (*.mkv)") + "\n" +
"*.ogg|" + i18n("OGG media (*.ogg)") + "\n" +
"*.mp4|" + i18n("MPEG-4 video (*.mp4)") + "\n" +
"*.mpeg|" + i18n("MPEG video (*.mpeg)") + "\n" +
"*.avi|" + i18n("Microsoft AVI video (*.avi)") + "\n" +
"*.mp3|" + i18n("MPEG Layer 3 audio (*.mp3)") + "\n" +
"*.mp2|" + i18n("MPEG Layer 2 audio (*.mp2)") + "\n" +
"*.flac|" + i18n("FLAC audio (*.flac)") + "\n",
0, i18n("Select file name for saved recording"));
updateRecordFileLabel();
}
void MpvPart::updateRecordFileLabel() {
m_recordFile->setText(
m_recordFilePath.isEmpty()
? i18n("No output file")
: m_recordFilePath
);
}
#include "libmpv_part.moc"