/* This file is part of the KDE project. | |
Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). | |
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 or 3 of the License. | |
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 <http://www.gnu.org/licenses/>. | |
*/ | |
#include <QResource> | |
#include <QUrl> | |
#include "abstractmediaplayer.h" | |
#include "defs.h" | |
#include "mediaobject.h" | |
#include "utils.h" | |
#include <cdbcols.h> | |
#include <cdblen.h> | |
#include <commdb.h> | |
QT_BEGIN_NAMESPACE | |
using namespace Phonon; | |
using namespace Phonon::MMF; | |
/*! \class MMF::AbstractMediaPlayer | |
\internal | |
*/ | |
//----------------------------------------------------------------------------- | |
// Constants | |
//----------------------------------------------------------------------------- | |
const int NullMaxVolume = -1; | |
const int BufferStatusTimerInterval = 100; // ms | |
//----------------------------------------------------------------------------- | |
// Constructor / destructor | |
//----------------------------------------------------------------------------- | |
MMF::AbstractMediaPlayer::AbstractMediaPlayer | |
(MediaObject *parent, const AbstractPlayer *player) | |
: AbstractPlayer(player) | |
, m_parent(parent) | |
, m_pending(NothingPending) | |
, m_positionTimer(new QTimer(this)) | |
, m_position(0) | |
, m_bufferStatusTimer(new QTimer(this)) | |
, m_mmfMaxVolume(NullMaxVolume) | |
, m_prefinishMarkSent(false) | |
, m_aboutToFinishSent(false) | |
#ifdef PHONON_MMF_PROGRESSIVE_DOWNLOAD | |
, m_download(0) | |
, m_downloadStalled(false) | |
#endif | |
{ | |
connect(m_positionTimer.data(), SIGNAL(timeout()), this, SLOT(positionTick())); | |
connect(m_bufferStatusTimer.data(), SIGNAL(timeout()), this, SLOT(bufferStatusTick())); | |
} | |
//----------------------------------------------------------------------------- | |
// MediaObjectInterface | |
//----------------------------------------------------------------------------- | |
void MMF::AbstractMediaPlayer::play() | |
{ | |
TRACE_CONTEXT(AbstractMediaPlayer::play, EAudioApi); | |
TRACE_ENTRY("state %d", privateState()); | |
switch (privateState()) { | |
case GroundState: | |
setError(tr("Not ready to play")); | |
break; | |
case LoadingState: | |
setPending(PlayPending); | |
break; | |
case StoppedState: | |
case PausedState: | |
startPlayback(); | |
break; | |
case PlayingState: | |
case BufferingState: | |
case ErrorState: | |
// Do nothing | |
break; | |
// Protection against adding new states and forgetting to update this switch | |
default: | |
TRACE_PANIC(InvalidStatePanic); | |
} | |
TRACE_EXIT("state %d", privateState()); | |
} | |
void MMF::AbstractMediaPlayer::pause() | |
{ | |
TRACE_CONTEXT(AbstractMediaPlayer::pause, EAudioApi); | |
TRACE_ENTRY("state %d", privateState()); | |
stopTimers(); | |
switch (privateState()) { | |
case GroundState: | |
case LoadingState: | |
case StoppedState: | |
setPending(PausePending); | |
break; | |
case PausedState: | |
// Do nothing | |
break; | |
case PlayingState: | |
case BufferingState: | |
changeState(PausedState); | |
// Fall through | |
case ErrorState: | |
doPause(); | |
break; | |
// Protection against adding new states and forgetting to update this switch | |
default: | |
TRACE_PANIC(InvalidStatePanic); | |
} | |
TRACE_EXIT("state %d", privateState()); | |
} | |
void MMF::AbstractMediaPlayer::stop() | |
{ | |
TRACE_CONTEXT(AbstractMediaPlayer::stop, EAudioApi); | |
TRACE_ENTRY("state %d", privateState()); | |
setPending(NothingPending); | |
stopTimers(); | |
switch (privateState()) { | |
case GroundState: | |
case LoadingState: | |
case StoppedState: | |
case ErrorState: | |
// Do nothing | |
break; | |
case PlayingState: | |
case BufferingState: | |
case PausedState: | |
doStop(); | |
changeState(StoppedState); | |
break; | |
// Protection against adding new states and forgetting to update this switch | |
default: | |
TRACE_PANIC(InvalidStatePanic); | |
} | |
TRACE_EXIT("state %d", privateState()); | |
} | |
void MMF::AbstractMediaPlayer::seek(qint64 ms) | |
{ | |
TRACE_CONTEXT(AbstractMediaPlayer::seek, EAudioApi); | |
TRACE_ENTRY("state %d pos %Ld", state(), ms); | |
switch (privateState()) { | |
// Fallthrough all these | |
case GroundState: | |
case StoppedState: | |
case PausedState: | |
case PlayingState: | |
case LoadingState: | |
{ | |
bool wasPlaying = false; | |
if (state() == PlayingState) { | |
stopPositionTimer(); | |
doPause(); | |
wasPlaying = true; | |
} | |
doSeek(ms); | |
m_position = ms; | |
resetMarksIfRewound(); | |
if(wasPlaying && state() != ErrorState) { | |
doPlay(); | |
startPositionTimer(); | |
} | |
break; | |
} | |
case BufferingState: | |
// Fallthrough | |
case ErrorState: | |
// Do nothing | |
break; | |
} | |
TRACE_EXIT_0(); | |
} | |
bool MMF::AbstractMediaPlayer::isSeekable() const | |
{ | |
return true; | |
} | |
qint64 MMF::AbstractMediaPlayer::currentTime() const | |
{ | |
return m_position; | |
} | |
void MMF::AbstractMediaPlayer::doSetTickInterval(qint32 interval) | |
{ | |
TRACE_CONTEXT(AbstractMediaPlayer::doSetTickInterval, EAudioApi); | |
TRACE_ENTRY("state %d m_interval %d interval %d", privateState(), tickInterval(), interval); | |
m_positionTimer->setInterval(interval); | |
TRACE_EXIT_0(); | |
} | |
void MMF::AbstractMediaPlayer::open() | |
{ | |
TRACE_CONTEXT(AbstractMediaPlayer::open, EAudioApi); | |
const MediaSource source = m_parent->source(); | |
TRACE_ENTRY("state %d source.type %d", privateState(), source.type()); | |
close(); | |
changeState(GroundState); | |
TInt symbianErr = KErrNone; | |
QString errorMessage; | |
switch (source.type()) { | |
case MediaSource::LocalFile: { | |
RFile *const file = m_parent->file(); | |
Q_ASSERT(file); | |
symbianErr = openFile(*file); | |
if (KErrNone != symbianErr) | |
errorMessage = tr("Error opening file"); | |
break; | |
} | |
case MediaSource::Url: { | |
const QUrl url(source.url()); | |
if (url.scheme() == QLatin1String("file")) { | |
RFile *const file = m_parent->file(); | |
Q_ASSERT(file); | |
symbianErr = openFile(*file); | |
if (KErrNone != symbianErr) | |
errorMessage = tr("Error opening file"); | |
} | |
#ifdef PHONON_MMF_PROGRESSIVE_DOWNLOAD | |
else if (url.scheme() == QLatin1String("http")) { | |
Q_ASSERT(!m_download); | |
m_download = new Download(url, this); | |
connect(m_download, SIGNAL(lengthChanged(qint64)), | |
this, SLOT(downloadLengthChanged(qint64))); | |
connect(m_download, SIGNAL(stateChanged(Download::State)), | |
this, SLOT(downloadStateChanged(Download::State))); | |
int iap = m_parent->currentIAP(); | |
TRACE("HTTP Url: Using IAP %d", iap); | |
m_download->start(iap); | |
} | |
#endif | |
else { | |
int iap = m_parent->currentIAP(); | |
TRACE("Using IAP %d", iap); | |
symbianErr = openUrl(url.toString(), iap); | |
if (KErrNone != symbianErr) | |
errorMessage = tr("Error opening URL"); | |
} | |
break; | |
} | |
case MediaSource::Stream: { | |
QResource *const resource = m_parent->resource(); | |
if (resource) { | |
m_buffer.Set(resource->data(), resource->size()); | |
symbianErr = openDescriptor(m_buffer); | |
if (KErrNone != symbianErr) | |
errorMessage = tr("Error opening resource"); | |
} else { | |
errorMessage = tr("Error opening source: resource not opened"); | |
} | |
break; | |
} | |
// Other source types are handled in MediaObject::createPlayer | |
// Protection against adding new media types and forgetting to update this switch | |
default: | |
TRACE_PANIC(InvalidMediaTypePanic); | |
} | |
if (errorMessage.isEmpty()) { | |
changeState(LoadingState); | |
} else { | |
if (symbianErr) | |
setError(errorMessage, symbianErr); | |
else | |
setError(errorMessage); | |
} | |
TRACE_EXIT_0(); | |
} | |
void MMF::AbstractMediaPlayer::close() | |
{ | |
doClose(); | |
#ifdef PHONON_MMF_PROGRESSIVE_DOWNLOAD | |
delete m_download; | |
m_download = 0; | |
#endif | |
m_position = 0; | |
} | |
void MMF::AbstractMediaPlayer::volumeChanged(qreal volume) | |
{ | |
TRACE_CONTEXT(AbstractMediaPlayer::volumeChanged, EAudioInternal); | |
TRACE_ENTRY("state %d", privateState()); | |
AbstractPlayer::volumeChanged(volume); | |
doVolumeChanged(); | |
TRACE_EXIT_0(); | |
} | |
//----------------------------------------------------------------------------- | |
// Private functions | |
//----------------------------------------------------------------------------- | |
void MMF::AbstractMediaPlayer::startPositionTimer() | |
{ | |
m_positionTimer->start(tickInterval()); | |
} | |
void MMF::AbstractMediaPlayer::stopPositionTimer() | |
{ | |
m_positionTimer->stop(); | |
} | |
void MMF::AbstractMediaPlayer::startBufferStatusTimer() | |
{ | |
m_bufferStatusTimer->start(BufferStatusTimerInterval); | |
} | |
void MMF::AbstractMediaPlayer::stopBufferStatusTimer() | |
{ | |
m_bufferStatusTimer->stop(); | |
} | |
void MMF::AbstractMediaPlayer::stopTimers() | |
{ | |
stopPositionTimer(); | |
stopBufferStatusTimer(); | |
} | |
void MMF::AbstractMediaPlayer::doVolumeChanged() | |
{ | |
switch (privateState()) { | |
case GroundState: | |
case LoadingState: | |
case ErrorState: | |
// Do nothing | |
break; | |
case StoppedState: | |
case PausedState: | |
case PlayingState: | |
case BufferingState: { | |
const qreal volume = (m_volume * m_mmfMaxVolume) + 0.5; | |
const int err = setDeviceVolume(volume); | |
if (KErrNone != err) { | |
setError(tr("Setting volume failed"), err); | |
} | |
break; | |
} | |
// Protection against adding new states and forgetting to update this | |
// switch | |
default: | |
Utils::panic(InvalidStatePanic); | |
} | |
} | |
//----------------------------------------------------------------------------- | |
// Protected functions | |
//----------------------------------------------------------------------------- | |
void MMF::AbstractMediaPlayer::bufferingStarted() | |
{ | |
m_stateBeforeBuffering = privateState(); | |
changeState(BufferingState); | |
bufferStatusTick(); | |
startBufferStatusTimer(); | |
} | |
void MMF::AbstractMediaPlayer::bufferingComplete() | |
{ | |
stopBufferStatusTimer(); | |
emit MMF::AbstractPlayer::bufferStatus(100); | |
if (!progressiveDownloadStalled()) | |
changeState(m_stateBeforeBuffering); | |
} | |
void MMF::AbstractMediaPlayer::maxVolumeChanged(int mmfMaxVolume) | |
{ | |
m_mmfMaxVolume = mmfMaxVolume; | |
doVolumeChanged(); | |
} | |
void MMF::AbstractMediaPlayer::loadingComplete(int error) | |
{ | |
TRACE_CONTEXT(AbstractMediaPlayer::loadingComplete, EAudioApi); | |
TRACE_ENTRY("state %d error %d", state(), error); | |
if (progressiveDownloadStalled()) { | |
Q_ASSERT(Phonon::BufferingState == state()); | |
if (KErrNone == error) { | |
bufferingComplete(); | |
doSeek(m_position); | |
startPlayback(); | |
#ifdef PHONON_MMF_PROGRESSIVE_DOWNLOAD | |
m_downloadStalled = false; | |
#endif | |
} | |
} else { | |
Q_ASSERT(Phonon::LoadingState == state()); | |
if (KErrNone == error) { | |
updateMetaData(); | |
changeState(StoppedState); | |
} else { | |
if (isProgressiveDownload() && KErrCorrupt == error) { | |
setProgressiveDownloadStalled(); | |
} else { | |
setError(tr("Loading clip failed"), error); | |
} | |
} | |
} | |
} | |
void MMF::AbstractMediaPlayer::playbackComplete(int error) | |
{ | |
stopTimers(); | |
if (KErrNone == error && !m_aboutToFinishSent) { | |
const qint64 total = totalTime(); | |
emit MMF::AbstractPlayer::tick(total); | |
m_aboutToFinishSent = true; | |
emit aboutToFinish(); | |
} | |
if (KErrNone == error) { | |
changeState(PausedState); | |
// MediaObject::switchToNextSource deletes the current player, so we | |
// call it via delayed slot invokation to ensure that this object does | |
// not get deleted during execution of a member function. | |
QMetaObject::invokeMethod(m_parent, "switchToNextSource", Qt::QueuedConnection); | |
} | |
else { | |
if (isProgressiveDownload() && KErrCorrupt == error) { | |
setProgressiveDownloadStalled(); | |
} else { | |
setError(tr("Playback complete"), error); | |
emit finished(); | |
} | |
} | |
} | |
qint64 MMF::AbstractMediaPlayer::toMilliSeconds(const TTimeIntervalMicroSeconds &in) | |
{ | |
return in.Int64() / 1000; | |
} | |
bool MMF::AbstractMediaPlayer::isProgressiveDownload() const | |
{ | |
#ifdef PHONON_MMF_PROGRESSIVE_DOWNLOAD | |
return (0 != m_download); | |
#else | |
return false; | |
#endif | |
} | |
bool MMF::AbstractMediaPlayer::progressiveDownloadStalled() const | |
{ | |
#ifdef PHONON_MMF_PROGRESSIVE_DOWNLOAD | |
return m_downloadStalled; | |
#else | |
return false; | |
#endif | |
} | |
//----------------------------------------------------------------------------- | |
// Slots | |
//----------------------------------------------------------------------------- | |
void MMF::AbstractMediaPlayer::positionTick() | |
{ | |
const qint64 pos = getCurrentTime(); | |
if (pos > m_position) { | |
m_position = pos; | |
emitMarksIfReached(m_position); | |
emit MMF::AbstractPlayer::tick(m_position); | |
} | |
} | |
void MMF::AbstractMediaPlayer::emitMarksIfReached(qint64 current) | |
{ | |
const qint64 total = totalTime(); | |
const qint64 remaining = total - current; | |
if (prefinishMark() && !m_prefinishMarkSent) { | |
if (remaining < (prefinishMark() + tickInterval()/2)) { | |
m_prefinishMarkSent = true; | |
emit prefinishMarkReached(remaining); | |
} | |
} | |
if (!m_aboutToFinishSent) { | |
if (remaining < tickInterval()) { | |
m_aboutToFinishSent = true; | |
emit aboutToFinish(); | |
} | |
} | |
} | |
void MMF::AbstractMediaPlayer::resetMarksIfRewound() | |
{ | |
const qint64 current = getCurrentTime(); | |
const qint64 total = totalTime(); | |
const qint64 remaining = total - current; | |
if (prefinishMark() && m_prefinishMarkSent) | |
if (remaining >= (prefinishMark() + tickInterval()/2)) | |
m_prefinishMarkSent = false; | |
if (m_aboutToFinishSent) | |
if (remaining >= tickInterval()) | |
m_aboutToFinishSent = false; | |
} | |
void MMF::AbstractMediaPlayer::setPending(Pending pending) | |
{ | |
const Phonon::State oldState = state(); | |
m_pending = pending; | |
const Phonon::State newState = state(); | |
if (newState != oldState) | |
emit stateChanged(newState, oldState); | |
} | |
void MMF::AbstractMediaPlayer::startPlayback() | |
{ | |
doPlay(); | |
startPositionTimer(); | |
changeState(PlayingState); | |
} | |
void MMF::AbstractMediaPlayer::setProgressiveDownloadStalled() | |
{ | |
#ifdef PHONON_MMF_PROGRESSIVE_DOWNLOAD | |
TRACE_CONTEXT(AbstractMediaPlayer::setProgressiveDownloadStalled, EAudioApi); | |
TRACE_ENTRY("state %d", state()); | |
Q_ASSERT(isProgressiveDownload()); | |
m_downloadStalled = true; | |
doClose(); | |
bufferingStarted(); | |
// Video player loses window handle when closed - need to reapply it here | |
videoOutputChanged(); | |
m_download->resume(); | |
#endif | |
} | |
void MMF::AbstractMediaPlayer::bufferStatusTick() | |
{ | |
// During progressive download, there is no way to detect the buffering status. | |
// Phonon does not support a "buffering; amount unknown" signal, therefore we | |
// return a buffering status of zero. | |
const int status = progressiveDownloadStalled() ? 0 : bufferStatus(); | |
emit MMF::AbstractPlayer::bufferStatus(status); | |
} | |
#ifdef PHONON_MMF_PROGRESSIVE_DOWNLOAD | |
void MMF::AbstractMediaPlayer::downloadLengthChanged(qint64 length) | |
{ | |
TRACE_CONTEXT(AbstractMediaPlayer::downloadLengthChanged, EAudioApi); | |
TRACE_ENTRY("length %Ld", length); | |
Q_UNUSED(length) | |
if (m_downloadStalled) { | |
bufferingComplete(); | |
int err = m_parent->openFileHandle(m_download->targetFileName()); | |
if (KErrNone == err) | |
err = openFile(*m_parent->file()); | |
if (KErrNone != err) | |
setError(tr("Error opening file")); | |
} | |
} | |
void MMF::AbstractMediaPlayer::downloadStateChanged(Download::State state) | |
{ | |
TRACE_CONTEXT(AbstractMediaPlayer::downloadStateChanged, EAudioApi); | |
TRACE_ENTRY("state %d", state); | |
switch (state) { | |
case Download::Idle: | |
case Download::Initializing: | |
break; | |
case Download::Downloading: | |
{ | |
int err = m_parent->openFileHandle(m_download->targetFileName()); | |
if (KErrNone == err) | |
err = openFile(*m_parent->file()); | |
else if (KErrCorrupt == err) | |
// Insufficient data downloaded - enter Buffering state | |
setProgressiveDownloadStalled(); | |
if (KErrNone != err) | |
setError(tr("Error opening file")); | |
} | |
break; | |
case Download::Complete: | |
break; | |
case Download::Error: | |
setError(tr("Download error")); | |
break; | |
} | |
} | |
#endif // PHONON_MMF_PROGRESSIVE_DOWNLOAD | |
Phonon::State MMF::AbstractMediaPlayer::phononState(PrivateState state) const | |
{ | |
Phonon::State result = AbstractPlayer::phononState(state); | |
if (PausePending == m_pending) { | |
Q_ASSERT(Phonon::StoppedState == result || Phonon::LoadingState == result); | |
result = Phonon::PausedState; | |
} | |
return result; | |
} | |
void MMF::AbstractMediaPlayer::changeState(PrivateState newState) | |
{ | |
TRACE_CONTEXT(AbstractMediaPlayer::changeState, EAudioInternal); | |
const Phonon::State oldPhononState = phononState(privateState()); | |
const Phonon::State newPhononState = phononState(newState); | |
if (LoadingState == oldPhononState && StoppedState == newPhononState) { | |
switch (m_pending) { | |
case NothingPending: | |
AbstractPlayer::changeState(newState); | |
break; | |
case PlayPending: | |
changeState(PlayingState); // necessary in order to apply initial volume | |
doVolumeChanged(); | |
startPlayback(); | |
break; | |
case PausePending: | |
AbstractPlayer::changeState(PausedState); | |
break; | |
} | |
setPending(NothingPending); | |
} else { | |
AbstractPlayer::changeState(newState); | |
} | |
} | |
void MMF::AbstractMediaPlayer::updateMetaData() | |
{ | |
TRACE_CONTEXT(AbstractMediaPlayer::updateMetaData, EAudioInternal); | |
TRACE_ENTRY_0(); | |
m_metaData.clear(); | |
const int numberOfEntries = numberOfMetaDataEntries(); | |
for(int i=0; i<numberOfEntries; ++i) { | |
const QPair<QString, QString> entry = metaDataEntry(i); | |
// Note that we capitalize the key, as required by the Ogg Vorbis | |
// metadata standard to which Phonon adheres: | |
// http://xiph.org/vorbis/doc/v-comment.html | |
m_metaData.insert(entry.first.toUpper(), entry.second); | |
} | |
emit metaDataChanged(m_metaData); | |
TRACE_EXIT_0(); | |
} | |
QT_END_NAMESPACE | |