playermanager.cpp 17.1 KB
Newer Older
1 2 3 4
/***************************************************************************
    begin                : Sat Feb 14 2004
    copyright            : (C) 2004 by Scott Wheeler
    email                : wheeler@kde.org
5 6 7 8

    copyright            : (C) 2007 by Matthias Kretz
    email                : kretz@kde.org

9
    copyright            : (C) 2008, 2009 by Michael Pyne
10
    email                : michael.pyne@kdemail.net
11 12 13 14 15 16 17 18 19 20 21
***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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.                                   *
 *                                                                         *
 ***************************************************************************/

22 23 24
#include "playermanager.h"
#include <config.h>

25
#include <kdebug.h>
Matthias Kretz's avatar
Matthias Kretz committed
26
#include <kmessagebox.h>
27
#include <klocale.h>
28 29 30
#include <kactioncollection.h>
#include <kselectaction.h>
#include <ktoggleaction.h>
Andreas Pakulat's avatar
Andreas Pakulat committed
31
#include <kurl.h>
32

33 34 35
#include <Phonon/AudioOutput>
#include <Phonon/MediaObject>
#include <Phonon/VolumeFaderEffect>
36

Laurent Montel's avatar
Laurent Montel committed
37
#include <QPixmap>
38 39
#include <QTimer>

40 41
#include <math.h>

42
#include "playlistinterface.h"
43
#include "playeradaptor.h"
44
#include "slideraction.h"
45
#include "statuslabel.h"
46
#include "actioncollection.h"
47
#include "collectionlist.h"
Michael Pyne's avatar
Michael Pyne committed
48
#include "coverinfo.h"
49
#include "tag.h"
50 51

using namespace ActionCollection;
52

Stephan Kulow's avatar
Stephan Kulow committed
53
enum PlayerManagerStatus { StatusStopped = -1, StatusPaused = 1, StatusPlaying = 2 };
54

55 56 57 58 59
////////////////////////////////////////////////////////////////////////////////
// protected members
////////////////////////////////////////////////////////////////////////////////

PlayerManager::PlayerManager() :
60
    QObject(),
61
    m_sliderAction(0),
62
    m_playlistInterface(0),
63
    m_statusLabel(0),
64
    m_noSeek(false),
65
    m_muted(false),
66
    m_setup(false),
67
    m_curOutputPath(0)
68
{
Michael Pyne's avatar
Michael Pyne committed
69 70 71 72 73
// This class is the first thing constructed during program startup, and
// therefore has no access to the widgets needed by the setup() method.
// Since the setup() method will be called indirectly by the player() method
// later, just disable it here. -- mpyne
//    setup();
Laurent Montel's avatar
Port it  
Laurent Montel committed
74
    new PlayerAdaptor( this );
75
    QDBusConnection::sessionBus().registerObject("/Player", this);
76 77 78 79 80 81 82 83 84 85 86 87
}

PlayerManager::~PlayerManager()
{
}

////////////////////////////////////////////////////////////////////////////////
// public members
////////////////////////////////////////////////////////////////////////////////

PlayerManager *PlayerManager::instance() // static
{
88 89
    static PlayerManager manager;
    return &manager;
90 91
}

92 93
bool PlayerManager::playing() const
{
94
    if(!m_setup)
95 96
        return false;

97 98
    Phonon::State state = m_media[m_curOutputPath]->state();
    return (state == Phonon::PlayingState || state == Phonon::BufferingState);
99 100 101 102
}

bool PlayerManager::paused() const
{
103
    if(!m_setup)
104 105
        return false;

106
    return m_media[m_curOutputPath]->state() == Phonon::PausedState;
107 108 109 110
}

float PlayerManager::volume() const
{
111
    if(!m_setup)
112
        return 1.0;
113

114
    return m_output[m_curOutputPath]->volume();
115 116
}

117 118
int PlayerManager::status() const
{
119
    if(!m_setup)
120
        return StatusStopped;
121

122
    if(paused())
123
        return StatusPaused;
124

125
    if(playing())
126
        return StatusPlaying;
127

128 129 130
    return 0;
}

Scott Wheeler's avatar
Scott Wheeler committed
131
int PlayerManager::totalTime() const
132
{
133
    if(!m_setup)
134 135
        return 0;

136
    return m_media[m_curOutputPath]->totalTime() / 1000;
137 138
}

