From 4cb5e6981104e6e874b19136e2361f7484edbb97 Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Thu, 1 Oct 2020 15:29:13 +0200 Subject: [PATCH] output/Interface: add virtual method Interrupt() This allows interrupting the output thread (for some plugins which implement this method). This way, operations can be canceled properly, instead of waiting for some external entity. --- src/output/Control.cxx | 14 ++++++++++++++ src/output/Control.hxx | 9 +++++++++ src/output/Error.hxx | 29 +++++++++++++++++++++++++++++ src/output/Filtered.cxx | 6 ++++++ src/output/Filtered.hxx | 2 ++ src/output/Interface.hxx | 29 +++++++++++++++++++++++++++++ src/output/Thread.cxx | 18 +++++++++++++++++- 7 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 src/output/Error.hxx diff --git a/src/output/Control.cxx b/src/output/Control.cxx index e4be4dfa7..fd4a1aa50 100644 --- a/src/output/Control.cxx +++ b/src/output/Control.cxx @@ -359,6 +359,9 @@ AudioOutputControl::LockPauseAsync() noexcept mixer_auto_close()) */ mixer_auto_close(output->mixer); + if (output) + output->Interrupt(); + const std::lock_guard protect(mutex); assert(allow_play); @@ -379,6 +382,9 @@ AudioOutputControl::LockDrainAsync() noexcept void AudioOutputControl::LockCancelAsync() noexcept { + if (output) + output->Interrupt(); + const std::lock_guard protect(mutex); if (IsOpen()) { @@ -403,6 +409,8 @@ AudioOutputControl::LockRelease() noexcept if (!output) return; + output->Interrupt(); + if (output->mixer != nullptr && (!always_on || !output->SupportsPause())) /* the device has no pause mode: close the mixer, @@ -426,6 +434,9 @@ AudioOutputControl::LockCloseWait() noexcept { assert(!open || !fail_timer.IsDefined()); + if (output) + output->Interrupt(); + std::unique_lock lock(mutex); CloseWait(lock); } @@ -434,6 +445,9 @@ void AudioOutputControl::BeginDestroy() noexcept { if (thread.IsDefined()) { + if (output) + output->Interrupt(); + const std::lock_guard protect(mutex); if (!killed) { killed = true; diff --git a/src/output/Control.hxx b/src/output/Control.hxx index c87dc133b..809c65101 100644 --- a/src/output/Control.hxx +++ b/src/output/Control.hxx @@ -196,6 +196,15 @@ class AudioOutputControl { */ bool allow_play = true; + /** + * Was an #AudioOutputInterrupted caught? In this case, + * playback is suspended, and the output thread waits for a + * command. + * + * This field is only valid while the output is open. + */ + bool caught_interrupted; + /** * True while the OutputThread is inside ao_play(). This * means the PlayerThread does not need to wake up the diff --git a/src/output/Error.hxx b/src/output/Error.hxx new file mode 100644 index 000000000..86fc1c297 --- /dev/null +++ b/src/output/Error.hxx @@ -0,0 +1,29 @@ +/* + * Copyright 2003-2020 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_AUDIO_OUTPUT_ERROR_HXX +#define MPD_AUDIO_OUTPUT_ERROR_HXX + +/** + * An exception class that will be thrown by various #AudioOutput + * methods after AudioOutput::Interrupt() has been called. + */ +class AudioOutputInterrupted {}; + +#endif diff --git a/src/output/Filtered.cxx b/src/output/Filtered.cxx index a38add5c4..b3833953e 100644 --- a/src/output/Filtered.cxx +++ b/src/output/Filtered.cxx @@ -183,6 +183,12 @@ FilteredAudioOutput::Drain() output->Drain(); } +void +FilteredAudioOutput::Interrupt() noexcept +{ + output->Interrupt(); +} + void FilteredAudioOutput::Cancel() noexcept { diff --git a/src/output/Filtered.hxx b/src/output/Filtered.hxx index 11a0ac742..7e2413cc9 100644 --- a/src/output/Filtered.hxx +++ b/src/output/Filtered.hxx @@ -216,6 +216,8 @@ public: */ void CloseSoftwareMixer() noexcept; + void Interrupt() noexcept; + gcc_pure std::chrono::steady_clock::duration Delay() noexcept; diff --git a/src/output/Interface.hxx b/src/output/Interface.hxx index a54e23a0b..e39dac0b0 100644 --- a/src/output/Interface.hxx +++ b/src/output/Interface.hxx @@ -126,6 +126,24 @@ public: return false; } + /** + * Interrupt a blocking operation inside the plugin. This + * method will be called from outside the output thread (and + * therefore the method must be thread-safe), to make the + * output thread ready for receiving a command. For example, + * it will be called to prepare for an upcoming Close(), + * Cancel() or Pause() call. + * + * This method can be called any time, even if the output is + * not open or disabled. + * + * Implementations usually send some kind of message/signal to + * the output thread to wake it up and return to the output + * thread loop (e.g. by throwing #AudioOutputInterrupted), + * where the incoming command will be handled and dispatched. + */ + virtual void Interrupt() noexcept {} + /** * Returns a positive number if the output thread shall further * delay the next call to Play() or Pause(), which will happen @@ -142,6 +160,11 @@ public: /** * Display metadata for the next chunk. Optional method, * because not all devices can display metadata. + * + * Throws on error. + * + * May throw #AudioOutputInterrupted after Interrupt() has + * been called. */ virtual void SendTag(const Tag &) {} @@ -151,6 +174,9 @@ public: * * Throws on error. * + * May throw #AudioOutputInterrupted after Interrupt() has + * been called. + * * @return the number of bytes played (must be a multiple of * the frame size) */ @@ -177,6 +203,9 @@ public: * disconnected. Plugins which do not support pausing will * simply be closed, and have to be reopened when unpaused. * + * May throw #AudioOutputInterrupted after Interrupt() has + * been called. + * * @return false on error (output will be closed by caller), * true for continue to pause * diff --git a/src/output/Thread.cxx b/src/output/Thread.cxx index 23b6acaa0..1427e0d32 100644 --- a/src/output/Thread.cxx +++ b/src/output/Thread.cxx @@ -18,6 +18,7 @@ */ #include "Control.hxx" +#include "Error.hxx" #include "Filtered.hxx" #include "Client.hxx" #include "Domain.hxx" @@ -135,6 +136,7 @@ AudioOutputControl::InternalOpen(const AudioFormat in_audio_format, last_error = nullptr; fail_timer.Reset(); + caught_interrupted = false; skip_delay = true; AudioFormat f; @@ -243,6 +245,9 @@ AudioOutputControl::PlayChunk(std::unique_lock &lock) noexcept const ScopeUnlock unlock(mutex); try { output->SendTag(*tag); + } catch (AudioOutputInterrupted) { + caught_interrupted = true; + return false; } catch (...) { FormatError(std::current_exception(), "Failed to send tag to %s", @@ -267,6 +272,9 @@ AudioOutputControl::PlayChunk(std::unique_lock &lock) noexcept nbytes = output->Play(data.data, data.size); assert(nbytes > 0); assert(nbytes <= data.size); + } catch (AudioOutputInterrupted) { + caught_interrupted = true; + return false; } catch (...) { FormatError(std::current_exception(), "Failed to play on %s", GetLogName()); @@ -342,6 +350,7 @@ AudioOutputControl::InternalPause(std::unique_lock &lock) noexcept try { const ScopeUnlock unlock(mutex); success = output->IteratePause(); + } catch (AudioOutputInterrupted) { } catch (...) { FormatError(std::current_exception(), "Failed to pause %s", @@ -425,7 +434,8 @@ AudioOutputControl::Task() noexcept /* no pending command: play (or wait for a command) */ - if (open && allow_play && InternalPlay(lock)) + if (open && allow_play && !caught_interrupted && + InternalPlay(lock)) /* don't wait for an event if there are more chunks in the pipe */ continue; @@ -463,6 +473,8 @@ AudioOutputControl::Task() noexcept break; } + caught_interrupted = false; + InternalPause(lock); break; @@ -475,6 +487,8 @@ AudioOutputControl::Task() noexcept break; } + caught_interrupted = false; + if (always_on) { /* in "always_on" mode, the output is paused instead of being closed; @@ -499,6 +513,8 @@ AudioOutputControl::Task() noexcept break; case Command::CANCEL: + caught_interrupted = false; + source.Cancel(); if (open) {