/**************************************************************************** ** ** Copyright (C) 2012 Research In Motion ** Contact: http://www.qt-project.org/legal ** ** This file is part of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and Digia. For licensing terms and ** conditions see http://qt.digia.com/licensing. For further information ** use the contact form at http://qt.digia.com/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 2.1 requirements ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** In addition, as a special exception, Digia gives you certain additional ** rights. These rights are described in the Digia Qt LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3.0 as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU General Public License version 3.0 requirements will be ** met: http://www.gnu.org/copyleft/gpl.html. ** ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "bbmediaplayercontrol.h" #include "bbmetadatareadercontrol.h" #include "bbvideowindowcontrol.h" #include "bbutil.h" #include #include #include #include #include #include #include #include #include #include #include QT_BEGIN_NAMESPACE static int idCounter = 0; BbMediaPlayerControl::BbMediaPlayerControl(QObject *parent) : QMediaPlayerControl(parent), m_connection(0), m_context(0), m_audioId(-1), m_state(QMediaPlayer::StoppedState), m_volume(100), m_muted(false), m_rate(1), m_id(-1), m_eventMonitor(0), m_position(0), m_mediaStatus(QMediaPlayer::NoMedia), m_playAfterMediaLoaded(false), m_inputAttached(false), m_stopEventsToIgnore(0), m_bufferStatus(0) { m_loadingTimer.setSingleShot(true); m_loadingTimer.setInterval(0); connect(&m_loadingTimer, SIGNAL(timeout()), this, SLOT(continueLoadMedia())); QCoreApplication::eventDispatcher()->installNativeEventFilter(this); openConnection(); } BbMediaPlayerControl::~BbMediaPlayerControl() { stop(); detach(); closeConnection(); QCoreApplication::eventDispatcher()->removeNativeEventFilter(this); } void BbMediaPlayerControl::openConnection() { m_connection = mmr_connect(NULL); if (!m_connection) { emitPError("Unable to connect to the multimedia renderer"); return; } m_id = idCounter++; m_contextName = QString("BbMediaPlayerControl_%1_%2").arg(m_id) .arg(QCoreApplication::applicationPid()); m_context = mmr_context_create(m_connection, m_contextName.toLatin1(), 0, S_IRWXU|S_IRWXG|S_IRWXO); if (!m_context) { emitPError("Unable to create context"); closeConnection(); return; } m_eventMonitor = mmrenderer_request_events(m_contextName.toLatin1(), 0, m_id); if (!m_eventMonitor) { qDebug() << "Unable to request multimedia events"; emit error(0, "Unable to request multimedia events"); } } void BbMediaPlayerControl::closeConnection() { if (m_eventMonitor) { mmrenderer_stop_events(m_eventMonitor); m_eventMonitor = 0; } if (m_context) { mmr_context_destroy(m_context); m_context = 0; m_contextName.clear(); } if (m_connection) { mmr_disconnect(m_connection); m_connection = 0; } } QString BbMediaPlayerControl::resourcePathForUrl(const QUrl &url) { // If this is a local file, mmrenderer expects the file:// prefix and an absolute path. // We treat URLs without scheme as local files, most likely someone just forgot to set the // file:// prefix when constructing the URL. if (url.isLocalFile() || url.scheme().isEmpty()) { QString relativeFilePath; if (!url.scheme().isEmpty()) relativeFilePath = url.toLocalFile(); else relativeFilePath = url.path(); const QFileInfo fileInfo(relativeFilePath); return QStringLiteral("file://") + fileInfo.absoluteFilePath(); // QRC, copy to temporary file, as mmrenderer does not support resource files } else if (url.scheme() == QStringLiteral("qrc")) { const QString qrcPath = ':' + url.path(); const QFileInfo resourceFileInfo(qrcPath); m_tempMediaFileName = QDir::tempPath() + QStringLiteral("/qtmedia_") + QUuid::createUuid().toString() + QStringLiteral(".") + resourceFileInfo.suffix(); if (!QFile::copy(qrcPath, m_tempMediaFileName)) { const QString errorMsg = QString("Failed to copy resource file to temporary file %1 for playback").arg(m_tempMediaFileName); qDebug() << errorMsg; emit error(0, errorMsg); return QString(); } return m_tempMediaFileName; // HTTP or similar URL, use as-is } else { return url.toString(); } } void BbMediaPlayerControl::attach() { // Should only be called in detached state Q_ASSERT(m_audioId == -1 && !m_inputAttached && m_tempMediaFileName.isEmpty()); if (m_media.isNull() || !m_context) { setMediaStatus(QMediaPlayer::NoMedia); return; } if (m_videoControl) m_videoControl->attachDisplay(m_context); m_audioId = mmr_output_attach(m_context, "audio:default", "audio"); if (m_audioId == -1) { emitMmError("mmr_output_attach() for audio failed"); return; } const QString resourcePath = resourcePathForUrl(m_media.canonicalUrl()); if (resourcePath.isEmpty()) { detach(); return; } if (mmr_input_attach(m_context, QFile::encodeName(resourcePath), "track") != 0) { emitMmError(QString("mmr_input_attach() for %1 failed").arg(resourcePath)); setMediaStatus(QMediaPlayer::InvalidMedia); detach(); return; } // For whatever reason, the mmrenderer sends out a MMR_STOPPED event when calling // mmr_input_attach() above. Ignore it, as otherwise we'll trigger stopping right after we // started. m_stopEventsToIgnore++; m_inputAttached = true; setMediaStatus(QMediaPlayer::LoadedMedia); m_bufferStatus = 0; emit bufferStatusChanged(m_bufferStatus); } void BbMediaPlayerControl::detach() { if (m_context) { if (m_inputAttached) { mmr_input_detach(m_context); m_inputAttached = false; } if (m_videoControl) m_videoControl->detachDisplay(); if (m_audioId != -1 && m_context) { mmr_output_detach(m_context, m_audioId); m_audioId = -1; } } if (!m_tempMediaFileName.isEmpty()) { QFile::remove(m_tempMediaFileName); m_tempMediaFileName.clear(); } m_loadingTimer.stop(); } QMediaPlayer::State BbMediaPlayerControl::state() const { return m_state; } QMediaPlayer::MediaStatus BbMediaPlayerControl::mediaStatus() const { return m_mediaStatus; } qint64 BbMediaPlayerControl::duration() const { return m_metaData.duration(); } qint64 BbMediaPlayerControl::position() const { return m_position; } void BbMediaPlayerControl::setPosition(qint64 position) { if (m_position != position) { m_position = position; // Don't update in stopped state, it would not have any effect. Instead, the position is // updated in play(). if (m_state != QMediaPlayer::StoppedState) setPositionInternal(m_position); emit positionChanged(m_position); } } int BbMediaPlayerControl::volume() const { return m_volume; } void BbMediaPlayerControl::setVolumeInternal(int newVolume) { if (!m_context) return; newVolume = qBound(0, newVolume, 100); if (m_audioId != -1) { strm_dict_t * dict = strm_dict_new(); dict = strm_dict_set(dict, "volume", QString::number(newVolume).toLatin1()); if (mmr_output_parameters(m_context, m_audioId, dict) != 0) emitMmError("mmr_output_parameters: Setting volume failed"); } } void BbMediaPlayerControl::setPlaybackRateInternal(qreal rate) { if (!m_context) return; const int mmRate = rate * 1000; if (mmr_speed_set(m_context, mmRate) != 0) emitMmError("mmr_speed_set failed"); } void BbMediaPlayerControl::setPositionInternal(qint64 position) { if (!m_context) return; if (m_metaData.isSeekable()) { if (mmr_seek(m_context, QString::number(position).toLatin1()) != 0) emitMmError("Seeking failed"); } } void BbMediaPlayerControl::setMediaStatus(QMediaPlayer::MediaStatus status) { if (m_mediaStatus != status) { m_mediaStatus = status; emit mediaStatusChanged(m_mediaStatus); } } void BbMediaPlayerControl::setState(QMediaPlayer::State state) { if (m_state != state) { m_state = state; emit stateChanged(m_state); } } void BbMediaPlayerControl::stopInternal(StopCommand stopCommand) { if (m_state != QMediaPlayer::StoppedState) { if (stopCommand == StopMmRenderer) { ++m_stopEventsToIgnore; mmr_stop(m_context); } setState(QMediaPlayer::StoppedState); } if (m_position != 0) { m_position = 0; emit positionChanged(0); } } void BbMediaPlayerControl::setVolume(int volume) { const int newVolume = qBound(0, volume, 100); if (m_volume != newVolume) { m_volume = newVolume; if (!m_muted) setVolumeInternal(m_volume); emit volumeChanged(m_volume); } } bool BbMediaPlayerControl::isMuted() const { return m_muted; } void BbMediaPlayerControl::setMuted(bool muted) { if (m_muted != muted) { m_muted = muted; setVolumeInternal(muted ? 0 : m_volume); emit mutedChanged(muted); } } int BbMediaPlayerControl::bufferStatus() const { return m_bufferStatus; } bool BbMediaPlayerControl::isAudioAvailable() const { return m_metaData.hasAudio(); } bool BbMediaPlayerControl::isVideoAvailable() const { return m_metaData.hasVideo(); } bool BbMediaPlayerControl::isSeekable() const { return m_metaData.isSeekable(); } QMediaTimeRange BbMediaPlayerControl::availablePlaybackRanges() const { // We can't get this information from the mmrenderer API yet, so pretend we can seek everywhere return QMediaTimeRange(0, m_metaData.duration()); } qreal BbMediaPlayerControl::playbackRate() const { return m_rate; } void BbMediaPlayerControl::setPlaybackRate(qreal rate) { if (m_rate != rate) { m_rate = rate; setPlaybackRateInternal(m_rate); emit playbackRateChanged(m_rate); } } QMediaContent BbMediaPlayerControl::media() const { return m_media; } const QIODevice *BbMediaPlayerControl::mediaStream() const { // Always 0, we don't support QIODevice streams return 0; } void BbMediaPlayerControl::setMedia(const QMediaContent &media, QIODevice *stream) { Q_UNUSED(stream); // not supported stop(); detach(); m_media = media; emit mediaChanged(m_media); // Slight hack: With MediaPlayer QtQuick elements that have autoPlay set to true, playback // would start before the QtQuick canvas is propagated to all elements, and therefore our // video output would not work. Therefore, delay actually playing the media a bit so that the // canvas is ready. // The mmrenderer doesn't allow to attach video outputs after playing has started, otherwise // this would be unnecessary. if (!m_media.isNull()) { setMediaStatus(QMediaPlayer::LoadingMedia); m_loadingTimer.start(); // singleshot timer to continueLoadMedia() } else { continueLoadMedia(); // still needed, as it will update the media status and clear metadata } } void BbMediaPlayerControl::continueLoadMedia() { attach(); updateMetaData(); if (m_playAfterMediaLoaded) play(); } void BbMediaPlayerControl::play() { if (m_playAfterMediaLoaded) m_playAfterMediaLoaded = false; // No-op if we are already playing, except if we were called from continueLoadMedia(), in which // case m_playAfterMediaLoaded is true (hence the 'else'). else if (m_state == QMediaPlayer::PlayingState) return; if (m_mediaStatus == QMediaPlayer::LoadingMedia) { // State changes are supposed to be synchronous setState(QMediaPlayer::PlayingState); // Defer playing to later, when the timer triggers continueLoadMedia() m_playAfterMediaLoaded = true; return; } // Un-pause the state when it is paused if (m_state == QMediaPlayer::PausedState) { setPlaybackRateInternal(m_rate); setState(QMediaPlayer::PlayingState); return; } if (m_media.isNull() || !m_connection || !m_context || m_audioId == -1) { setState(QMediaPlayer::StoppedState); return; } setPositionInternal(m_position); setVolumeInternal(m_volume); setPlaybackRateInternal(m_rate); if (mmr_play(m_context) != 0) { setState(QMediaPlayer::StoppedState); emitMmError("mmr_play() failed"); return; } setState( QMediaPlayer::PlayingState); } void BbMediaPlayerControl::pause() { if (m_state == QMediaPlayer::PlayingState) { setPlaybackRateInternal(0); setState(QMediaPlayer::PausedState); } } void BbMediaPlayerControl::stop() { stopInternal(StopMmRenderer); } void BbMediaPlayerControl::setVideoControl(BbVideoWindowControl *videoControl) { m_videoControl = videoControl; } void BbMediaPlayerControl::setMetaDataReaderControl(BbMetaDataReaderControl *metaDataReaderControl) { m_metaDataReaderControl = metaDataReaderControl; } bool BbMediaPlayerControl::nativeEventFilter(const QByteArray &eventType, void *message, long *result) { Q_UNUSED(eventType); Q_UNUSED(result); bps_event_t * const event = static_cast(message); if (!event || (bps_event_get_domain(event) != mmrenderer_get_domain() && bps_event_get_domain(event) != screen_get_domain())) return false; if (m_videoControl) m_videoControl->bpsEventHandler(event); if (bps_event_get_domain(event) == mmrenderer_get_domain()) { if (bps_event_get_code(event) == MMRENDERER_STATE_CHANGE) { const mmrenderer_state_t newState = mmrenderer_event_get_state(event); if (newState == MMR_STOPPED) { // Only react to stop events that happen when the end of the stream is reached and // playback is stopped because of this. // Ignore other stop event sources, souch as calling mmr_stop() ourselves and // mmr_input_attach(). if (m_stopEventsToIgnore > 0) { --m_stopEventsToIgnore; } else { setMediaStatus(QMediaPlayer::EndOfMedia); stopInternal(IgnoreMmRenderer); } return false; } } if (bps_event_get_code(event) == MMRENDERER_STATUS_UPDATE) { // Prevent spurious position change events from overriding our own position, for example // when setting the position to 0 in stop(). // Also, don't change the position while we're loading the media, as then play() would // set a wrong initial position. if (m_state != QMediaPlayer::PlayingState || m_mediaStatus == QMediaPlayer::LoadingMedia || m_mediaStatus == QMediaPlayer::NoMedia || m_mediaStatus == QMediaPlayer::InvalidMedia) return false; const qint64 newPosition = QString::fromLatin1(mmrenderer_event_get_position(event)).toLongLong(); if (newPosition != 0 && newPosition != m_position) { m_position = newPosition; emit positionChanged(m_position); } const QString bufferStatus = QString::fromLatin1(mmrenderer_event_get_bufferlevel(event)); const int slashPos = bufferStatus.indexOf('/'); if (slashPos != -1) { const int fill = bufferStatus.left(slashPos).toInt(); const int capacity = bufferStatus.mid(slashPos + 1).toInt(); if (capacity != 0) { m_bufferStatus = fill / static_cast(capacity) * 100.0f; emit bufferStatusChanged(m_bufferStatus); } } } } return false; } void BbMediaPlayerControl::updateMetaData() { if (m_mediaStatus == QMediaPlayer::LoadedMedia) m_metaData.parse(m_contextName); else m_metaData.clear(); if (m_videoControl) m_videoControl->setMetaData(m_metaData); if (m_metaDataReaderControl) m_metaDataReaderControl->setMetaData(m_metaData); emit durationChanged(m_metaData.duration()); emit audioAvailableChanged(m_metaData.hasAudio()); emit videoAvailableChanged(m_metaData.hasVideo()); emit availablePlaybackRangesChanged(availablePlaybackRanges()); emit seekableChanged(m_metaData.isSeekable()); } void BbMediaPlayerControl::emitMmError(const QString &msg) { int errorCode = MMR_ERROR_NONE; const QString errorMessage = mmErrorMessage(msg, m_context, &errorCode); qDebug() << errorMessage; emit error(errorCode, errorMessage); } void BbMediaPlayerControl::emitPError(const QString &msg) { const QString errorMessage = QString("%1: %2").arg(msg).arg(strerror(errno)); qDebug() << errorMessage; emit error(errno, errorMessage); } QT_END_NAMESPACE