Scott Wheeler's avatar
Scott Wheeler committed
139
int PlayerManager::currentTime() const
140
{
141
    if(!m_setup)
142 143
        return 0;

144
    return m_media[m_curOutputPath]->currentTime() / 1000;
145 146
}

147 148 149 150 151 152 153
QStringList PlayerManager::trackProperties()
{
    return FileHandle::properties();
}

QString PlayerManager::trackProperty(const QString &property) const
{
Michael Pyne's avatar
Michael Pyne committed
154
    if(!playing() && !paused())
155
        return QString();
156 157 158 159

    return m_file.property(property);
}

Michael Pyne's avatar
Michael Pyne committed
160 161 162 163 164
QPixmap PlayerManager::trackCover(const QString &size) const
{
    if(!playing() && !paused())
        return QPixmap();

165
    if(size.toLower() == "small")
Michael Pyne's avatar
Michael Pyne committed
166
        return m_file.coverInfo()->pixmap(CoverInfo::Thumbnail);
167
    if(size.toLower() == "large")
Michael Pyne's avatar
Michael Pyne committed
168 169 170 171 172
        return m_file.coverInfo()->pixmap(CoverInfo::FullSize);

    return QPixmap();
}

173 174 175 176 177
FileHandle PlayerManager::playingFile() const
{
    return m_file;
}

178 179 180
QString PlayerManager::playingString() const
{
    if(!playing())
181
        return QString();
182

183 184 185 186 187
    QString str = m_file.tag()->artist() + " - " + m_file.tag()->title();
    if(m_file.tag()->artist().isEmpty())
        str = m_file.tag()->title();

    return str;
188 189
}

190
void PlayerManager::setPlaylistInterface(PlaylistInterface *interface)
191 192 193 194
{
    m_playlistInterface = interface;
}

195 196 197 198 199
void PlayerManager::setStatusLabel(StatusLabel *label)
{
    m_statusLabel = label;
}

200 201 202 203
////////////////////////////////////////////////////////////////////////////////
// public slots
////////////////////////////////////////////////////////////////////////////////

204
void PlayerManager::play(const FileHandle &file)
205
{
206
    if(!m_setup)
207 208
        setup();

209
    if(!m_media[0] || !m_media[1] || !m_playlistInterface)
210 211
        return;

212 213 214 215
    stopCrossfade();

    // The "currently playing" media object.
    Phonon::MediaObject *mediaObject = m_media[m_curOutputPath];
216

217
    if(file.isNull()) {
218
        if(paused())
219
            mediaObject->play();
220
        else if(playing()) {
221
            mediaObject->seek(0);
222
        }
223
        else {
224
            m_playlistInterface->playNext();
225
            m_file = m_playlistInterface->currentFile();
226

227
            if(!m_file.isNull())
228
            {
229 230
                mediaObject->setCurrentSource(KUrl::fromPath(m_file.absFilePath()));
                mediaObject->play();
231
            }
232
        }
233
    }
234
    else {
235
        m_file = file;
Matthias Kretz's avatar
Matthias Kretz committed
236

237 238
        mediaObject->setCurrentSource(KUrl::fromPath(m_file.absFilePath()));
        mediaObject->play();
239
    }
240

241 242
    // Our state changed handler will perform the follow up actions necessary
    // once we actually start playing.
243 244
}

245 246 247 248 249 250 251 252 253
void PlayerManager::play(const QString &file)
{
    CollectionListItem *item = CollectionList::instance()->lookup(file);
    if(item) {
        Playlist::setPlaying(item);
        play(item->file());
    }
}

Scott Wheeler's avatar
Scott Wheeler committed
254 255 256 257 258
void PlayerManager::play()
{
    play(FileHandle::null());
}

259 260
void PlayerManager::pause()
{
261
    if(!m_setup)
262 263
        return;

264
    if(paused()) {
265 266 267 268
        play();
        return;
    }

269
    action("pause")->setEnabled(false);
270

271
    m_media[m_curOutputPath]->pause();
272 273

    emit signalPause();
274 275 276 277
}

void PlayerManager::stop()
{
278
    if(!m_setup || !m_playlistInterface)
279 280
        return;

281 282 283 284
    action("pause")->setEnabled(false);
    action("stop")->setEnabled(false);
    action("back")->setEnabled(false);
    action("forward")->setEnabled(false);
285
    action("forwardAlbum")->setEnabled(false);
286

287 288 289 290
    // Fading out playback is for chumps.
    stopCrossfade();
    m_media[0]->stop();
    m_media[1]->stop();
291

292
    emit signalStop();
293 294
}

