1 | /* |
2 | Copyright (C) 2007-2008 Tanguy Krotoff <tkrotoff@gmail.com> |
3 | Copyright (C) 2008 Lukas Durfina <lukas.durfina@gmail.com> |
4 | Copyright (C) 2009 Fathi Boudra <fabo@kde.org> |
5 | Copyright (C) 2010 Ben Cooksley <sourtooth@gmail.com> |
6 | Copyright (C) 2009-2011 vlc-phonon AUTHORS <kde-multimedia@kde.org> |
7 | Copyright (C) 2010-2021 Harald Sitter <sitter@kde.org> |
8 | |
9 | This library is free software; you can redistribute it and/or |
10 | modify it under the terms of the GNU Lesser General Public |
11 | License as published by the Free Software Foundation; either |
12 | version 2.1 of the License, or (at your option) any later version. |
13 | |
14 | This library is distributed in the hope that it will be useful, |
15 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
17 | Lesser General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Lesser General Public |
20 | License along with this library. If not, see <http://www.gnu.org/licenses/>. |
21 | */ |
22 | |
23 | #include "mediaobject.h" |
24 | |
25 | #include <QtCore/QDir> |
26 | #include <QtCore/QStringBuilder> |
27 | #include <QtCore/QUrl> |
28 | |
29 | #include <phonon/pulsesupport.h> |
30 | |
31 | #include <vlc/libvlc_version.h> |
32 | #include <vlc/vlc.h> |
33 | |
34 | #include "utils/debug.h" |
35 | #include "utils/libvlc.h" |
36 | #include "media.h" |
37 | #include "sinknode.h" |
38 | #include "streamreader.h" |
39 | |
40 | //Time in milliseconds before sending aboutToFinish() signal |
41 | //2 seconds |
42 | static const int ABOUT_TO_FINISH_TIME = 2000; |
43 | |
44 | namespace Phonon { |
45 | namespace VLC { |
46 | |
47 | MediaObject::MediaObject(QObject *parent) |
48 | : QObject(parent) |
49 | , m_nextSource(MediaSource(QUrl())) |
50 | , m_streamReader(0) |
51 | , m_state(Phonon::StoppedState) |
52 | , m_tickInterval(0) |
53 | , m_transitionTime(0) |
54 | , m_media(0) |
55 | { |
56 | qRegisterMetaType<QMultiMap<QString, QString> >(typeName: "QMultiMap<QString, QString>" ); |
57 | |
58 | m_player = new MediaPlayer(this); |
59 | Q_ASSERT(m_player); |
60 | if (!m_player->libvlc_media_player()) |
61 | error() << "libVLC:" << LibVLC::errorMessage(); |
62 | |
63 | // Player signals. |
64 | connect(sender: m_player, SIGNAL(seekableChanged(bool)), receiver: this, SIGNAL(seekableChanged(bool))); |
65 | connect(sender: m_player, SIGNAL(timeChanged(qint64)), receiver: this, SLOT(timeChanged(qint64))); |
66 | connect(sender: m_player, SIGNAL(stateChanged(MediaPlayer::State)), receiver: this, SLOT(updateState(MediaPlayer::State))); |
67 | connect(sender: m_player, SIGNAL(hasVideoChanged(bool)), receiver: this, SLOT(onHasVideoChanged(bool))); |
68 | connect(sender: m_player, SIGNAL(bufferChanged(int)), receiver: this, SLOT(setBufferStatus(int))); |
69 | connect(sender: m_player, SIGNAL(timeChanged(qint64)), receiver: this, SLOT(timeChanged(qint64))); |
70 | |
71 | // Internal Signals. |
72 | connect(asender: this, SIGNAL(moveToNext()), SLOT(moveToNextSource())); |
73 | connect(sender: m_refreshTimer, SIGNAL(timeout()), receiver: this, SLOT(refreshDescriptors())); |
74 | |
75 | resetMembers(); |
76 | } |
77 | |
78 | MediaObject::~MediaObject() |
79 | { |
80 | unloadMedia(); |
81 | // Shutdown the pulseaudio mainloop before the MediaPlayer gets destroyed |
82 | // (it is a child of the MO). There appears to be a peculiar race condition |
83 | // between the pa_thread_mainloop used by VLC and the pa_glib_mainloop used |
84 | // by Phonon's PulseSupport where for a very short time frame after the |
85 | // former was stopped and freed the latter can run and fall over |
86 | // Invalid read from eventfd: Bad file descriptor |
87 | // Code should not be reached at pulsecore/fdsem.c:157, function flush(). Aborting. |
88 | // Since we don't use PulseSupport since VLC 2.2 we can simply force a |
89 | // loop shutdown even when the application isn't about to terminate. |
90 | // The instance gets created again anyway. |
91 | PulseSupport::shutdown(); |
92 | } |
93 | |
94 | void MediaObject::resetMembers() |
95 | { |
96 | // default to -1, so that streams won't break and to comply with the docs (-1 if unknown) |
97 | m_totalTime = -1; |
98 | m_hasVideo = false; |
99 | m_seekpoint = 0; |
100 | |
101 | m_prefinishEmitted = false; |
102 | m_aboutToFinishEmitted = false; |
103 | |
104 | m_lastTick = 0; |
105 | |
106 | m_timesVideoChecked = 0; |
107 | |
108 | m_buffering = false; |
109 | m_stateAfterBuffering = ErrorState; |
110 | |
111 | resetMediaController(); |
112 | |
113 | // Forcefully shutdown plusesupport to prevent crashing between the PS PA glib mainloop |
114 | // and the VLC PA threaded mainloop. See destructor. |
115 | PulseSupport::shutdown(); |
116 | } |
117 | |
118 | void MediaObject::play() |
119 | { |
120 | DEBUG_BLOCK; |
121 | |
122 | switch (m_state) { |
123 | case PlayingState: |
124 | // Do not do anything if we are already playing (as per documentation). |
125 | return; |
126 | case PausedState: |
127 | m_player->resume(); |
128 | break; |
129 | default: |
130 | setupMedia(); |
131 | if (m_player->play()) |
132 | error() << "libVLC:" << LibVLC::errorMessage(); |
133 | break; |
134 | } |
135 | } |
136 | |
137 | void MediaObject::pause() |
138 | { |
139 | DEBUG_BLOCK; |
140 | switch (m_state) { |
141 | case BufferingState: |
142 | case PlayingState: |
143 | m_player->pause(); |
144 | break; |
145 | case PausedState: |
146 | return; |
147 | default: |
148 | debug() << "doing paused play" ; |
149 | setupMedia(); |
150 | m_player->pausedPlay(); |
151 | break; |
152 | } |
153 | } |
154 | |
155 | void MediaObject::stop() |
156 | { |
157 | DEBUG_BLOCK; |
158 | if (m_streamReader) |
159 | m_streamReader->unlock(); |
160 | m_nextSource = MediaSource(QUrl()); |
161 | m_player->stop(); |
162 | } |
163 | |
164 | void MediaObject::seek(qint64 milliseconds) |
165 | { |
166 | DEBUG_BLOCK; |
167 | |
168 | switch (m_state) { |
169 | case PlayingState: |
170 | case PausedState: |
171 | case BufferingState: |
172 | break; |
173 | default: |
174 | // Seeking while not being in a playingish state is cached for later. |
175 | m_seekpoint = milliseconds; |
176 | return; |
177 | } |
178 | |
179 | debug() << "seeking" << milliseconds << "msec" ; |
180 | |
181 | m_player->setTime(milliseconds); |
182 | |
183 | const qint64 time = currentTime(); |
184 | const qint64 total = totalTime(); |
185 | |
186 | // Reset last tick marker so we emit time even after seeking |
187 | if (time < m_lastTick) |
188 | m_lastTick = time; |
189 | if (time < total - m_prefinishMark) |
190 | m_prefinishEmitted = false; |
191 | if (time < total - ABOUT_TO_FINISH_TIME) |
192 | m_aboutToFinishEmitted = false; |
193 | } |
194 | |
195 | void MediaObject::timeChanged(qint64 time) |
196 | { |
197 | const qint64 totalTime = m_totalTime; |
198 | |
199 | switch (m_state) { |
200 | case PlayingState: |
201 | case BufferingState: |
202 | case PausedState: |
203 | emitTick(time); |
204 | default: |
205 | break; |
206 | } |
207 | |
208 | if (m_state == PlayingState || m_state == BufferingState) { // Buffering is concurrent |
209 | if (time >= totalTime - m_prefinishMark) { |
210 | if (!m_prefinishEmitted) { |
211 | m_prefinishEmitted = true; |
212 | emit prefinishMarkReached(msecToEnd: totalTime - time); |
213 | } |
214 | } |
215 | // Note that when the totalTime is <= 0 we cannot calculate any sane delta. |
216 | if (totalTime > 0 && time >= totalTime - ABOUT_TO_FINISH_TIME) |
217 | emitAboutToFinish(); |
218 | } |
219 | } |
220 | |
221 | void MediaObject::emitTick(qint64 time) |
222 | { |
223 | if (m_tickInterval == 0) // Make sure we do not ever emit ticks when deactivated.\] |
224 | return; |
225 | if (time + m_tickInterval >= m_lastTick) { |
226 | m_lastTick = time; |
227 | emit tick(time); |
228 | } |
229 | } |
230 | |
231 | void MediaObject::loadMedia(const QByteArray &mrl) |
232 | { |
233 | DEBUG_BLOCK; |
234 | |
235 | // Initial state is loading, from which we quickly progress to stopped because |
236 | // libvlc does not provide feedback on loading and the media does not get loaded |
237 | // until we play it. |
238 | // FIXME: libvlc should really allow for this as it can cause unexpected delay |
239 | // even though the GUI might indicate that playback should start right away. |
240 | changeState(newState: Phonon::LoadingState); |
241 | |
242 | m_mrl = mrl; |
243 | debug() << "loading encoded:" << m_mrl; |
244 | |
245 | // We do not have a loading state generally speaking, usually the backend |
246 | // is expected to go to loading state and then at some point reach stopped, |
247 | // at which point playback can be started. |
248 | // See state enum documentation for more information. |
249 | changeState(newState: Phonon::StoppedState); |
250 | } |
251 | |
252 | void MediaObject::loadMedia(const QString &mrl) |
253 | { |
254 | loadMedia(mrl: mrl.toUtf8()); |
255 | } |
256 | |
257 | qint32 MediaObject::tickInterval() const |
258 | { |
259 | return m_tickInterval; |
260 | } |
261 | |
262 | /** |
263 | * Supports runtime changes. |
264 | * If the user goes to tick(0) we stop the timer, otherwise we fire it up. |
265 | */ |
266 | void MediaObject::setTickInterval(qint32 interval) |
267 | { |
268 | m_tickInterval = interval; |
269 | } |
270 | |
271 | qint64 MediaObject::currentTime() const |
272 | { |
273 | qint64 time = -1; |
274 | |
275 | switch (state()) { |
276 | case Phonon::PausedState: |
277 | case Phonon::BufferingState: |
278 | case Phonon::PlayingState: |
279 | time = m_player->time(); |
280 | break; |
281 | case Phonon::StoppedState: |
282 | case Phonon::LoadingState: |
283 | time = 0; |
284 | break; |
285 | case Phonon::ErrorState: |
286 | time = -1; |
287 | break; |
288 | } |
289 | |
290 | return time; |
291 | } |
292 | |
293 | Phonon::State MediaObject::state() const |
294 | { |
295 | return m_state; |
296 | } |
297 | |
298 | Phonon::ErrorType MediaObject::errorType() const |
299 | { |
300 | return Phonon::NormalError; |
301 | } |
302 | |
303 | MediaSource MediaObject::source() const |
304 | { |
305 | return m_mediaSource; |
306 | } |
307 | |
308 | void MediaObject::setSource(const MediaSource &source) |
309 | { |
310 | DEBUG_BLOCK; |
311 | |
312 | // Reset previous streamereaders |
313 | if (m_streamReader) { |
314 | m_streamReader->unlock(); |
315 | delete m_streamReader; |
316 | m_streamReader = 0; |
317 | // For streamreaders we exchange the player's seekability with the |
318 | // reader's so here we change it back. |
319 | // Note: the reader auto-disconnects due to destruction. |
320 | connect(sender: m_player, SIGNAL(seekableChanged(bool)), receiver: this, SIGNAL(seekableChanged(bool))); |
321 | } |
322 | |
323 | // Reset previous isScreen flag |
324 | m_isScreen = false; |
325 | |
326 | m_mediaSource = source; |
327 | |
328 | QByteArray url; |
329 | switch (source.type()) { |
330 | case MediaSource::Invalid: |
331 | error() << Q_FUNC_INFO << "MediaSource Type is Invalid:" << source.type(); |
332 | break; |
333 | case MediaSource::Empty: |
334 | error() << Q_FUNC_INFO << "MediaSource is empty." ; |
335 | break; |
336 | case MediaSource::LocalFile: |
337 | case MediaSource::Url: |
338 | debug() << "MediaSource::Url:" << source.url(); |
339 | if (source.url().scheme().isEmpty()) { |
340 | url = "file://" ; |
341 | // QUrl considers url.scheme.isEmpty() == url.isRelative(), |
342 | // so to be sure the url is not actually absolute we just |
343 | // check the first character |
344 | if (!source.url().toString().startsWith(c: '/')) |
345 | url.append(a: QFile::encodeName(fileName: QDir::currentPath()) + '/'); |
346 | } |
347 | url += source.url().toEncoded(); |
348 | loadMedia(mrl: url); |
349 | break; |
350 | case MediaSource::Disc: |
351 | switch (source.discType()) { |
352 | case Phonon::NoDisc: |
353 | error() << Q_FUNC_INFO << "the MediaSource::Disc doesn't specify which one (Phonon::NoDisc)" ; |
354 | return; |
355 | case Phonon::Cd: |
356 | loadMedia(QStringLiteral("cdda://" ) % m_mediaSource.deviceName()); |
357 | break; |
358 | case Phonon::Dvd: |
359 | loadMedia(QStringLiteral("dvd://" ) % m_mediaSource.deviceName()); |
360 | break; |
361 | case Phonon::Vcd: |
362 | loadMedia(QStringLiteral("vcd://" ) % m_mediaSource.deviceName()); |
363 | break; |
364 | case Phonon::BluRay: |
365 | loadMedia(QStringLiteral("bluray://" ) % m_mediaSource.deviceName()); |
366 | break; |
367 | } |
368 | break; |
369 | case MediaSource::CaptureDevice: { |
370 | QByteArray driverName; |
371 | QString deviceName; |
372 | |
373 | if (source.deviceAccessList().isEmpty()) { |
374 | error() << Q_FUNC_INFO << "No device access list for this capture device" ; |
375 | break; |
376 | } |
377 | |
378 | // TODO try every device in the access list until it works, not just the first one |
379 | driverName = source.deviceAccessList().first().first; |
380 | deviceName = source.deviceAccessList().first().second; |
381 | |
382 | if (driverName == QByteArray("v4l2" )) { |
383 | loadMedia(QStringLiteral("v4l2://" ) % deviceName); |
384 | } else if (driverName == QByteArray("alsa" )) { |
385 | /* |
386 | * Replace "default" and "plughw" and "x-phonon" with "hw" for capture device names, because |
387 | * VLC does not want to open them when using default instead of hw. |
388 | * plughw also does not work. |
389 | * |
390 | * TODO investigate what happens |
391 | */ |
392 | if (deviceName.startsWith(s: QLatin1String("default" ))) { |
393 | deviceName.replace(i: 0, len: 7, after: "hw" ); |
394 | } |
395 | if (deviceName.startsWith(s: QLatin1String("plughw" ))) { |
396 | deviceName.replace(i: 0, len: 6, after: "hw" ); |
397 | } |
398 | if (deviceName.startsWith(s: QLatin1String("x-phonon" ))) { |
399 | deviceName.replace(i: 0, len: 8, after: "hw" ); |
400 | } |
401 | |
402 | loadMedia(QStringLiteral("alsa://" ) % deviceName); |
403 | } else if (driverName == "screen" ) { |
404 | loadMedia(QStringLiteral("screen://" ) % deviceName); |
405 | |
406 | // Set the isScreen flag needed to add extra options in playInternal |
407 | m_isScreen = true; |
408 | } else { |
409 | error() << Q_FUNC_INFO << "Unsupported MediaSource::CaptureDevice:" << driverName; |
410 | break; |
411 | } |
412 | break; |
413 | } |
414 | case MediaSource::Stream: |
415 | m_streamReader = new StreamReader(this); |
416 | // LibVLC refuses to emit seekability as it does a try-and-seek approach |
417 | // to work around this we exchange the player's seekability signal |
418 | // for the readers |
419 | // https://bugs.kde.org/show_bug.cgi?id=293012 |
420 | connect(sender: m_streamReader, SIGNAL(streamSeekableChanged(bool)), receiver: this, SIGNAL(seekableChanged(bool))); |
421 | disconnect(sender: m_player, SIGNAL(seekableChanged(bool)), receiver: this, SIGNAL(seekableChanged(bool))); |
422 | // Only connect now to avoid seekability detection before we are connected. |
423 | m_streamReader->connectToSource(mediaSource: source); |
424 | loadMedia(mrl: QByteArray("imem://" )); |
425 | break; |
426 | } |
427 | |
428 | debug() << "Sending currentSourceChanged" ; |
429 | emit currentSourceChanged(newSource: m_mediaSource); |
430 | } |
431 | |
432 | void MediaObject::setNextSource(const MediaSource &source) |
433 | { |
434 | DEBUG_BLOCK; |
435 | debug() << source.url(); |
436 | m_nextSource = source; |
437 | // This function is not ever called by the consumer but only libphonon. |
438 | // Furthermore libphonon only calls this function in its aboutToFinish slot, |
439 | // iff sources are already in the queue. In case our aboutToFinish was too |
440 | // late we may already be stopped when the slot gets activated. |
441 | // Therefore we need to make sure that we move to the next source iff |
442 | // this function is called when we are in stoppedstate. |
443 | if (m_state == StoppedState) |
444 | moveToNext(); |
445 | } |
446 | |
447 | qint32 MediaObject::prefinishMark() const |
448 | { |
449 | return m_prefinishMark; |
450 | } |
451 | |
452 | void MediaObject::setPrefinishMark(qint32 msecToEnd) |
453 | { |
454 | m_prefinishMark = msecToEnd; |
455 | if (currentTime() < totalTime() - m_prefinishMark) { |
456 | // Not about to finish |
457 | m_prefinishEmitted = false; |
458 | } |
459 | } |
460 | |
461 | qint32 MediaObject::transitionTime() const |
462 | { |
463 | return m_transitionTime; |
464 | } |
465 | |
466 | void MediaObject::setTransitionTime(qint32 time) |
467 | { |
468 | m_transitionTime = time; |
469 | } |
470 | |
471 | void MediaObject::emitAboutToFinish() |
472 | { |
473 | if (!m_aboutToFinishEmitted) { |
474 | // Track is about to finish |
475 | m_aboutToFinishEmitted = true; |
476 | emit aboutToFinish(); |
477 | } |
478 | } |
479 | |
480 | // State changes are force queued by libphonon. |
481 | void MediaObject::changeState(Phonon::State newState) |
482 | { |
483 | DEBUG_BLOCK; |
484 | |
485 | // State not changed |
486 | if (newState == m_state) |
487 | return; |
488 | |
489 | debug() << m_state << "-->" << newState; |
490 | |
491 | #ifdef __GNUC__ |
492 | #warning do we actually need m_seekpoint? if a consumer seeks before playing state that is their problem?! |
493 | #endif |
494 | // Workaround that seeking needs to work before the file is being played... |
495 | // We store seeks and apply them when going to seek (or discard them on reset). |
496 | if (newState == PlayingState) { |
497 | if (m_seekpoint != 0) { |
498 | seek(milliseconds: m_seekpoint); |
499 | m_seekpoint = 0; |
500 | } |
501 | } |
502 | |
503 | // State changed |
504 | Phonon::State previousState = m_state; |
505 | m_state = newState; |
506 | emit stateChanged(newState: m_state, oldState: previousState); |
507 | } |
508 | |
509 | void MediaObject::moveToNextSource() |
510 | { |
511 | DEBUG_BLOCK; |
512 | |
513 | setSource(m_nextSource); |
514 | |
515 | // The consumer may set an invalid source as final source to force a |
516 | // queued stop, regardless of how fast the consumer is at actually calling |
517 | // stop. Such a source must not cause an actual move (moving ~= state |
518 | // changes towards playing) but instead we only set the source to reflect |
519 | // that we got the setNextSource call. |
520 | if (hasNextTrack()) |
521 | play(); |
522 | |
523 | m_nextSource = MediaSource(QUrl()); |
524 | } |
525 | |
526 | inline bool MediaObject::hasNextTrack() |
527 | { |
528 | return m_nextSource.type() != MediaSource::Invalid && m_nextSource.type() != MediaSource::Empty; |
529 | } |
530 | |
531 | inline void MediaObject::unloadMedia() |
532 | { |
533 | if (m_media) { |
534 | m_media->disconnect(receiver: this); |
535 | m_media->deleteLater(); |
536 | m_media = 0; |
537 | } |
538 | } |
539 | |
540 | void MediaObject::setupMedia() |
541 | { |
542 | DEBUG_BLOCK; |
543 | |
544 | unloadMedia(); |
545 | resetMembers(); |
546 | |
547 | // Create a media with the given MRL |
548 | m_media = new Media(m_mrl, this); |
549 | |
550 | if (m_isScreen) { |
551 | m_media->addOption(option: QLatin1String("screen-fps=24.0" )); |
552 | m_media->addOption(option: QLatin1String("screen-caching=300" )); |
553 | } |
554 | |
555 | if (source().discType() == Cd && m_currentTitle > 0) |
556 | m_media->setCdTrack(m_currentTitle); |
557 | |
558 | if (m_streamReader) |
559 | // StreamReader is no sink but a source, for this we have no concept right now |
560 | // also we do not need one since the reader is the only source we have. |
561 | // Consequently we need to manually tell the StreamReader to attach to the Media. |
562 | m_streamReader->addToMedia(media: m_media); |
563 | |
564 | if (!m_subtitleAutodetect) |
565 | m_media->addOption(option: QLatin1String(":no-sub-autodetect-file" )); |
566 | |
567 | if (m_subtitleEncoding != QLatin1String("UTF-8" )) // utf8 is phonon default, so let vlc handle it |
568 | m_media->addOption(option: QLatin1String(":subsdec-encoding=" ), argument: m_subtitleEncoding); |
569 | |
570 | if (!m_subtitleFontChanged) // Update font settings |
571 | m_subtitleFont = QFont(); |
572 | |
573 | #ifdef __GNUC__ |
574 | #warning freetype module is not working as expected - font api not working |
575 | #endif |
576 | // BUG: VLC's freetype module doesn't pick up per-media options |
577 | // vlc -vvvv --freetype-font="Comic Sans MS" multiple_sub_sample.mkv :freetype-font=Arial |
578 | // https://trac.videolan.org/vlc/ticket/9797 |
579 | m_media->addOption(option: QLatin1String(":freetype-font=" ), argument: m_subtitleFont.family()); |
580 | m_media->addOption(option: QLatin1String(":freetype-fontsize=" ), functionPtr: m_subtitleFont.pointSize()); |
581 | if (m_subtitleFont.bold()) |
582 | m_media->addOption(option: QLatin1String(":freetype-bold" )); |
583 | else |
584 | m_media->addOption(option: QLatin1String(":no-freetype-bold" )); |
585 | |
586 | foreach (SinkNode *sink, m_sinks) { |
587 | sink->addToMedia(media: m_media); |
588 | } |
589 | |
590 | // Connect to Media signals. Disconnection is done at unloading. |
591 | connect(sender: m_media, SIGNAL(durationChanged(qint64)), |
592 | receiver: this, SLOT(updateDuration(qint64))); |
593 | connect(sender: m_media, SIGNAL(metaDataChanged()), |
594 | receiver: this, SLOT(updateMetaData())); |
595 | |
596 | // Update available audio channels/subtitles/angles/chapters/etc... |
597 | // i.e everything from MediaController |
598 | // There is no audio channel/subtitle/angle/chapter events inside libvlc |
599 | // so let's send our own events... |
600 | // This will reset the GUI |
601 | resetMediaController(); |
602 | |
603 | // Play |
604 | m_player->setMedia(m_media); |
605 | } |
606 | |
607 | QString MediaObject::errorString() const |
608 | { |
609 | return libvlc_errmsg(); |
610 | } |
611 | |
612 | bool MediaObject::hasVideo() const |
613 | { |
614 | // Cached: sometimes 4.0.0-dev sends the vout event but then |
615 | // has_vout is still false. Guard against this by simply always reporting |
616 | // the last hasVideoChanged value. If that is off we can still drop into |
617 | // libvlc in case it changed meanwhile. |
618 | return m_hasVideo || m_player->hasVideoOutput(); |
619 | } |
620 | |
621 | bool MediaObject::isSeekable() const |
622 | { |
623 | if (m_streamReader) |
624 | return m_streamReader->streamSeekable(); |
625 | return m_player->isSeekable(); |
626 | } |
627 | |
628 | void MediaObject::updateDuration(qint64 newDuration) |
629 | { |
630 | // This here cache is needed because we need to provide -1 as totalTime() |
631 | // for as long as we do not get a proper update through this slot. |
632 | // VLC reports -1 with no media but 0 if it does not know the duration, so |
633 | // apps that assume 0 = unknown get screwed if they query too early. |
634 | // http://bugs.tomahawk-player.org/browse/TWK-1029 |
635 | m_totalTime = newDuration; |
636 | emit totalTimeChanged(newTotalTime: m_totalTime); |
637 | } |
638 | |
639 | void MediaObject::updateMetaData() |
640 | { |
641 | QMultiMap<QString, QString> metaDataMap; |
642 | |
643 | const QString artist = m_media->meta(meta: libvlc_meta_Artist); |
644 | const QString title = m_media->meta(meta: libvlc_meta_Title); |
645 | const QString nowPlaying = m_media->meta(meta: libvlc_meta_NowPlaying); |
646 | |
647 | // Streams sometimes have the artist and title munged in nowplaying. |
648 | // With ALBUM = Title and TITLE = NowPlaying it will still show up nicely in Amarok. |
649 | if (artist.isEmpty() && !nowPlaying.isEmpty()) { |
650 | metaDataMap.insert(key: QLatin1String("ALBUM" ), value: title); |
651 | metaDataMap.insert(key: QLatin1String("TITLE" ), value: nowPlaying); |
652 | } else { |
653 | metaDataMap.insert(key: QLatin1String("ALBUM" ), value: m_media->meta(meta: libvlc_meta_Album)); |
654 | metaDataMap.insert(key: QLatin1String("TITLE" ), value: title); |
655 | } |
656 | |
657 | metaDataMap.insert(key: QLatin1String("ARTIST" ), value: artist); |
658 | metaDataMap.insert(key: QLatin1String("DATE" ), value: m_media->meta(meta: libvlc_meta_Date)); |
659 | metaDataMap.insert(key: QLatin1String("GENRE" ), value: m_media->meta(meta: libvlc_meta_Genre)); |
660 | metaDataMap.insert(key: QLatin1String("TRACKNUMBER" ), value: m_media->meta(meta: libvlc_meta_TrackNumber)); |
661 | metaDataMap.insert(key: QLatin1String("DESCRIPTION" ), value: m_media->meta(meta: libvlc_meta_Description)); |
662 | metaDataMap.insert(key: QLatin1String("COPYRIGHT" ), value: m_media->meta(meta: libvlc_meta_Copyright)); |
663 | metaDataMap.insert(key: QLatin1String("URL" ), value: m_media->meta(meta: libvlc_meta_URL)); |
664 | metaDataMap.insert(key: QLatin1String("ENCODEDBY" ), value: m_media->meta(meta: libvlc_meta_EncodedBy)); |
665 | |
666 | if (metaDataMap == m_vlcMetaData) { |
667 | // No need to issue any change, the data is the same |
668 | return; |
669 | } |
670 | m_vlcMetaData = metaDataMap; |
671 | |
672 | emit metaDataChanged(metaData: metaDataMap); |
673 | } |
674 | |
675 | void MediaObject::updateState(MediaPlayer::State state) |
676 | { |
677 | DEBUG_BLOCK; |
678 | debug() << state; |
679 | debug() << "attempted autoplay?" << m_attemptingAutoplay; |
680 | |
681 | if (m_attemptingAutoplay) { |
682 | switch (state) { |
683 | case MediaPlayer::PlayingState: |
684 | case MediaPlayer::PausedState: |
685 | m_attemptingAutoplay = false; |
686 | break; |
687 | case MediaPlayer::ErrorState: |
688 | debug() << "autoplay failed, must be end of media." ; |
689 | // The error should not be reflected to the consumer. So we swap it |
690 | // for finished() which is actually what is happening here. |
691 | // Or so we think ;) |
692 | state = MediaPlayer::EndedState; |
693 | --m_currentTitle; |
694 | break; |
695 | default: |
696 | debug() << "not handling as part of autplay:" << state; |
697 | break; |
698 | } |
699 | } |
700 | |
701 | switch (state) { |
702 | case MediaPlayer::NoState: |
703 | changeState(newState: LoadingState); |
704 | break; |
705 | case MediaPlayer::OpeningState: |
706 | changeState(newState: LoadingState); |
707 | break; |
708 | case MediaPlayer::BufferingState: |
709 | changeState(newState: BufferingState); |
710 | break; |
711 | case MediaPlayer::PlayingState: |
712 | changeState(newState: PlayingState); |
713 | break; |
714 | case MediaPlayer::PausedState: |
715 | changeState(newState: PausedState); |
716 | break; |
717 | case MediaPlayer::StoppedState: |
718 | changeState(newState: StoppedState); |
719 | break; |
720 | case MediaPlayer::EndedState: |
721 | if (hasNextTrack()) { |
722 | moveToNextSource(); |
723 | } else if (source().discType() == Cd && m_autoPlayTitles && !m_attemptingAutoplay) { |
724 | debug() << "trying to simulate autoplay" ; |
725 | m_attemptingAutoplay = true; |
726 | m_player->setCdTrack(++m_currentTitle); |
727 | } else { |
728 | m_attemptingAutoplay = false; |
729 | emitAboutToFinish(); |
730 | emit finished(); |
731 | changeState(newState: StoppedState); |
732 | } |
733 | break; |
734 | case MediaPlayer::ErrorState: |
735 | debug() << errorString(); |
736 | emitAboutToFinish(); |
737 | emit finished(); |
738 | changeState(newState: ErrorState); |
739 | break; |
740 | } |
741 | |
742 | if (m_buffering) { |
743 | switch (state) { |
744 | case MediaPlayer::BufferingState: |
745 | break; |
746 | case MediaPlayer::PlayingState: |
747 | debug() << "Restoring buffering state after state change to Playing" ; |
748 | changeState(newState: BufferingState); |
749 | m_stateAfterBuffering = PlayingState; |
750 | break; |
751 | case MediaPlayer::PausedState: |
752 | debug() << "Restoring buffering state after state change to Paused" ; |
753 | changeState(newState: BufferingState); |
754 | m_stateAfterBuffering = PausedState; |
755 | break; |
756 | default: |
757 | debug() << "Buffering aborted!" ; |
758 | m_buffering = false; |
759 | break; |
760 | } |
761 | } |
762 | |
763 | return; |
764 | } |
765 | |
766 | void MediaObject::onHasVideoChanged(bool hasVideo) |
767 | { |
768 | DEBUG_BLOCK; |
769 | if (m_hasVideo != hasVideo) { |
770 | m_hasVideo = hasVideo; |
771 | emit hasVideoChanged(b_has_video: m_hasVideo); |
772 | } else { |
773 | // We can simply return if we are have the appropriate caching already. |
774 | // Otherwise we'd do pointless rescans of mediacontroller stuff. |
775 | // MC and MO are force-reset on media changes anyway. |
776 | return; |
777 | } |
778 | |
779 | refreshDescriptors(); |
780 | } |
781 | |
782 | void MediaObject::setBufferStatus(int percent) |
783 | { |
784 | // VLC does not have a buffering state (surprise!) but instead only sends the |
785 | // event (surprise!). Hence we need to simulate the state change. |
786 | // Problem with BufferingState is that it is actually concurrent to Playing or Paused |
787 | // meaning that while you are buffering you can also pause, thus triggering |
788 | // a state change to paused. To handle this we let updateState change the |
789 | // state accordingly (as we need to allow the UI to update itself, and |
790 | // immediately after that we change back to buffering again. |
791 | // This loop can only be interrupted by a state change to !Playing & !Paused |
792 | // or by reaching a 100 % buffer caching (i.e. full cache). |
793 | |
794 | m_buffering = true; |
795 | if (m_state != BufferingState) { |
796 | m_stateAfterBuffering = m_state; |
797 | changeState(newState: BufferingState); |
798 | } |
799 | |
800 | emit bufferStatus(percentFilled: percent); |
801 | |
802 | // Transit to actual state only after emission so the signal is still |
803 | // delivered while in BufferingState. |
804 | if (percent >= 100) { // http://trac.videolan.org/vlc/ticket/5277 |
805 | m_buffering = false; |
806 | changeState(newState: m_stateAfterBuffering); |
807 | } |
808 | } |
809 | |
810 | void MediaObject::refreshDescriptors() |
811 | { |
812 | if (m_player->titleCount() > 0) |
813 | refreshTitles(); |
814 | |
815 | if (hasVideo()) { |
816 | refreshAudioChannels(); |
817 | refreshSubtitles(); |
818 | |
819 | if (m_player->videoChapterCount() > 0) |
820 | refreshChapters(title: m_player->title()); |
821 | } |
822 | } |
823 | |
824 | qint64 MediaObject::totalTime() const |
825 | { |
826 | return m_totalTime; |
827 | } |
828 | |
829 | void MediaObject::addSink(SinkNode *node) |
830 | { |
831 | Q_ASSERT(!m_sinks.contains(node)); |
832 | m_sinks.append(t: node); |
833 | } |
834 | |
835 | void MediaObject::removeSink(SinkNode *node) |
836 | { |
837 | Q_ASSERT(node); |
838 | m_sinks.removeAll(t: node); |
839 | } |
840 | |
841 | } // namespace VLC |
842 | } // namespace Phonon |
843 | |