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