diff --git a/README.md b/README.md index a182ce02..5925a155 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ This code has been run and tested on Windows XP/Vista/7, Ubuntu Linux, Mac OS X. qtdeclarative5-dev libqtwebkit-dev libxtst-dev liblzo2-dev libbz2-dev \ libao-dev libavutil-dev libavformat-dev libtiff5-dev libeb16-dev \ libqt5webkit5-dev libqt5svg5-dev libqt5x11extras5-dev qttools5-dev \ - qttools5-dev-tools + qttools5-dev-tools qtmultimedia5-dev libqt5multimedia5-plugins ## How to build @@ -82,16 +82,21 @@ If you have problem building with libeb-dev package, you can pass qmake "CONFIG+=no_epwing_support" -### Building without internal audio player +### Building without internal audio players If you have problem building with FFmpeg/libao (for example, Ubuntu older than 12.04), you can pass -`"DISABLE_INTERNAL_PLAYER=1"` to `qmake` in order to disable internal audio player completely: +`"CONFIG+=no_ffmpeg_player"` to `qmake` in order to disable FFmpeg+libao internal audio player back end: - qmake "DISABLE_INTERNAL_PLAYER=1" + qmake "CONFIG+=no_ffmpeg_player" + +If you have problem building with Qt5 Multimedia or experience GStreamer run-time errors (for example, Ubuntu 14.04), you can pass +`"CONFIG+=no_qtmultimedia_player"` to `qmake` in order to disable Qt Multimedia internal audio player back end: + + qmake "CONFIG+=no_qtmultimedia_player" NB: All additional settings for `qmake` that you need must be combined in one `qmake` launch, for example: - qmake "CONFIG+=zim_support" "CONFIG+=no_extra_tiff_handler" "DISABLE_INTERNAL_PLAYER=1" + qmake "CONFIG+=zim_support" "CONFIG+=no_extra_tiff_handler" "CONFIG+=no_ffmpeg_player" Then, invoke `make clean` before `make` because the setting change: diff --git a/audioplayerfactory.cc b/audioplayerfactory.cc index 68184b29..6f0fd0b1 100644 --- a/audioplayerfactory.cc +++ b/audioplayerfactory.cc @@ -5,11 +5,13 @@ #include #include "audioplayerfactory.hh" #include "ffmpegaudioplayer.hh" +#include "multimediaaudioplayer.hh" #include "externalaudioplayer.hh" #include "gddebug.hh" AudioPlayerFactory::AudioPlayerFactory( Config::Preferences const & p ) : useInternalPlayer( p.useInternalPlayer ), + internalPlayerBackend( p.internalPlayerBackend ), audioPlaybackProgram( p.audioPlaybackProgram ) { reset(); @@ -20,10 +22,17 @@ void AudioPlayerFactory::setPreferences( Config::Preferences const & p ) if( p.useInternalPlayer != useInternalPlayer ) { useInternalPlayer = p.useInternalPlayer; + internalPlayerBackend = p.internalPlayerBackend; audioPlaybackProgram = p.audioPlaybackProgram; reset(); } else + if( useInternalPlayer && p.internalPlayerBackend != internalPlayerBackend ) + { + internalPlayerBackend = p.internalPlayerBackend; + reset(); + } + else if( !useInternalPlayer && p.audioPlaybackProgram != audioPlaybackProgram ) { audioPlaybackProgram = p.audioPlaybackProgram; @@ -38,16 +47,35 @@ void AudioPlayerFactory::setPreferences( Config::Preferences const & p ) void AudioPlayerFactory::reset() { -#ifndef DISABLE_INTERNAL_PLAYER if( useInternalPlayer ) - playerPtr.reset( new Ffmpeg::AudioPlayer ); - else -#endif { - QScopedPointer< ExternalAudioPlayer > externalPlayer( new ExternalAudioPlayer ); - setAudioPlaybackProgram( *externalPlayer ); - playerPtr.reset( externalPlayer.take() ); + // qobject_cast checks below account for the case when an unsupported backend + // is stored in config. After this backend is replaced with the default one + // upon preferences saving, the code below does not reset playerPtr with + // another object of the same type. + +#ifdef MAKE_FFMPEG_PLAYER + Q_ASSERT( Config::InternalPlayerBackend::defaultBackend().isFfmpeg() + && "Adjust the code below after changing the default backend." ); + + if( !internalPlayerBackend.isQtmultimedia() ) + { + if( qobject_cast< Ffmpeg::AudioPlayer * >( playerPtr.data() ) == 0 ) + playerPtr.reset( new Ffmpeg::AudioPlayer ); + return; + } +#endif + +#ifdef MAKE_QTMULTIMEDIA_PLAYER + if( qobject_cast< MultimediaAudioPlayer * >( playerPtr.data() ) == 0 ) + playerPtr.reset( new MultimediaAudioPlayer ); + return; +#endif } + + QScopedPointer< ExternalAudioPlayer > externalPlayer( new ExternalAudioPlayer ); + setAudioPlaybackProgram( *externalPlayer ); + playerPtr.reset( externalPlayer.take() ); } void AudioPlayerFactory::setAudioPlaybackProgram( ExternalAudioPlayer & externalPlayer ) diff --git a/audioplayerfactory.hh b/audioplayerfactory.hh index 09fdf43a..3775a81e 100644 --- a/audioplayerfactory.hh +++ b/audioplayerfactory.hh @@ -25,6 +25,7 @@ private: void setAudioPlaybackProgram( ExternalAudioPlayer & externalPlayer ); bool useInternalPlayer; + Config::InternalPlayerBackend internalPlayerBackend; QString audioPlaybackProgram; AudioPlayerPtr playerPtr; }; diff --git a/config.cc b/config.cc index 04f8dc19..9053478c 100644 --- a/config.cc +++ b/config.cc @@ -78,6 +78,56 @@ QKeySequence HotKey::toKeySequence() const return QKeySequence( key1 | modifiers, v2 ); } +bool InternalPlayerBackend::anyAvailable() +{ +#if defined( MAKE_FFMPEG_PLAYER ) || defined( MAKE_QTMULTIMEDIA_PLAYER ) + return true; +#else + return false; +#endif +} + +InternalPlayerBackend InternalPlayerBackend::defaultBackend() +{ +#if defined( MAKE_FFMPEG_PLAYER ) + return ffmpeg(); +#elif defined( MAKE_QTMULTIMEDIA_PLAYER ) + return qtmultimedia(); +#else + return InternalPlayerBackend( QString() ); +#endif +} + +QStringList InternalPlayerBackend::nameList() +{ + QStringList result; +#ifdef MAKE_FFMPEG_PLAYER + result.push_back( ffmpeg().uiName() ); +#endif +#ifdef MAKE_QTMULTIMEDIA_PLAYER + result.push_back( qtmultimedia().uiName() ); +#endif + return result; +} + +bool InternalPlayerBackend::isFfmpeg() const +{ +#ifdef MAKE_FFMPEG_PLAYER + return *this == ffmpeg(); +#else + return false; +#endif +} + +bool InternalPlayerBackend::isQtmultimedia() const +{ +#ifdef MAKE_QTMULTIMEDIA_PLAYER + return *this == qtmultimedia(); +#else + return false; +#endif +} + Preferences::Preferences(): newTabsOpenAfterCurrentOne( false ), newTabsOpenInBackground( true ), @@ -114,11 +164,8 @@ Preferences::Preferences(): #endif pronounceOnLoadMain( false ), pronounceOnLoadPopup( false ), -#ifndef DISABLE_INTERNAL_PLAYER - useInternalPlayer( true ), -#else - useInternalPlayer( false ), -#endif + useInternalPlayer( InternalPlayerBackend::anyAvailable() ), + internalPlayerBackend( InternalPlayerBackend::defaultBackend() ), checkForNewReleases( true ), disallowContentFromOtherSites( false ), enableWebPlugins( false ), @@ -769,12 +816,16 @@ Class load() throw( exError ) c.preferences.pronounceOnLoadMain = ( preferences.namedItem( "pronounceOnLoadMain" ).toElement().text() == "1" ); c.preferences.pronounceOnLoadPopup = ( preferences.namedItem( "pronounceOnLoadPopup" ).toElement().text() == "1" ); -#ifndef DISABLE_INTERNAL_PLAYER - if ( !preferences.namedItem( "useInternalPlayer" ).isNull() ) - c.preferences.useInternalPlayer = ( preferences.namedItem( "useInternalPlayer" ).toElement().text() == "1" ); -#else - c.preferences.useInternalPlayer = false; -#endif + if ( InternalPlayerBackend::anyAvailable() ) + { + if ( !preferences.namedItem( "useInternalPlayer" ).isNull() ) + c.preferences.useInternalPlayer = ( preferences.namedItem( "useInternalPlayer" ).toElement().text() == "1" ); + } + else + c.preferences.useInternalPlayer = false; + + if ( !preferences.namedItem( "internalPlayerBackend" ).isNull() ) + c.preferences.internalPlayerBackend.setUiName( preferences.namedItem( "internalPlayerBackend" ).toElement().text() ); if ( !preferences.namedItem( "audioPlaybackProgram" ).isNull() ) c.preferences.audioPlaybackProgram = preferences.namedItem( "audioPlaybackProgram" ).toElement().text(); @@ -1660,6 +1711,10 @@ void save( Class const & c ) throw( exError ) opt.appendChild( dd.createTextNode( c.preferences.useInternalPlayer ? "1" : "0" ) ); preferences.appendChild( opt ); + opt = dd.createElement( "internalPlayerBackend" ); + opt.appendChild( dd.createTextNode( c.preferences.internalPlayerBackend.uiName() ) ); + preferences.appendChild( opt ); + opt = dd.createElement( "audioPlaybackProgram" ); opt.appendChild( dd.createTextNode( c.preferences.audioPlaybackProgram ) ); preferences.appendChild( opt ); diff --git a/config.hh b/config.hh index 435bc9e9..eff158ce 100644 --- a/config.hh +++ b/config.hh @@ -177,6 +177,52 @@ struct FullTextSearch {} }; +/// This class encapsulates supported backend preprocessor logic, +/// discourages duplicating backend names in code, which is error-prone. +class InternalPlayerBackend +{ +public: + /// Returns true if at least one backend is available. + static bool anyAvailable(); + /// Returns the default backend or null backend if none is available. + static InternalPlayerBackend defaultBackend(); + /// Returns the name list of supported backends. + static QStringList nameList(); + + /// Returns true if built with FFmpeg player support and the name matches. + bool isFfmpeg() const; + /// Returns true if built with Qt Multimedia player support and the name matches. + bool isQtmultimedia() const; + + QString const & uiName() const + { return name; } + + void setUiName( QString const & name_ ) + { name = name_; } + + bool operator == ( InternalPlayerBackend const & other ) const + { return name == other.name; } + + bool operator != ( InternalPlayerBackend const & other ) const + { return ! operator == ( other ); } + +private: +#ifdef MAKE_FFMPEG_PLAYER + static InternalPlayerBackend ffmpeg() + { return InternalPlayerBackend( "FFmpeg+libao" ); } +#endif + +#ifdef MAKE_QTMULTIMEDIA_PLAYER + static InternalPlayerBackend qtmultimedia() + { return InternalPlayerBackend( "Qt Multimedia" ); } +#endif + + explicit InternalPlayerBackend( QString const & name_ ) : name( name_ ) + {} + + QString name; +}; + /// Various user preferences struct Preferences { @@ -222,8 +268,9 @@ struct Preferences // Whether the word should be pronounced on page load, in main window/popup bool pronounceOnLoadMain, pronounceOnLoadPopup; - QString audioPlaybackProgram; bool useInternalPlayer; + InternalPlayerBackend internalPlayerBackend; + QString audioPlaybackProgram; ProxyServer proxyServer; diff --git a/ffmpegaudio.cc b/ffmpegaudio.cc index bc37822f..ed1172bd 100644 --- a/ffmpegaudio.cc +++ b/ffmpegaudio.cc @@ -1,4 +1,4 @@ -#ifndef DISABLE_INTERNAL_PLAYER +#ifdef MAKE_FFMPEG_PLAYER #include "ffmpegaudio.hh" @@ -589,4 +589,4 @@ void DecoderThread::cancel( bool waitUntilFinished ) } -#endif //DISABLE_INTERNAL_PLAYER +#endif // MAKE_FFMPEG_PLAYER diff --git a/ffmpegaudio.hh b/ffmpegaudio.hh index 2dbda7bb..c1425345 100644 --- a/ffmpegaudio.hh +++ b/ffmpegaudio.hh @@ -1,7 +1,7 @@ #ifndef __FFMPEGAUDIO_HH_INCLUDED__ #define __FFMPEGAUDIO_HH_INCLUDED__ -#ifndef DISABLE_INTERNAL_PLAYER +#ifdef MAKE_FFMPEG_PLAYER #include #include @@ -52,6 +52,6 @@ signals: } -#endif // DISABLE_INTERNAL_PLAYER +#endif // MAKE_FFMPEG_PLAYER #endif // __FFMPEGAUDIO_HH_INCLUDED__ diff --git a/ffmpegaudioplayer.hh b/ffmpegaudioplayer.hh index bfce56e6..49ddbf8b 100644 --- a/ffmpegaudioplayer.hh +++ b/ffmpegaudioplayer.hh @@ -7,7 +7,7 @@ #include "audioplayerinterface.hh" #include "ffmpegaudio.hh" -#ifndef DISABLE_INTERNAL_PLAYER +#ifdef MAKE_FFMPEG_PLAYER namespace Ffmpeg { @@ -36,6 +36,6 @@ public: } -#endif // DISABLE_INTERNAL_PLAYER +#endif // MAKE_FFMPEG_PLAYER #endif // FFMPEGAUDIOPLAYER_HH_INCLUDED diff --git a/goldendict.pro b/goldendict.pro index fa1505b2..788a5e97 100644 --- a/goldendict.pro +++ b/goldendict.pro @@ -28,11 +28,21 @@ greaterThan(QT_MAJOR_VERSION, 4) { webkitwidgets \ printsupport \ help + + # QMediaPlayer is not available in Qt4. + !CONFIG( no_qtmultimedia_player ) { + QT += multimedia + DEFINES += MAKE_QTMULTIMEDIA_PLAYER + } } else { QT += webkit CONFIG += help } +!CONFIG( no_ffmpeg_player ) { + DEFINES += MAKE_FFMPEG_PLAYER +} + QT += sql CONFIG += exceptions \ rtti \ @@ -46,8 +56,6 @@ LIBS += \ -lbz2 \ -llzo2 -!isEmpty(DISABLE_INTERNAL_PLAYER): DEFINES += DISABLE_INTERNAL_PLAYER - win32 { TARGET = GoldenDict @@ -97,7 +105,7 @@ win32 { LIBS += -lvorbisfile \ -lvorbis \ -logg - isEmpty(DISABLE_INTERNAL_PLAYER) { + !CONFIG( no_ffmpeg_player ) { LIBS += -lao \ -lavutil-gd \ -lavformat-gd \ @@ -143,7 +151,7 @@ unix:!mac { vorbis \ ogg \ hunspell - isEmpty(DISABLE_INTERNAL_PLAYER) { + !CONFIG( no_ffmpeg_player ) { PKGCONFIG += ao \ libavutil \ libavformat \ @@ -193,7 +201,7 @@ mac { -logg \ -lhunspell-1.6.1 \ -llzo2 - isEmpty(DISABLE_INTERNAL_PLAYER) { + !CONFIG( no_ffmpeg_player ) { LIBS += -lao \ -lavutil-gd \ -lavformat-gd \ @@ -267,6 +275,7 @@ HEADERS += folding.hh \ audioplayerinterface.hh \ audioplayerfactory.hh \ ffmpegaudioplayer.hh \ + multimediaaudioplayer.hh \ externalaudioplayer.hh \ externalviewer.hh \ wordfinder.hh \ @@ -399,6 +408,7 @@ SOURCES += folding.cc \ scanpopup.cc \ articleview.cc \ audioplayerfactory.cc \ + multimediaaudioplayer.cc \ externalaudioplayer.cc \ externalviewer.cc \ wordfinder.cc \ diff --git a/multimediaaudioplayer.cc b/multimediaaudioplayer.cc new file mode 100644 index 00000000..f4f10990 --- /dev/null +++ b/multimediaaudioplayer.cc @@ -0,0 +1,43 @@ +/* This file is (c) 2018 Igor Kushnir + * Part of GoldenDict. Licensed under GPLv3 or later, see the LICENSE file */ + +#ifdef MAKE_QTMULTIMEDIA_PLAYER + +#include +#include +#include "multimediaaudioplayer.hh" + +MultimediaAudioPlayer::MultimediaAudioPlayer() : + player( 0, QMediaPlayer::StreamPlayback ) +{ + typedef void( QMediaPlayer::* ErrorSignal )( QMediaPlayer::Error ); + connect( &player, static_cast< ErrorSignal >( &QMediaPlayer::error ), + this, &MultimediaAudioPlayer::onMediaPlayerError ); +} + +QString MultimediaAudioPlayer::play( const char * data, int size ) +{ + stop(); + + audioBuffer.setData( data, size ); + if( !audioBuffer.open( QIODevice::ReadOnly ) ) + return tr( "Couldn't open audio buffer for reading." ); + + player.setMedia( QMediaContent(), &audioBuffer ); + player.play(); + return QString(); +} + +void MultimediaAudioPlayer::stop() +{ + player.setMedia( QMediaContent() ); // Forget about audioBuffer. + audioBuffer.close(); + audioBuffer.setData( QByteArray() ); // Free memory. +} + +void MultimediaAudioPlayer::onMediaPlayerError() +{ + emit error( player.errorString() ); +} + +#endif // MAKE_QTMULTIMEDIA_PLAYER diff --git a/multimediaaudioplayer.hh b/multimediaaudioplayer.hh new file mode 100644 index 00000000..873af412 --- /dev/null +++ b/multimediaaudioplayer.hh @@ -0,0 +1,32 @@ +/* This file is (c) 2018 Igor Kushnir + * Part of GoldenDict. Licensed under GPLv3 or later, see the LICENSE file */ + +#ifndef MULTIMEDIAAUDIOPLAYER_HH_INCLUDED +#define MULTIMEDIAAUDIOPLAYER_HH_INCLUDED + +#ifdef MAKE_QTMULTIMEDIA_PLAYER + +#include +#include +#include "audioplayerinterface.hh" + +class MultimediaAudioPlayer : public AudioPlayerInterface +{ + Q_OBJECT +public: + MultimediaAudioPlayer(); + + virtual QString play( const char * data, int size ); + virtual void stop(); + +private slots: + void onMediaPlayerError(); + +private: + QBuffer audioBuffer; + QMediaPlayer player; ///< Depends on audioBuffer. +}; + +#endif // MAKE_QTMULTIMEDIA_PLAYER + +#endif // MULTIMEDIAAUDIOPLAYER_HH_INCLUDED diff --git a/preferences.cc b/preferences.cc index e4f53ea0..ee82614c 100644 --- a/preferences.cc +++ b/preferences.cc @@ -243,14 +243,28 @@ Preferences::Preferences( QWidget * parent, Config::Class & cfg_ ): ui.pronounceOnLoadMain->setChecked( p.pronounceOnLoadMain ); ui.pronounceOnLoadPopup->setChecked( p.pronounceOnLoadPopup ); -#ifdef DISABLE_INTERNAL_PLAYER - ui.useInternalPlayer->hide(); -#else - if ( p.useInternalPlayer ) - ui.useInternalPlayer->setChecked( true ); + ui.internalPlayerBackend->addItems( Config::InternalPlayerBackend::nameList() ); + + // Make sure that exactly one radio button in the group is checked and that + // on_useExternalPlayer_toggled() is called. + ui.useExternalPlayer->setChecked( true ); + + if( ui.internalPlayerBackend->count() > 0 ) + { + // Checking ui.useInternalPlayer automatically unchecks ui.useExternalPlayer. + ui.useInternalPlayer->setChecked( p.useInternalPlayer ); + + int index = ui.internalPlayerBackend->findText( p.internalPlayerBackend.uiName() ); + if( index < 0 ) // The specified backend is unavailable. + index = ui.internalPlayerBackend->findText( Config::InternalPlayerBackend::defaultBackend().uiName() ); + Q_ASSERT( index >= 0 && "Logic error: the default backend must be present in the backend name list." ); + ui.internalPlayerBackend->setCurrentIndex( index ); + } else -#endif - ui.useExternalPlayer->setChecked( true ); + { + ui.useInternalPlayer->hide(); + ui.internalPlayerBackend->hide(); + } ui.audioPlaybackProgram->setText( p.audioPlaybackProgram ); @@ -400,6 +414,7 @@ Config::Preferences Preferences::getPreferences() p.pronounceOnLoadMain = ui.pronounceOnLoadMain->isChecked(); p.pronounceOnLoadPopup = ui.pronounceOnLoadPopup->isChecked(); p.useInternalPlayer = ui.useInternalPlayer->isChecked(); + p.internalPlayerBackend.setUiName( ui.internalPlayerBackend->currentText() ); p.audioPlaybackProgram = ui.audioPlaybackProgram->text(); p.proxyServer.enabled = ui.useProxyServer->isChecked(); @@ -592,6 +607,7 @@ void Preferences::on_buttonBox_accepted() void Preferences::on_useExternalPlayer_toggled( bool enabled ) { + ui.internalPlayerBackend->setEnabled( !enabled ); ui.audioPlaybackProgram->setEnabled( enabled ); } diff --git a/preferences.ui b/preferences.ui index 2e1ecee6..3e47d6fe 100644 --- a/preferences.ui +++ b/preferences.ui @@ -902,37 +902,43 @@ p, li { white-space: pre-wrap; } Playback - - + + - Play audio files via FFmpeg(libav) and libao + Play audio files via built-in audio support - Use internal player + Use internal player: - - - - - - Use any external program to play audio files - - - Use external program: - - - - - - - false - - - - + + + + Choose audio back end + + + + + + + Use any external program to play audio files + + + Use external program: + + + + + + + false + + + Enter audio player command line + +