/* Copyright (C) 2007-2008 Tanguy Krotoff Copyright (C) 2008 Lukas Durfina Copyright (C) 2009 Fathi Boudra Copyright (C) 2010 Ben Cooksley Copyright (C) 2009-2011 vlc-phonon AUTHORS Copyright (C) 2010-2021 Harald Sitter 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; either version 2.1 of the License, or (at your option) any later version. 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 library. If not, see . */ #include "mediaobject.h" #include #include #include #include #include #include #include "utils/debug.h" #include "utils/libvlc.h" #include "media.h" #include "sinknode.h" #include "streamreader.h" //Time in milliseconds before sending aboutToFinish() signal //2 seconds static const int ABOUT_TO_FINISH_TIME = 2000; namespace Phonon { namespace VLC { MediaObject::MediaObject(QObject *parent) : QObject(parent) , m_nextSource(MediaSource(QUrl())) , m_streamReader(0) , m_state(Phonon::StoppedState) , m_tickInterval(0) , m_transitionTime(0) , m_media(0) { qRegisterMetaType >("QMultiMap"); m_player = new MediaPlayer(this); Q_ASSERT(m_player); if (!m_player->libvlc_media_player()) error() << "libVLC:" << LibVLC::errorMessage(); // Player signals. connect(m_player, SIGNAL(seekableChanged(bool)), this, SIGNAL(seekableChanged(bool))); connect(m_player, SIGNAL(timeChanged(qint64)), this, SLOT(timeChanged(qint64))); connect(m_player, SIGNAL(stateChanged(MediaPlayer::State)), this, SLOT(updateState(MediaPlayer::State))); connect(m_player, SIGNAL(hasVideoChanged(bool)), this, SLOT(onHasVideoChanged(bool))); connect(m_player, SIGNAL(bufferChanged(int)), this, SLOT(setBufferStatus(int))); connect(m_player, SIGNAL(timeChanged(qint64)), this, SLOT(timeChanged(qint64))); // Internal Signals. connect(this, SIGNAL(moveToNext()), SLOT(moveToNextSource())); connect(m_refreshTimer, SIGNAL(timeout()), this, SLOT(refreshDescriptors())); resetMembers(); } MediaObject::~MediaObject() { unloadMedia(); // Shutdown the pulseaudio mainloop before the MediaPlayer gets destroyed // (it is a child of the MO). There appears to be a peculiar race condition // between the pa_thread_mainloop used by VLC and the pa_glib_mainloop used // by Phonon's PulseSupport where for a very short time frame after the // former was stopped and freed the latter can run and fall over // Invalid read from eventfd: Bad file descriptor // Code should not be reached at pulsecore/fdsem.c:157, function flush(). Aborting. // Since we don't use PulseSupport since VLC 2.2 we can simply force a // loop shutdown even when the application isn't about to terminate. // The instance gets created again anyway. PulseSupport::shutdown(); } void MediaObject::resetMembers() { // default to -1, so that streams won't break and to comply with the docs (-1 if unknown) m_totalTime = -1; m_hasVideo = false; m_seekpoint = 0; m_prefinishEmitted = false; m_aboutToFinishEmitted = false; m_lastTick = 0; m_timesVideoChecked = 0; m_buffering = false; m_stateAfterBuffering = ErrorState; resetMediaController(); // Forcefully shutdown plusesupport to prevent crashing between the PS PA glib mainloop // and the VLC PA threaded mainloop. See destructor. PulseSupport::shutdown(); } void MediaObject::play() { DEBUG_BLOCK; switch (m_state) { case PlayingState: // Do not do anything if we are already playing (as per documentation). return; case PausedState: m_player->resume(); break; default: setupMedia(); if (m_player->play()) error() << "libVLC:" << LibVLC::errorMessage(); break; } } void MediaObject::pause() { DEBUG_BLOCK; switch (m_state) { case BufferingState: case PlayingState: m_player->pause(); break; case PausedState: return; default: debug() << "doing paused play"; setupMedia(); m_player->pausedPlay(); break; } } void MediaObject::stop() { DEBUG_BLOCK; if (m_streamReader) m_streamReader->unlock(); m_nextSource = MediaSource(QUrl()); m_player->stop(); } void MediaObject::seek(qint64 milliseconds) { DEBUG_BLOCK; switch (m_state) { case PlayingState: case PausedState: case BufferingState: break; default: // Seeking while not being in a playingish state is cached for later. m_seekpoint = milliseconds; return; } debug() << "seeking" << milliseconds << "msec"; m_player->setTime(milliseconds); const qint64 time = currentTime(); const qint64 total = totalTime(); // Reset last tick marker so we emit time even after seeking if (time < m_lastTick) m_lastTick = time; if (time < total - m_prefinishMark) m_prefinishEmitted = false; if (time < total - ABOUT_TO_FINISH_TIME) m_aboutToFinishEmitted = false; } void MediaObject::timeChanged(qint64 time) { const qint64 totalTime = m_totalTime; switch (m_state) { case PlayingState: case BufferingState: case PausedState: emitTick(time); default: break; } if (m_state == PlayingState || m_state == BufferingState) { // Buffering is concurrent if (time >= totalTime - m_prefinishMark) { if (!m_prefinishEmitted) { m_prefinishEmitted = true; emit prefinishMarkReached(totalTime - time); } } // Note that when the totalTime is <= 0 we cannot calculate any sane delta. if (totalTime > 0 && time >= totalTime - ABOUT_TO_FINISH_TIME) emitAboutToFinish(); } } void MediaObject::emitTick(qint64 time) { if (m_tickInterval == 0) // Make sure we do not ever emit ticks when deactivated.\] return; if (time + m_tickInterval >= m_lastTick) { m_lastTick = time; emit tick(time); } } void MediaObject::loadMedia(const QByteArray &mrl) { DEBUG_BLOCK; // Initial state is loading, from which we quickly progress to stopped because // libvlc does not provide feedback on loading and the media does not get loaded // until we play it. // FIXME: libvlc should really allow for this as it can cause unexpected delay // even though the GUI might indicate that playback should start right away. changeState(Phonon::LoadingState); m_mrl = mrl; debug() << "loading encoded:" << m_mrl; // We do not have a loading state generally speaking, usually the backend // is expected to go to loading state and then at some point reach stopped, // at which point playback can be started. // See state enum documentation for more information. changeState(Phonon::StoppedState); } void MediaObject::loadMedia(const QString &mrl) { loadMedia(mrl.toUtf8()); } qint32 MediaObject::tickInterval() const { return m_tickInterval; } /** * Supports runtime changes. * If the user goes to tick(0) we stop the timer, otherwise we fire it up. */ void MediaObject::setTickInterval(qint32 interval) { m_tickInterval = interval; } qint64 MediaObject::currentTime() const { qint64 time = -1; switch (state()) { case Phonon::PausedState: case Phonon::BufferingState: case Phonon::PlayingState: time = m_player->time(); break; case Phonon::StoppedState: case Phonon::LoadingState: time = 0; break; case Phonon::ErrorState: time = -1; break; } return time; } Phonon::State MediaObject::state() const { return m_state; } Phonon::ErrorType MediaObject::errorType() const { return Phonon::NormalError; } MediaSource MediaObject::source() const { return m_mediaSource; } void MediaObject::setSource(const MediaSource &source) { DEBUG_BLOCK; // Reset previous streamereaders if (m_streamReader) { m_streamReader->unlock(); delete m_streamReader; m_streamReader = 0; // For streamreaders we exchange the player's seekability with the // reader's so here we change it back. // Note: the reader auto-disconnects due to destruction. connect(m_player, SIGNAL(seekableChanged(bool)), this, SIGNAL(seekableChanged(bool))); } // Reset previous isScreen flag m_isScreen = false; m_mediaSource = source; QByteArray url; switch (source.type()) { case MediaSource::Invalid: error() << Q_FUNC_INFO << "MediaSource Type is Invalid:" << source.type(); break; case MediaSource::Empty: error() << Q_FUNC_INFO << "MediaSource is empty."; break; case MediaSource::LocalFile: case MediaSource::Url: debug() << "MediaSource::Url:" << source.url(); if (source.url().scheme().isEmpty()) { url = "file://"; // QUrl considers url.scheme.isEmpty() == url.isRelative(), // so to be sure the url is not actually absolute we just // check the first character if (!source.url().toString().startsWith('/')) url.append(QFile::encodeName(QDir::currentPath()) + '/'); } url += source.url().toEncoded(); loadMedia(url); break; case MediaSource::Disc: switch (source.discType()) { case Phonon::NoDisc: error() << Q_FUNC_INFO << "the MediaSource::Disc doesn't specify which one (Phonon::NoDisc)"; return; case Phonon::Cd: loadMedia(QStringLiteral("cdda://") % m_mediaSource.deviceName()); break; case Phonon::Dvd: loadMedia(QStringLiteral("dvd://") % m_mediaSource.deviceName()); break; case Phonon::Vcd: loadMedia(QStringLiteral("vcd://") % m_mediaSource.deviceName()); break; case Phonon::BluRay: loadMedia(QStringLiteral("bluray://") % m_mediaSource.deviceName()); break; } break; case MediaSource::CaptureDevice: { QByteArray driverName; QString deviceName; if (source.deviceAccessList().isEmpty()) { error() << Q_FUNC_INFO << "No device access list for this capture device"; break; } // TODO try every device in the access list until it works, not just the first one driverName = source.deviceAccessList().first().first; deviceName = source.deviceAccessList().first().second; if (driverName == QByteArray("v4l2")) { loadMedia(QStringLiteral("v4l2://") % deviceName); } else if (driverName == QByteArray("alsa")) { /* * Replace "default" and "plughw" and "x-phonon" with "hw" for capture device names, because * VLC does not want to open them when using default instead of hw. * plughw also does not work. * * TODO investigate what happens */ if (deviceName.startsWith(QLatin1String("default"))) { deviceName.replace(0, 7, "hw"); } if (deviceName.startsWith(QLatin1String("plughw"))) { deviceName.replace(0, 6, "hw"); } if (deviceName.startsWith(QLatin1String("x-phonon"))) { deviceName.replace(0, 8, "hw"); } loadMedia(QStringLiteral("alsa://") % deviceName); } else if (driverName == "screen") { loadMedia(QStringLiteral("screen://") % deviceName); // Set the isScreen flag needed to add extra options in playInternal m_isScreen = true; } else { error() << Q_FUNC_INFO << "Unsupported MediaSource::CaptureDevice:" << driverName; break; } break; } case MediaSource::Stream: m_streamReader = new StreamReader(this); // LibVLC refuses to emit seekability as it does a try-and-seek approach // to work around this we exchange the player's seekability signal // for the readers // https://bugs.kde.org/show_bug.cgi?id=293012 connect(m_streamReader, SIGNAL(streamSeekableChanged(bool)), this, SIGNAL(seekableChanged(bool))); disconnect(m_player, SIGNAL(seekableChanged(bool)), this, SIGNAL(seekableChanged(bool))); // Only connect now to avoid seekability detection before we are connected. m_streamReader->connectToSource(source); loadMedia(QByteArray("imem://")); break; } debug() << "Sending currentSourceChanged"; emit currentSourceChanged(m_mediaSource); } void MediaObject::setNextSource(const MediaSource &source) { DEBUG_BLOCK; debug() << source.url(); m_nextSource = source; // This function is not ever called by the consumer but only libphonon. // Furthermore libphonon only calls this function in its aboutToFinish slot, // iff sources are already in the queue. In case our aboutToFinish was too // late we may already be stopped when the slot gets activated. // Therefore we need to make sure that we move to the next source iff // this function is called when we are in stoppedstate. if (m_state == StoppedState) moveToNext(); } qint32 MediaObject::prefinishMark() const { return m_prefinishMark; } void MediaObject::setPrefinishMark(qint32 msecToEnd) { m_prefinishMark = msecToEnd; if (currentTime() < totalTime() - m_prefinishMark) { // Not about to finish m_prefinishEmitted = false; } } qint32 MediaObject::transitionTime() const { return m_transitionTime; } void MediaObject::setTransitionTime(qint32 time) { m_transitionTime = time; } void MediaObject::emitAboutToFinish() { if (!m_aboutToFinishEmitted) { // Track is about to finish m_aboutToFinishEmitted = true; emit aboutToFinish(); } } // State changes are force queued by libphonon. void MediaObject::changeState(Phonon::State newState) { DEBUG_BLOCK; // State not changed if (newState == m_state) return; debug() << m_state << "-->" << newState; #ifdef __GNUC__ #warning do we actually need m_seekpoint? if a consumer seeks before playing state that is their problem?! #endif // Workaround that seeking needs to work before the file is being played... // We store seeks and apply them when going to seek (or discard them on reset). if (newState == PlayingState) { if (m_seekpoint != 0) { seek(m_seekpoint); m_seekpoint = 0; } } // State changed Phonon::State previousState = m_state; m_state = newState; emit stateChanged(m_state, previousState); } void MediaObject::moveToNextSource() { DEBUG_BLOCK; setSource(m_nextSource); // The consumer may set an invalid source as final source to force a // queued stop, regardless of how fast the consumer is at actually calling // stop. Such a source must not cause an actual move (moving ~= state // changes towards playing) but instead we only set the source to reflect // that we got the setNextSource call. if (hasNextTrack()) play(); m_nextSource = MediaSource(QUrl()); } inline bool MediaObject::hasNextTrack() { return m_nextSource.type() != MediaSource::Invalid && m_nextSource.type() != MediaSource::Empty; } inline void MediaObject::unloadMedia() { if (m_media) { m_media->disconnect(this); m_media->deleteLater(); m_media = 0; } } void MediaObject::setupMedia() { DEBUG_BLOCK; unloadMedia(); resetMembers(); // Create a media with the given MRL m_media = new Media(m_mrl, this); if (m_isScreen) { m_media->addOption(QLatin1String("screen-fps=24.0")); m_media->addOption(QLatin1String("screen-caching=300")); } if (source().discType() == Cd && m_currentTitle > 0) m_media->setCdTrack(m_currentTitle); if (m_streamReader) // StreamReader is no sink but a source, for this we have no concept right now // also we do not need one since the reader is the only source we have. // Consequently we need to manually tell the StreamReader to attach to the Media. m_streamReader->addToMedia(m_media); if (!m_subtitleAutodetect) m_media->addOption(QLatin1String(":no-sub-autodetect-file")); if (m_subtitleEncoding != QLatin1String("UTF-8")) // utf8 is phonon default, so let vlc handle it m_media->addOption(QLatin1String(":subsdec-encoding="), m_subtitleEncoding); if (!m_subtitleFontChanged) // Update font settings m_subtitleFont = QFont(); #ifdef __GNUC__ #warning freetype module is not working as expected - font api not working #endif // BUG: VLC's freetype module doesn't pick up per-media options // vlc -vvvv --freetype-font="Comic Sans MS" multiple_sub_sample.mkv :freetype-font=Arial // https://trac.videolan.org/vlc/ticket/9797 m_media->addOption(QLatin1String(":freetype-font="), m_subtitleFont.family()); m_media->addOption(QLatin1String(":freetype-fontsize="), m_subtitleFont.pointSize()); if (m_subtitleFont.bold()) m_media->addOption(QLatin1String(":freetype-bold")); else m_media->addOption(QLatin1String(":no-freetype-bold")); foreach (SinkNode *sink, m_sinks) { sink->addToMedia(m_media); } // Connect to Media signals. Disconnection is done at unloading. connect(m_media, SIGNAL(durationChanged(qint64)), this, SLOT(updateDuration(qint64))); connect(m_media, SIGNAL(metaDataChanged()), this, SLOT(updateMetaData())); // Update available audio channels/subtitles/angles/chapters/etc... // i.e everything from MediaController // There is no audio channel/subtitle/angle/chapter events inside libvlc // so let's send our own events... // This will reset the GUI resetMediaController(); // Play m_player->setMedia(m_media); } QString MediaObject::errorString() const { return libvlc_errmsg(); } bool MediaObject::hasVideo() const { // Cached: sometimes 4.0.0-dev sends the vout event but then // has_vout is still false. Guard against this by simply always reporting // the last hasVideoChanged value. If that is off we can still drop into // libvlc in case it changed meanwhile. return m_hasVideo || m_player->hasVideoOutput(); } bool MediaObject::isSeekable() const { if (m_streamReader) return m_streamReader->streamSeekable(); return m_player->isSeekable(); } void MediaObject::updateDuration(qint64 newDuration) { // This here cache is needed because we need to provide -1 as totalTime() // for as long as we do not get a proper update through this slot. // VLC reports -1 with no media but 0 if it does not know the duration, so // apps that assume 0 = unknown get screwed if they query too early. // http://bugs.tomahawk-player.org/browse/TWK-1029 m_totalTime = newDuration; emit totalTimeChanged(m_totalTime); } void MediaObject::updateMetaData() { QMultiMap metaDataMap; const QString artist = m_media->meta(libvlc_meta_Artist); const QString title = m_media->meta(libvlc_meta_Title); const QString nowPlaying = m_media->meta(libvlc_meta_NowPlaying); // Streams sometimes have the artist and title munged in nowplaying. // With ALBUM = Title and TITLE = NowPlaying it will still show up nicely in Amarok. if (artist.isEmpty() && !nowPlaying.isEmpty()) { metaDataMap.insert(QLatin1String("ALBUM"), title); metaDataMap.insert(QLatin1String("TITLE"), nowPlaying); } else { metaDataMap.insert(QLatin1String("ALBUM"), m_media->meta(libvlc_meta_Album)); metaDataMap.insert(QLatin1String("TITLE"), title); } metaDataMap.insert(QLatin1String("ARTIST"), artist); metaDataMap.insert(QLatin1String("DATE"), m_media->meta(libvlc_meta_Date)); metaDataMap.insert(QLatin1String("GENRE"), m_media->meta(libvlc_meta_Genre)); metaDataMap.insert(QLatin1String("TRACKNUMBER"), m_media->meta(libvlc_meta_TrackNumber)); metaDataMap.insert(QLatin1String("DESCRIPTION"), m_media->meta(libvlc_meta_Description)); metaDataMap.insert(QLatin1String("COPYRIGHT"), m_media->meta(libvlc_meta_Copyright)); metaDataMap.insert(QLatin1String("URL"), m_media->meta(libvlc_meta_URL)); metaDataMap.insert(QLatin1String("ENCODEDBY"), m_media->meta(libvlc_meta_EncodedBy)); if (metaDataMap == m_vlcMetaData) { // No need to issue any change, the data is the same return; } m_vlcMetaData = metaDataMap; emit metaDataChanged(metaDataMap); } void MediaObject::updateState(MediaPlayer::State state) { DEBUG_BLOCK; debug() << state; debug() << "attempted autoplay?" << m_attemptingAutoplay; if (m_attemptingAutoplay) { switch (state) { case MediaPlayer::PlayingState: case MediaPlayer::PausedState: m_attemptingAutoplay = false; break; case MediaPlayer::ErrorState: debug() << "autoplay failed, must be end of media."; // The error should not be reflected to the consumer. So we swap it // for finished() which is actually what is happening here. // Or so we think ;) state = MediaPlayer::EndedState; --m_currentTitle; break; default: debug() << "not handling as part of autplay:" << state; break; } } switch (state) { case MediaPlayer::NoState: changeState(LoadingState); break; case MediaPlayer::OpeningState: changeState(LoadingState); break; case MediaPlayer::BufferingState: changeState(BufferingState); break; case MediaPlayer::PlayingState: changeState(PlayingState); break; case MediaPlayer::PausedState: changeState(PausedState); break; case MediaPlayer::StoppedState: changeState(StoppedState); break; case MediaPlayer::EndedState: if (hasNextTrack()) { moveToNextSource(); } else if (source().discType() == Cd && m_autoPlayTitles && !m_attemptingAutoplay) { debug() << "trying to simulate autoplay"; m_attemptingAutoplay = true; m_player->setCdTrack(++m_currentTitle); } else { m_attemptingAutoplay = false; emitAboutToFinish(); emit finished(); changeState(StoppedState); } break; case MediaPlayer::ErrorState: debug() << errorString(); emitAboutToFinish(); emit finished(); changeState(ErrorState); break; } if (m_buffering) { switch (state) { case MediaPlayer::BufferingState: break; case MediaPlayer::PlayingState: debug() << "Restoring buffering state after state change to Playing"; changeState(BufferingState); m_stateAfterBuffering = PlayingState; break; case MediaPlayer::PausedState: debug() << "Restoring buffering state after state change to Paused"; changeState(BufferingState); m_stateAfterBuffering = PausedState; break; default: debug() << "Buffering aborted!"; m_buffering = false; break; } } return; } void MediaObject::onHasVideoChanged(bool hasVideo) { DEBUG_BLOCK; if (m_hasVideo != hasVideo) { m_hasVideo = hasVideo; emit hasVideoChanged(m_hasVideo); } else { // We can simply return if we are have the appropriate caching already. // Otherwise we'd do pointless rescans of mediacontroller stuff. // MC and MO are force-reset on media changes anyway. return; } refreshDescriptors(); } void MediaObject::setBufferStatus(int percent) { // VLC does not have a buffering state (surprise!) but instead only sends the // event (surprise!). Hence we need to simulate the state change. // Problem with BufferingState is that it is actually concurrent to Playing or Paused // meaning that while you are buffering you can also pause, thus triggering // a state change to paused. To handle this we let updateState change the // state accordingly (as we need to allow the UI to update itself, and // immediately after that we change back to buffering again. // This loop can only be interrupted by a state change to !Playing & !Paused // or by reaching a 100 % buffer caching (i.e. full cache). m_buffering = true; if (m_state != BufferingState) { m_stateAfterBuffering = m_state; changeState(BufferingState); } emit bufferStatus(percent); // Transit to actual state only after emission so the signal is still // delivered while in BufferingState. if (percent >= 100) { // http://trac.videolan.org/vlc/ticket/5277 m_buffering = false; changeState(m_stateAfterBuffering); } } void MediaObject::refreshDescriptors() { if (m_player->titleCount() > 0) refreshTitles(); if (hasVideo()) { refreshAudioChannels(); refreshSubtitles(); if (m_player->videoChapterCount() > 0) refreshChapters(m_player->title()); } } qint64 MediaObject::totalTime() const { return m_totalTime; } void MediaObject::addSink(SinkNode *node) { Q_ASSERT(!m_sinks.contains(node)); m_sinks.append(node); } void MediaObject::removeSink(SinkNode *node) { Q_ASSERT(node); m_sinks.removeAll(node); } } // namespace VLC } // namespace Phonon