Laurent Montel's avatar
Laurent Montel committed
295
void PlayerManager::setVolume(float volume)
296
{
297
    if(!m_setup)
298
        setup();
299

300 301
    m_output[0]->setVolume(volume);
    m_output[1]->setVolume(volume);
302 303
}

304
void PlayerManager::seek(int seekTime)
305
{
306
    if(!m_setup)
307
        return;
308

309 310
    stopCrossfade();
    m_media[m_curOutputPath]->seek(seekTime);
311 312 313 314
}

void PlayerManager::seekForward()
{
315 316 317 318 319 320
    Phonon::MediaObject *mediaObject = m_media[m_curOutputPath];
    const qint64 total = mediaObject->totalTime();
    const qint64 newtime = mediaObject->currentTime() + total / 100;

    stopCrossfade();
    mediaObject->seek(qMin(total, newtime));
321 322 323 324
}

void PlayerManager::seekBack()
{
325 326 327 328 329 330
    Phonon::MediaObject *mediaObject = m_media[m_curOutputPath];
    const qint64 total = mediaObject->totalTime();
    const qint64 newtime = mediaObject->currentTime() - total / 100;

    stopCrossfade();
    mediaObject->seek(qMax(qint64(0), newtime));
331 332
}

333 334
void PlayerManager::playPause()
{
David Faure's avatar
David Faure committed
335
    playing() ? action("pause")->trigger() : action("play")->trigger();
336 337
}

338 339
void PlayerManager::forward()
{
340 341
    m_playlistInterface->playNext();
    FileHandle file = m_playlistInterface->currentFile();
342

343 344 345 346 347 348 349 350
    if(!file.isNull())
        play(file);
    else
        stop();
}

void PlayerManager::back()
{
351 352
    m_playlistInterface->playPrevious();
    FileHandle file = m_playlistInterface->currentFile();
353

354 355 356 357 358 359
    if(!file.isNull())
        play(file);
    else
        stop();
}

360 361
void PlayerManager::volumeUp()
{
362
    if(!m_setup)
363
        return;
364

365
    setVolume(volume() + 0.04); // 4% up
366 367 368 369
}

void PlayerManager::volumeDown()
{
370
    if(!m_output)
371
        return;
372

373
    setVolume(volume() - 0.04); // 4% down
374 375 376 377
}

void PlayerManager::mute()
{
378
    if(!m_setup)
379 380
        return;

381
    m_output[m_curOutputPath]->setMuted(!m_output[m_curOutputPath]->isMuted());
382 383
}

384 385 386
////////////////////////////////////////////////////////////////////////////////
// private slots
////////////////////////////////////////////////////////////////////////////////
387

388 389
void PlayerManager::slotNeedNextUrl()
{
Matthias Kretz's avatar
Matthias Kretz committed
390 391
    if(m_file.isNull())
        return;
392

393 394
    m_playlistInterface->playNext();
    FileHandle nextFile = m_playlistInterface->currentFile();
395 396

    if(!nextFile.isNull()) {
397
        m_file = nextFile;
398
        crossfadeToFile(m_file);
399 400 401
    }
}

402 403
void PlayerManager::slotFinished()
{
404 405 406 407 408
    // It is possible to end up in this function if a file simply fails to play or if the
    // user moves the slider all the way to the end, therefore see if we can keep playing
    // and if we can, do so.  Otherwise, stop.  Note that this slot should
    // only be called by the currently "main" output path (i.e. not from the
    // crossfading one)
409

410 411
    m_playlistInterface->playNext();
    m_file = m_playlistInterface->currentFile();
412

413 414 415 416 417 418 419
    if(m_file.isNull()) {
        stop();
    }
    else {
        m_media[m_curOutputPath]->setCurrentSource(m_file.absFilePath());
        m_media[m_curOutputPath]->play();
    }
420 421
}

422 423 424 425 426
void PlayerManager::slotLength(qint64 msec)
{
    m_statusLabel->setItemTotalTime(msec / 1000);
}

427
void PlayerManager::slotTick(qint64 msec)
428
{
429
    if(!m_setup || !m_playlistInterface)
430
        return;
431

432
    m_noSeek = true;
433

434
    if(m_statusLabel)
435
        m_statusLabel->setItemCurrentTime(msec / 1000);
436

437
    m_noSeek = false;
438 439
}

