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.
This commit is contained in:
Max Kellermann 2020-10-01 15:29:13 +02:00
parent b0596291a8
commit 4cb5e69811
7 changed files with 106 additions and 1 deletions

View File

@ -359,6 +359,9 @@ AudioOutputControl::LockPauseAsync() noexcept
mixer_auto_close()) */
mixer_auto_close(output->mixer);
if (output)
output->Interrupt();
const std::lock_guard<Mutex> protect(mutex);
assert(allow_play);
@ -379,6 +382,9 @@ AudioOutputControl::LockDrainAsync() noexcept
void
AudioOutputControl::LockCancelAsync() noexcept
{
if (output)
output->Interrupt();
const std::lock_guard<Mutex> 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<Mutex> lock(mutex);
CloseWait(lock);
}
@ -434,6 +445,9 @@ void
AudioOutputControl::BeginDestroy() noexcept
{
if (thread.IsDefined()) {
if (output)
output->Interrupt();
const std::lock_guard<Mutex> protect(mutex);
if (!killed) {
killed = true;

View File

@ -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

29
src/output/Error.hxx Normal file
View File

@ -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

View File

@ -183,6 +183,12 @@ FilteredAudioOutput::Drain()
output->Drain();
}
void
FilteredAudioOutput::Interrupt() noexcept
{
output->Interrupt();
}
void
FilteredAudioOutput::Cancel() noexcept
{

View File

@ -216,6 +216,8 @@ public:
*/
void CloseSoftwareMixer() noexcept;
void Interrupt() noexcept;
gcc_pure
std::chrono::steady_clock::duration Delay() noexcept;

View File

@ -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
*

View File

@ -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<Mutex> &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<Mutex> &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<Mutex> &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) {