1 /* This file is part of Clementine.
2 Copyright 2009-2012, David Sansome <me@davidsansome.com>
3 Copyright 2010-2011, 2014, Arnaud Bienner <arnaud.bienner@gmail.com>
4 Copyright 2010-2012, 2014, John Maguire <john.maguire@gmail.com>
5 Copyright 2011, Paweł Bara <keirangtp@gmail.com>
6 Copyright 2011, Andrea Decorte <adecorte@gmail.com>
7 Copyright 2012, Anand <anandtp@live.in>
8 Copyright 2012, Arash Abedinzadeh <arash.abedinzadeh@gmail.com>
9 Copyright 2013, Andreas <asfa194@gmail.com>
10 Copyright 2013, Kevin Cox <kevincox.ca@gmail.com>
11 Copyright 2014, Mark Furneaux <mark@romaco.ca>
12 Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
13
14 Clementine is free software: you can redistribute it and/or modify
15 it under the terms of the GNU General Public License as published by
16 the Free Software Foundation, either version 3 of the License, or
17 (at your option) any later version.
18
19 Clementine is distributed in the hope that it will be useful,
20 but WITHOUT ANY WARRANTY; without even the implied warranty of
21 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 GNU General Public License for more details.
23
24 You should have received a copy of the GNU General Public License
25 along with Clementine. If not, see <http://www.gnu.org/licenses/>.
26 */
27
28 #include "player.h"
29
30 #include <memory>
31
32 #include <QSettings>
33 #include <QSortFilterProxyModel>
34 #include <QtDebug>
35 #include <QtConcurrentRun>
36
37 #include "config.h"
38 #include "core/application.h"
39 #include "core/logging.h"
40 #include "core/urlhandler.h"
41 #include "engines/enginebase.h"
42 #include "engines/gstengine.h"
43 #include "library/librarybackend.h"
44 #include "playlist/playlist.h"
45 #include "playlist/playlistitem.h"
46 #include "playlist/playlistmanager.h"
47
48 #ifdef HAVE_LIBLASTFM
49 #include "internet/lastfm/lastfmservice.h"
50 #endif
51
52 using std::shared_ptr;
53
54 const char* Player::kSettingsGroup = "Player";
55
Player(Application * app,QObject * parent)56 Player::Player(Application* app, QObject* parent)
57 : PlayerInterface(parent),
58 app_(app),
59 lastfm_(nullptr),
60 engine_(new GstEngine(app_->task_manager())),
61 stream_change_type_(Engine::First),
62 last_state_(Engine::Empty),
63 nb_errors_received_(0),
64 volume_before_mute_(50),
65 last_pressed_previous_(QDateTime::currentDateTime()),
66 menu_previousmode_(PreviousBehaviour_DontRestart),
67 seek_step_sec_(10) {
68 settings_.beginGroup("Player");
69
70 SetVolume(settings_.value("volume", 50).toInt());
71
72 connect(engine_.get(), SIGNAL(Error(QString)), SIGNAL(Error(QString)));
73
74 connect(engine_.get(), SIGNAL(ValidSongRequested(QUrl)),
75 SLOT(ValidSongRequested(QUrl)));
76 connect(engine_.get(), SIGNAL(InvalidSongRequested(QUrl)),
77 SLOT(InvalidSongRequested(QUrl)));
78 }
79
~Player()80 Player::~Player() {}
81
Init()82 void Player::Init() {
83 if (!engine_->Init()) qFatal("Error initialising audio engine");
84
85 connect(engine_.get(), SIGNAL(StateChanged(Engine::State)),
86 SLOT(EngineStateChanged(Engine::State)));
87 connect(engine_.get(), SIGNAL(TrackAboutToEnd()), SLOT(TrackAboutToEnd()));
88 connect(engine_.get(), SIGNAL(TrackEnded()), SLOT(TrackEnded()));
89 connect(engine_.get(), SIGNAL(MetaData(Engine::SimpleMetaBundle)),
90 SLOT(EngineMetadataReceived(Engine::SimpleMetaBundle)));
91
92 engine_->SetVolume(settings_.value("volume", 50).toInt());
93
94 ReloadSettings();
95
96 #ifdef HAVE_LIBLASTFM
97 lastfm_ = app_->scrobbler();
98 #endif
99 }
100
ReloadSettings()101 void Player::ReloadSettings() {
102 QSettings s;
103 s.beginGroup(kSettingsGroup);
104
105 menu_previousmode_ = PreviousBehaviour(
106 s.value("menu_previousmode", PreviousBehaviour_DontRestart).toInt());
107
108 seek_step_sec_ = s.value("seek_step_sec", 10).toInt();
109
110 s.endGroup();
111
112 engine_->ReloadSettings();
113 }
114
HandleLoadResult(const UrlHandler::LoadResult & result)115 void Player::HandleLoadResult(const UrlHandler::LoadResult& result) {
116 // Might've been an async load, so check we're still on the same item
117 shared_ptr<PlaylistItem> item = app_->playlist_manager()->active()->current_item();
118 if (!item) {
119 loading_async_ = QUrl();
120 return;
121 }
122
123 if (item->Url() != result.original_url_) return;
124
125 switch (result.type_) {
126 case UrlHandler::LoadResult::NoMoreTracks:
127 qLog(Debug) << "URL handler for" << result.original_url_
128 << "said no more tracks";
129
130 loading_async_ = QUrl();
131 NextItem(stream_change_type_);
132 break;
133
134 case UrlHandler::LoadResult::TrackAvailable: {
135 qLog(Debug) << "URL handler for" << result.original_url_ << "returned"
136 << result.media_url_;
137
138 // If there was no length info in song's metadata, use the one provided by
139 // URL handler, if there is one
140 if (item->Metadata().length_nanosec() <= 0 &&
141 result.length_nanosec_ != -1) {
142 Song song = item->Metadata();
143 song.set_length_nanosec(result.length_nanosec_);
144 item->SetTemporaryMetadata(song);
145 app_->playlist_manager()->active()->InformOfCurrentSongChange();
146 }
147 engine_->Play(
148 result.media_url_, stream_change_type_, item->Metadata().has_cue(),
149 item->Metadata().beginning_nanosec(), item->Metadata().end_nanosec());
150
151 current_item_ = item;
152 loading_async_ = QUrl();
153 break;
154 }
155
156 case UrlHandler::LoadResult::WillLoadAsynchronously:
157 qLog(Debug) << "URL handler for" << result.original_url_
158 << "is loading asynchronously";
159
160 // We'll get called again later with either NoMoreTracks or TrackAvailable
161 loading_async_ = result.original_url_;
162 break;
163 }
164 }
165
Next()166 void Player::Next() { NextInternal(Engine::Manual); }
167
NextInternal(Engine::TrackChangeFlags change)168 void Player::NextInternal(Engine::TrackChangeFlags change) {
169 if (HandleStopAfter()) return;
170
171 if (app_->playlist_manager()->active()->current_item()) {
172 const QUrl url = app_->playlist_manager()->active()->current_item()->Url();
173
174 if (url_handlers_.contains(url.scheme())) {
175 // The next track is already being loaded
176 if (url == loading_async_) return;
177
178 stream_change_type_ = change;
179 HandleLoadResult(url_handlers_[url.scheme()]->LoadNext(url));
180 return;
181 }
182 }
183
184 NextItem(change);
185 }
186
NextItem(Engine::TrackChangeFlags change)187 void Player::NextItem(Engine::TrackChangeFlags change) {
188 Playlist* active_playlist = app_->playlist_manager()->active();
189
190 // If we received too many errors in auto change, with repeat enabled, we stop
191 if (change == Engine::Auto) {
192 const PlaylistSequence::RepeatMode repeat_mode =
193 active_playlist->sequence()->repeat_mode();
194 if (repeat_mode != PlaylistSequence::Repeat_Off) {
195 if ((repeat_mode == PlaylistSequence::Repeat_Track &&
196 nb_errors_received_ >= 3) ||
197 (nb_errors_received_ >=
198 app_->playlist_manager()->active()->proxy()->rowCount())) {
199 // We received too many "Error" state changes: probably looping over a
200 // playlist which contains only unavailable elements: stop now.
201 nb_errors_received_ = 0;
202 Stop();
203 return;
204 }
205 }
206 }
207
208 // Manual track changes override "Repeat track"
209 const bool ignore_repeat_track = change & Engine::Manual;
210
211 int i = active_playlist->next_row(ignore_repeat_track);
212 if (i == -1) {
213 app_->playlist_manager()->active()->set_current_row(i);
214 emit PlaylistFinished();
215 Stop();
216 return;
217 }
218
219 PlayAt(i, change, false);
220 }
221
HandleStopAfter()222 bool Player::HandleStopAfter() {
223 if (app_->playlist_manager()->active()->stop_after_current()) {
224 // Find what the next track would've been, and mark that one as current
225 // so it plays next time the user presses Play.
226 const int next_row = app_->playlist_manager()->active()->next_row();
227 if (next_row != -1) {
228 app_->playlist_manager()->active()->set_current_row(next_row, true);
229 }
230
231 app_->playlist_manager()->active()->StopAfter(-1);
232
233 Stop(true);
234 return true;
235 }
236 return false;
237 }
238
TrackEnded()239 void Player::TrackEnded() {
240 if (HandleStopAfter()) return;
241
242 if (current_item_ && current_item_->IsLocalLibraryItem() &&
243 current_item_->Metadata().id() != -1 &&
244 !app_->playlist_manager()->active()->have_incremented_playcount() &&
245 app_->playlist_manager()->active()->get_lastfm_status() !=
246 Playlist::LastFM_Seeked) {
247 // The track finished before its scrobble point (30 seconds), so increment
248 // the play count now.
249 app_->playlist_manager()->library_backend()->IncrementPlayCountAsync(
250 current_item_->Metadata().id());
251 }
252
253 NextInternal(Engine::Auto);
254 }
255
PlayPause()256 void Player::PlayPause() {
257 switch (engine_->state()) {
258 case Engine::Paused:
259 engine_->Unpause();
260 break;
261
262 case Engine::Playing: {
263 // We really shouldn't pause last.fm streams
264 // Stopping seems like a reasonable thing to do (especially on mac where
265 // there
266 // is no media key for stop).
267 if (current_item_->options() & PlaylistItem::PauseDisabled) {
268 Stop();
269 } else {
270 engine_->Pause();
271 }
272 break;
273 }
274
275 case Engine::Empty:
276 case Engine::Error:
277 case Engine::Idle: {
278 app_->playlist_manager()->SetActivePlaylist(
279 app_->playlist_manager()->current_id());
280 if (app_->playlist_manager()->active()->rowCount() == 0) break;
281
282 int i = app_->playlist_manager()->active()->current_row();
283 if (i == -1) i = app_->playlist_manager()->active()->last_played_row();
284 if (i == -1) i = 0;
285
286 PlayAt(i, Engine::First, true);
287 break;
288 }
289 }
290 }
291
RestartOrPrevious()292 void Player::RestartOrPrevious() {
293 if (engine_->position_nanosec() < 8 * kNsecPerSec) return Previous();
294
295 SeekTo(0);
296 }
297
Stop(bool stop_after)298 void Player::Stop(bool stop_after) {
299 engine_->Stop(stop_after);
300 app_->playlist_manager()->active()->set_current_row(-1);
301 current_item_.reset();
302 }
303
StopAfterCurrent()304 void Player::StopAfterCurrent() {
305 app_->playlist_manager()->active()->StopAfter(
306 app_->playlist_manager()->active()->current_row());
307 }
308
PreviousWouldRestartTrack() const309 bool Player::PreviousWouldRestartTrack() const {
310 // Check if it has been over two seconds since previous button was pressed
311 return menu_previousmode_ == PreviousBehaviour_Restart &&
312 last_pressed_previous_.isValid() &&
313 last_pressed_previous_.secsTo(QDateTime::currentDateTime()) >= 2;
314 }
315
Previous()316 void Player::Previous() { PreviousItem(Engine::Manual); }
317
PreviousItem(Engine::TrackChangeFlags change)318 void Player::PreviousItem(Engine::TrackChangeFlags change) {
319 const bool ignore_repeat_track = change & Engine::Manual;
320
321 if (menu_previousmode_ == PreviousBehaviour_Restart) {
322 // Check if it has been over two seconds since previous button was pressed
323 QDateTime now = QDateTime::currentDateTime();
324 if (last_pressed_previous_.isValid() &&
325 last_pressed_previous_.secsTo(now) >= 2) {
326 last_pressed_previous_ = now;
327 PlayAt(app_->playlist_manager()->active()->current_row(), change, false);
328 return;
329 }
330 last_pressed_previous_ = now;
331 }
332
333 int i = app_->playlist_manager()->active()->previous_row(ignore_repeat_track);
334 app_->playlist_manager()->active()->set_current_row(i);
335 if (i == -1) {
336 Stop();
337 PlayAt(i, change, true);
338 return;
339 }
340
341 PlayAt(i, change, false);
342 }
343
EngineStateChanged(Engine::State state)344 void Player::EngineStateChanged(Engine::State state) {
345 if (Engine::Error == state) {
346 nb_errors_received_++;
347 } else {
348 nb_errors_received_ = 0;
349 }
350
351 switch (state) {
352 case Engine::Paused:
353 emit Paused();
354 break;
355 case Engine::Playing:
356 emit Playing();
357 break;
358 case Engine::Error:
359 case Engine::Empty:
360 case Engine::Idle:
361 emit Stopped();
362 break;
363 }
364 last_state_ = state;
365 }
366
SetVolume(int value)367 void Player::SetVolume(int value) {
368 int old_volume = engine_->volume();
369
370 int volume = qBound(0, value, 100);
371 settings_.setValue("volume", volume);
372 engine_->SetVolume(volume);
373
374 if (volume != old_volume) {
375 emit VolumeChanged(volume);
376 }
377 }
378
GetVolume() const379 int Player::GetVolume() const { return engine_->volume(); }
380
PlayAt(int index,Engine::TrackChangeFlags change,bool reshuffle)381 void Player::PlayAt(int index, Engine::TrackChangeFlags change,
382 bool reshuffle) {
383 if (change == Engine::Manual &&
384 engine_->position_nanosec() != engine_->length_nanosec()) {
385 emit TrackSkipped(current_item_);
386 const QUrl& url = current_item_->Url();
387 if (url_handlers_.contains(url.scheme())) {
388 url_handlers_[url.scheme()]->TrackSkipped();
389 }
390 }
391
392 if (current_item_ && app_->playlist_manager()->active()->has_item_at(index) &&
393 current_item_->Metadata().IsOnSameAlbum(
394 app_->playlist_manager()->active()->item_at(index)->Metadata())) {
395 change |= Engine::SameAlbum;
396 }
397
398 if (reshuffle) app_->playlist_manager()->active()->ReshuffleIndices();
399 app_->playlist_manager()->active()->set_current_row(index);
400
401 if (app_->playlist_manager()->active()->current_row() == -1) {
402 // Maybe index didn't exist in the playlist.
403 return;
404 }
405
406 current_item_ = app_->playlist_manager()->active()->current_item();
407 const QUrl url = current_item_->Url();
408
409 if (url_handlers_.contains(url.scheme())) {
410 // It's already loading
411 if (url == loading_async_) return;
412
413 stream_change_type_ = change;
414 HandleLoadResult(url_handlers_[url.scheme()]->StartLoading(url));
415 } else {
416 loading_async_ = QUrl();
417 engine_->Play(current_item_->Url(), change,
418 current_item_->Metadata().has_cue(),
419 current_item_->Metadata().beginning_nanosec(),
420 current_item_->Metadata().end_nanosec());
421
422 #ifdef HAVE_LIBLASTFM
423 if (lastfm_->IsScrobblingEnabled())
424 lastfm_->NowPlaying(current_item_->Metadata());
425 #endif
426 }
427 }
428
CurrentMetadataChanged(const Song & metadata)429 void Player::CurrentMetadataChanged(const Song& metadata) {
430 // those things might have changed (especially when a previously invalid
431 // song was reloaded) so we push the latest version into Engine
432 engine_->RefreshMarkers(metadata.beginning_nanosec(), metadata.end_nanosec());
433
434 #ifdef HAVE_LIBLASTFM
435 lastfm_->NowPlaying(metadata);
436 #endif
437 }
438
SeekTo(int seconds)439 void Player::SeekTo(int seconds) {
440 const qint64 length_nanosec = engine_->length_nanosec();
441
442 // If the length is 0 then either there is no song playing, or the song isn't
443 // seekable.
444 if (length_nanosec <= 0) {
445 return;
446 }
447
448 const qint64 nanosec =
449 qBound(0ll, qint64(seconds) * kNsecPerSec, length_nanosec);
450 engine_->Seek(nanosec);
451
452 // If we seek the track we need to move the scrobble point
453 qLog(Info) << "Track seeked to" << nanosec << "ns - updating scrobble point";
454 app_->playlist_manager()->active()->UpdateScrobblePoint(nanosec);
455
456 emit Seeked(nanosec / 1000);
457 }
458
SeekForward()459 void Player::SeekForward() {
460 SeekTo(engine()->position_nanosec() / kNsecPerSec + seek_step_sec_);
461 }
462
SeekBackward()463 void Player::SeekBackward() {
464 SeekTo(engine()->position_nanosec() / kNsecPerSec - seek_step_sec_);
465 }
466
EngineMetadataReceived(const Engine::SimpleMetaBundle & bundle)467 void Player::EngineMetadataReceived(const Engine::SimpleMetaBundle& bundle) {
468 PlaylistItemPtr item = app_->playlist_manager()->active()->current_item();
469 if (!item) return;
470
471 Engine::SimpleMetaBundle bundle_copy = bundle;
472
473 // Maybe the metadata is from icycast and has "Artist - Title" shoved
474 // together in the title field.
475 const int dash_pos = bundle_copy.title.indexOf('-');
476 if (dash_pos != -1 && bundle_copy.artist.isEmpty()) {
477 // Split on " - " if it exists, otherwise split on "-".
478 const int space_dash_pos = bundle_copy.title.indexOf(" - ");
479 if (space_dash_pos != -1) {
480 bundle_copy.artist = bundle_copy.title.left(space_dash_pos).trimmed();
481 bundle_copy.title = bundle_copy.title.mid(space_dash_pos + 3).trimmed();
482 } else {
483 bundle_copy.artist = bundle_copy.title.left(dash_pos).trimmed();
484 bundle_copy.title = bundle_copy.title.mid(dash_pos + 1).trimmed();
485 }
486 }
487
488 Song song = item->Metadata();
489 song.MergeFromSimpleMetaBundle(bundle_copy);
490
491 // Ignore useless metadata
492 if (song.title().isEmpty() && song.artist().isEmpty()) return;
493
494 app_->playlist_manager()->active()->SetStreamMetadata(item->Url(), song);
495 }
496
GetItemAt(int pos) const497 PlaylistItemPtr Player::GetItemAt(int pos) const {
498 if (pos < 0 || pos >= app_->playlist_manager()->active()->rowCount())
499 return PlaylistItemPtr();
500 return app_->playlist_manager()->active()->item_at(pos);
501 }
502
Mute()503 void Player::Mute() {
504 const int current_volume = engine_->volume();
505
506 if (current_volume == 0) {
507 SetVolume(volume_before_mute_);
508 } else {
509 volume_before_mute_ = current_volume;
510 SetVolume(0);
511 }
512 }
513
Pause()514 void Player::Pause() { engine_->Pause(); }
515
Play()516 void Player::Play() {
517 switch (GetState()) {
518 case Engine::Playing:
519 SeekTo(0);
520 break;
521 case Engine::Paused:
522 engine_->Unpause();
523 break;
524 default:
525 PlayPause();
526 break;
527 }
528 }
529
ShowOSD()530 void Player::ShowOSD() {
531 if (current_item_) emit ForceShowOSD(current_item_->Metadata(), false);
532 }
533
TogglePrettyOSD()534 void Player::TogglePrettyOSD() {
535 if (current_item_) emit ForceShowOSD(current_item_->Metadata(), true);
536 }
537
TrackAboutToEnd()538 void Player::TrackAboutToEnd() {
539 // If the current track was from a URL handler then it might have special
540 // behaviour to queue up a subsequent track. We don't want to preload (and
541 // scrobble) the next item in the playlist if it's just going to be stopped
542 // again immediately after.
543 if (app_->playlist_manager()->active()->current_item()) {
544 const QUrl url = app_->playlist_manager()->active()->current_item()->Url();
545 if (url_handlers_.contains(url.scheme())) {
546 url_handlers_[url.scheme()]->TrackAboutToEnd();
547 return;
548 }
549 }
550
551 const bool has_next_row =
552 app_->playlist_manager()->active()->next_row() != -1;
553 PlaylistItemPtr next_item;
554
555 if (has_next_row) {
556 next_item = app_->playlist_manager()->active()->item_at(
557 app_->playlist_manager()->active()->next_row());
558 }
559
560 if (engine_->is_autocrossfade_enabled()) {
561 // Crossfade is on, so just start playing the next track. The current one
562 // will fade out, and the new one will fade in
563
564 // But, if there's no next track and we don't want to fade out, then do
565 // nothing and just let the track finish to completion.
566 if (!engine_->is_fadeout_enabled() && !has_next_row) return;
567
568 // If the next track is on the same album (or same cue file), and the
569 // user doesn't want to crossfade between tracks on the same album, then
570 // don't do this automatic crossfading.
571 if (engine_->crossfade_same_album() || !has_next_row || !next_item ||
572 !current_item_->Metadata().IsOnSameAlbum(next_item->Metadata())) {
573 TrackEnded();
574 return;
575 }
576 }
577
578 // Crossfade is off, so start preloading the next track so we don't get a
579 // gap between songs.
580 if (!has_next_row || !next_item) return;
581
582 QUrl url = next_item->Url();
583
584 // Get the actual track URL rather than the stream URL.
585 if (url_handlers_.contains(url.scheme())) {
586 UrlHandler::LoadResult result = url_handlers_[url.scheme()]->LoadNext(url);
587 switch (result.type_) {
588 case UrlHandler::LoadResult::NoMoreTracks:
589 return;
590
591 case UrlHandler::LoadResult::WillLoadAsynchronously:
592 loading_async_ = url;
593 return;
594
595 case UrlHandler::LoadResult::TrackAvailable:
596 url = result.media_url_;
597 break;
598 }
599 }
600 engine_->StartPreloading(url, next_item->Metadata().has_cue(),
601 next_item->Metadata().beginning_nanosec(),
602 next_item->Metadata().end_nanosec());
603 }
604
IntroPointReached()605 void Player::IntroPointReached() { NextInternal(Engine::Intro); }
606
ValidSongRequested(const QUrl & url)607 void Player::ValidSongRequested(const QUrl& url) {
608 emit SongChangeRequestProcessed(url, true);
609 }
610
InvalidSongRequested(const QUrl & url)611 void Player::InvalidSongRequested(const QUrl& url) {
612 // first send the notification to others...
613 emit SongChangeRequestProcessed(url, false);
614 // ... and now when our listeners have completed their processing of the
615 // current item we can change the current item by skipping to the next song
616
617 QSettings s;
618 s.beginGroup(kSettingsGroup);
619
620 bool stop_playback = s.value("stop_play_if_fail", 0).toBool();
621 s.endGroup();
622
623 if (stop_playback) {
624 Stop();
625 } else {
626 NextItem(Engine::Auto);
627 }
628 }
629
RegisterUrlHandler(UrlHandler * handler)630 void Player::RegisterUrlHandler(UrlHandler* handler) {
631 const QString scheme = handler->scheme();
632
633 if (url_handlers_.contains(scheme)) {
634 qLog(Warning) << "Tried to register a URL handler for" << scheme
635 << "but one was already registered";
636 return;
637 }
638
639 qLog(Info) << "Registered URL handler for" << scheme;
640 url_handlers_.insert(scheme, handler);
641 connect(handler, SIGNAL(destroyed(QObject*)),
642 SLOT(UrlHandlerDestroyed(QObject*)));
643 connect(handler, SIGNAL(AsyncLoadComplete(UrlHandler::LoadResult)),
644 SLOT(HandleLoadResult(UrlHandler::LoadResult)));
645 }
646
UnregisterUrlHandler(UrlHandler * handler)647 void Player::UnregisterUrlHandler(UrlHandler* handler) {
648 const QString scheme = url_handlers_.key(handler);
649 if (scheme.isEmpty()) {
650 qLog(Warning) << "Tried to unregister a URL handler for"
651 << handler->scheme() << "that wasn't registered";
652 return;
653 }
654
655 qLog(Info) << "Unregistered URL handler for" << scheme;
656 url_handlers_.remove(scheme);
657 disconnect(handler, SIGNAL(destroyed(QObject*)), this,
658 SLOT(UrlHandlerDestroyed(QObject*)));
659 disconnect(handler, SIGNAL(AsyncLoadComplete(UrlHandler::LoadResult)), this,
660 SLOT(HandleLoadResult(UrlHandler::LoadResult)));
661 }
662
HandlerForUrl(const QUrl & url) const663 const UrlHandler* Player::HandlerForUrl(const QUrl& url) const {
664 QMap<QString, UrlHandler*>::const_iterator it =
665 url_handlers_.constFind(url.scheme());
666 if (it == url_handlers_.constEnd()) {
667 return nullptr;
668 }
669 return *it;
670 }
671
UrlHandlerDestroyed(QObject * object)672 void Player::UrlHandlerDestroyed(QObject* object) {
673 UrlHandler* handler = static_cast<UrlHandler*>(object);
674 const QString scheme = url_handlers_.key(handler);
675 if (!scheme.isEmpty()) {
676 url_handlers_.remove(scheme);
677 }
678 }
679