440
void PlayerManager::slotStateChanged(Phonon::State newstate, Phonon::State oldstate)
Matthias Kretz's avatar
Matthias Kretz committed
441
{
442 443 444 445 446 447
    // Use sender() since either media object may have sent the signal.
    Phonon::MediaObject *mediaObject = qobject_cast<Phonon::MediaObject *>(sender());
    if(!mediaObject)
        return;

    // Handle errors for either media object
448
    if(newstate == Phonon::ErrorState) {
449 450 451 452 453 454
        QString errorMessage =
            i18nc(
              "%1 will be the /path/to/file, %2 will be some string from Phonon describing the error",
              "JuK is unable to play the audio file<nl><filename>%1</filename><nl>"
                "for the following reason:<nl><message>%2</message>",
              m_file.absFilePath(),
455
              mediaObject->errorString()
456 457
            );

458
        switch(mediaObject->errorType()) {
459 460 461
            case Phonon::NoError:
                kDebug() << "received a state change to ErrorState but errorType is NoError!?";
                break;
462

Matthias Kretz's avatar
Matthias Kretz committed
463 464
            case Phonon::NormalError:
                forward();
465
                KMessageBox::information(0, errorMessage);
Matthias Kretz's avatar
Matthias Kretz committed
466
                break;
467

Matthias Kretz's avatar
Matthias Kretz committed
468 469 470
            case Phonon::FatalError:
                // stop playback
                stop();
471
                KMessageBox::sorry(0, errorMessage);
Matthias Kretz's avatar
Matthias Kretz committed
472 473 474
                break;
        }
    }
475

476 477 478 479 480
    // Now bail out if we're not dealing with the currently playing media
    // object.

    if(mediaObject != m_media[m_curOutputPath])
        return;
481

482 483 484 485 486 487
    // Handle state changes for the playing media object.
    if(newstate == Phonon::StoppedState && oldstate != Phonon::LoadingState) {
        // If this occurs it should be due to a transitory shift (i.e. playing a different
        // song when one is playing now), since it didn't occur in the error handler.  Just
        // in case we really did abruptly stop, handle that case in a couple of seconds.
        QTimer::singleShot(2000, this, SLOT(slotUpdateGuiIfStopped()));
488
    }
489 490 491 492 493 494 495 496 497 498
    else if(newstate == Phonon::PlayingState) {
        action("pause")->setEnabled(true);
        action("stop")->setEnabled(true);
        action("forward")->setEnabled(true);
        if(action<KToggleAction>("albumRandomPlay")->isChecked())
            action("forwardAlbum")->setEnabled(true);
        action("back")->setEnabled(true);

        emit signalPlay();
    }
499 500
}

501 502 503 504 505 506 507 508
////////////////////////////////////////////////////////////////////////////////
// private members
////////////////////////////////////////////////////////////////////////////////

void PlayerManager::setup()
{
    // All of the actions required by this class should be listed here.

509 510 511
    if(!action("pause") ||
       !action("stop") ||
       !action("back") ||
512
       !action("forwardAlbum") ||
513 514
       !action("forward") ||
       !action("trackPositionAction"))
515
    {
Thiago Macieira's avatar
Thiago Macieira committed
516
        kWarning(65432) << "Could not find all of the required actions.";
517 518 519
        return;
    }

520 521 522 523
    if(m_setup)
        return;
    m_setup = true;

524 525 526 527 528 529
    // We use two audio paths at all times to make cross fading easier (and to also easily
    // support not using cross fading with the same code).  The currently playing audio
    // path is controlled using m_curOutputPath.

    for(int i = 0; i < 2; ++i) {
        m_output[i] = new Phonon::AudioOutput(Phonon::MusicCategory, this);
530

531 532 533
        m_media[i] = new Phonon::MediaObject(this);
        m_audioPath[i] = Phonon::createPath(m_media[i], m_output[i]);
        m_media[i]->setTickInterval(200);
534

535 536 537 538 539 540 541 542 543 544 545
        // Pre-cache a volume fader object
        m_fader[i] = new Phonon::VolumeFaderEffect(m_media[i]);
        m_audioPath[i].insertEffect(m_fader[i]);
        m_fader[i]->setVolume(1.0f);

        connect(m_media[i], SIGNAL(stateChanged(Phonon::State, Phonon::State)), SLOT(slotStateChanged(Phonon::State, Phonon::State)));
        connect(m_media[i], SIGNAL(aboutToFinish()), SLOT(slotNeedNextUrl()));
        connect(m_media[i], SIGNAL(totalTimeChanged(qint64)), SLOT(slotLength(qint64)));
        connect(m_media[i], SIGNAL(tick(qint64)), SLOT(slotTick(qint64)));
        connect(m_media[i], SIGNAL(finished()), SLOT(slotFinished()));
    }
546

547 548
    // initialize action states

549 550 551 552
    action("pause")->setEnabled(false);
    action("stop")->setEnabled(false);
    action("back")->setEnabled(false);
    action("forward")->setEnabled(false);
553
    action("forwardAlbum")->setEnabled(false);
554

555
    // setup sliders (a separate slot is used to switch as needed)
556

557
    m_sliderAction = action<SliderAction>("trackPositionAction");
558

559
    if(m_sliderAction->trackPositionSlider())
560 561
        m_sliderAction->trackPositionSlider()->setMediaObject(m_media[0]);

562
    if(m_sliderAction->volumeSlider())
563 564 565 566 567 568 569
        m_sliderAction->volumeSlider()->setAudioOutput(m_output[0]);
}

void PlayerManager::slotUpdateSliders()
{
    m_sliderAction->trackPositionSlider()->setMediaObject(m_media[m_curOutputPath]);
    m_sliderAction->volumeSlider()->setAudioOutput(m_output[m_curOutputPath]);
570

571 572
    disconnect(m_media[1 - m_curOutputPath], 0, this, SLOT(slotUpdateSliders()));
    connect(m_media[1 - m_curOutputPath], SIGNAL(finished()), SLOT(slotFinished()));
573 574
}

575
void PlayerManager::slotUpdateGuiIfStopped()
576
{
577 578 579
    if(m_media[0]->state() == Phonon::StoppedState && m_media[1]->state() == Phonon::StoppedState)
        stop();
}
580

581 582 583
void PlayerManager::crossfadeToFile(const FileHandle &newFile)
{
    int nextOutputPath = 1 - m_curOutputPath;
584

585 586 587
    // Don't need this anymore
    disconnect(m_media[m_curOutputPath], SIGNAL(finished()), this, 0);
    connect(m_media[nextOutputPath], SIGNAL(finished()), SLOT(slotFinished()));
588

589 590 591
    // Wait a couple of seconds and switch slider objects.  (We would simply
    // handle this when finished() is emitted but phonon-gst is buggy).
    QTimer::singleShot(2000, this, SLOT(slotUpdateSliders()));
592

593
    m_fader[nextOutputPath]->setVolume(0.0f);
594

595 596
    m_media[nextOutputPath]->setCurrentSource(newFile.absFilePath());
    m_media[nextOutputPath]->play();
597

598 599
    m_fader[m_curOutputPath]->setVolume(1.0f);
    m_fader[m_curOutputPath]->fadeTo(0.0f, 2000);
600

601
    m_fader[nextOutputPath]->fadeTo(1.0f, 2000);
602

603 604
    m_curOutputPath = nextOutputPath;
}
605

606 607 608 609
void PlayerManager::stopCrossfade()
{
    // According to the Phonon docs, setVolume immediately takes effect,
    // which is "good enough for government work" ;)
610

611 612 613
    // 1 - curOutputPath is the other output path...
    m_fader[m_curOutputPath]->setVolume(1.0f);
    m_fader[1 - m_curOutputPath]->setVolume(0.0f);
614

615
    m_media[1 - m_curOutputPath]->stop();
616 617
}

Michael Pyne's avatar
Michael Pyne committed
618 619 620 621 622 623 624 625 626 627 628
QString PlayerManager::randomPlayMode() const
{
    if(action<KToggleAction>("randomPlay")->isChecked())
        return "Random";
    if(action<KToggleAction>("albumRandomPlay")->isChecked())
        return "AlbumRandom";
    return "NoRandom";
}

void PlayerManager::setRandomPlayMode(const QString &randomMode)
{
629
    if(randomMode.toLower() == "random")
Michael Pyne's avatar
Michael Pyne committed
630
        action<KToggleAction>("randomPlay")->setChecked(true);
631
    if(randomMode.toLower() == "albumrandom")
Michael Pyne's avatar
Michael Pyne committed
632
        action<KToggleAction>("albumRandomPlay")->setChecked(true);
633
    if(randomMode.toLower() == "norandom")
Michael Pyne's avatar
Michael Pyne committed
634 635 636
        action<KToggleAction>("disableRandomPlay")->setChecked(true);
}

637
#include "playermanager.moc"
638

639
// vim: set et sw=4 tw=0 sta: