add "moveoutput" command
This commit is contained in:
parent
ac126ede22
commit
c16233fa74
1
NEWS
1
NEWS
|
@ -3,6 +3,7 @@ ver 0.22 (not yet released)
|
||||||
- "findadd"/"searchadd"/"searchaddpl" support the "sort" and
|
- "findadd"/"searchadd"/"searchaddpl" support the "sort" and
|
||||||
"window" parameters
|
"window" parameters
|
||||||
- add command "readpicture" to download embedded pictures
|
- add command "readpicture" to download embedded pictures
|
||||||
|
- command "moveoutput" moves an output between partitions
|
||||||
* tags
|
* tags
|
||||||
- new tags "Grouping" (for ID3 "TIT1"), "Work" and "Conductor"
|
- new tags "Grouping" (for ID3 "TIT1"), "Work" and "Conductor"
|
||||||
* input
|
* input
|
||||||
|
|
|
@ -1258,6 +1258,9 @@ client is assigned to one partition at a time.
|
||||||
:command:`newpartition {NAME}`
|
:command:`newpartition {NAME}`
|
||||||
Create a new partition.
|
Create a new partition.
|
||||||
|
|
||||||
|
:command:`moveoutput {OUTPUTNAME}`
|
||||||
|
Move an output to the current partition.
|
||||||
|
|
||||||
Audio output devices
|
Audio output devices
|
||||||
====================
|
====================
|
||||||
|
|
||||||
|
|
|
@ -139,6 +139,7 @@ static constexpr struct command commands[] = {
|
||||||
#endif
|
#endif
|
||||||
{ "move", PERMISSION_CONTROL, 2, 2, handle_move },
|
{ "move", PERMISSION_CONTROL, 2, 2, handle_move },
|
||||||
{ "moveid", PERMISSION_CONTROL, 2, 2, handle_moveid },
|
{ "moveid", PERMISSION_CONTROL, 2, 2, handle_moveid },
|
||||||
|
{ "moveoutput", PERMISSION_ADMIN, 1, 1, handle_moveoutput },
|
||||||
{ "newpartition", PERMISSION_ADMIN, 1, 1, handle_newpartition },
|
{ "newpartition", PERMISSION_ADMIN, 1, 1, handle_newpartition },
|
||||||
{ "next", PERMISSION_CONTROL, 0, 0, handle_next },
|
{ "next", PERMISSION_CONTROL, 0, 0, handle_next },
|
||||||
{ "notcommands", PERMISSION_NONE, 0, 0, handle_not_commands },
|
{ "notcommands", PERMISSION_NONE, 0, 0, handle_not_commands },
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
#include "Instance.hxx"
|
#include "Instance.hxx"
|
||||||
#include "Partition.hxx"
|
#include "Partition.hxx"
|
||||||
#include "IdleFlags.hxx"
|
#include "IdleFlags.hxx"
|
||||||
|
#include "output/Filtered.hxx"
|
||||||
#include "client/Client.hxx"
|
#include "client/Client.hxx"
|
||||||
#include "client/Response.hxx"
|
#include "client/Response.hxx"
|
||||||
#include "util/CharUtil.hxx"
|
#include "util/CharUtil.hxx"
|
||||||
|
@ -113,3 +114,44 @@ handle_newpartition(Client &client, Request request, Response &response)
|
||||||
|
|
||||||
return CommandResult::OK;
|
return CommandResult::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CommandResult
|
||||||
|
handle_moveoutput(Client &client, Request request, Response &response)
|
||||||
|
{
|
||||||
|
const char *output_name = request[0];
|
||||||
|
|
||||||
|
auto &dest_partition = client.GetPartition();
|
||||||
|
auto *existing_output = dest_partition.outputs.FindByName(output_name);
|
||||||
|
if (existing_output != nullptr && !existing_output->IsDummy())
|
||||||
|
/* this output is already in the specified partition,
|
||||||
|
so nothing needs to be done */
|
||||||
|
return CommandResult::OK;
|
||||||
|
|
||||||
|
/* find the partition which owns this output currently */
|
||||||
|
auto &instance = client.GetInstance();
|
||||||
|
for (auto &partition : instance.partitions) {
|
||||||
|
if (&partition == &dest_partition)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
auto *output = partition.outputs.FindByName(output_name);
|
||||||
|
if (output == nullptr || output->IsDummy())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const bool was_enabled = output->IsEnabled();
|
||||||
|
|
||||||
|
if (existing_output != nullptr)
|
||||||
|
/* move the output back where it once was */
|
||||||
|
existing_output->ReplaceDummy(output->Steal(),
|
||||||
|
was_enabled);
|
||||||
|
else
|
||||||
|
/* add it to the output list */
|
||||||
|
dest_partition.outputs.Add(output->Steal(),
|
||||||
|
was_enabled);
|
||||||
|
|
||||||
|
instance.EmitIdle(IDLE_OUTPUT);
|
||||||
|
return CommandResult::OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Error(ACK_ERROR_NO_EXIST, "No such output");
|
||||||
|
return CommandResult::ERROR;
|
||||||
|
}
|
||||||
|
|
|
@ -35,4 +35,7 @@ handle_listpartitions(Client &client, Request request, Response &response);
|
||||||
CommandResult
|
CommandResult
|
||||||
handle_newpartition(Client &client, Request request, Response &response);
|
handle_newpartition(Client &client, Request request, Response &response);
|
||||||
|
|
||||||
|
CommandResult
|
||||||
|
handle_moveoutput(Client &client, Request request, Response &response);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
|
|
||||||
#include "Control.hxx"
|
#include "Control.hxx"
|
||||||
#include "Filtered.hxx"
|
#include "Filtered.hxx"
|
||||||
|
#include "Client.hxx"
|
||||||
#include "mixer/MixerControl.hxx"
|
#include "mixer/MixerControl.hxx"
|
||||||
#include "config/Block.hxx"
|
#include "config/Block.hxx"
|
||||||
#include "Log.hxx"
|
#include "Log.hxx"
|
||||||
|
@ -31,7 +32,9 @@ static constexpr PeriodClock::Duration REOPEN_AFTER = std::chrono::seconds(10);
|
||||||
|
|
||||||
AudioOutputControl::AudioOutputControl(std::unique_ptr<FilteredAudioOutput> _output,
|
AudioOutputControl::AudioOutputControl(std::unique_ptr<FilteredAudioOutput> _output,
|
||||||
AudioOutputClient &_client) noexcept
|
AudioOutputClient &_client) noexcept
|
||||||
:output(std::move(_output)), client(_client),
|
:output(std::move(_output)),
|
||||||
|
name(output->GetName()),
|
||||||
|
client(_client),
|
||||||
thread(BIND_THIS_METHOD(Task))
|
thread(BIND_THIS_METHOD(Task))
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -49,40 +52,86 @@ AudioOutputControl::Configure(const ConfigBlock &block)
|
||||||
enabled = block.GetBlockValue("enabled", true);
|
enabled = block.GetBlockValue("enabled", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<FilteredAudioOutput>
|
||||||
|
AudioOutputControl::Steal() noexcept
|
||||||
|
{
|
||||||
|
assert(!IsDummy());
|
||||||
|
|
||||||
|
/* close and disable the output */
|
||||||
|
{
|
||||||
|
std::unique_lock<Mutex> lock(mutex);
|
||||||
|
if (really_enabled && output->SupportsEnableDisable())
|
||||||
|
CommandWait(lock, Command::DISABLE);
|
||||||
|
|
||||||
|
enabled = really_enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* stop the thread */
|
||||||
|
StopThread();
|
||||||
|
|
||||||
|
/* now we can finally remove it */
|
||||||
|
const std::lock_guard<Mutex> protect(mutex);
|
||||||
|
return std::exchange(output, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
AudioOutputControl::ReplaceDummy(std::unique_ptr<FilteredAudioOutput> new_output,
|
||||||
|
bool _enabled) noexcept
|
||||||
|
{
|
||||||
|
assert(IsDummy());
|
||||||
|
assert(new_output);
|
||||||
|
|
||||||
|
{
|
||||||
|
const std::lock_guard<Mutex> protect(mutex);
|
||||||
|
output = std::move(new_output);
|
||||||
|
enabled = _enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
client.ApplyEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
const char *
|
const char *
|
||||||
AudioOutputControl::GetName() const noexcept
|
AudioOutputControl::GetName() const noexcept
|
||||||
{
|
{
|
||||||
return output->GetName();
|
return name.c_str();
|
||||||
}
|
}
|
||||||
|
|
||||||
const char *
|
const char *
|
||||||
AudioOutputControl::GetPluginName() const noexcept
|
AudioOutputControl::GetPluginName() const noexcept
|
||||||
{
|
{
|
||||||
return output->GetPluginName();
|
return output ? output->GetPluginName() : "dummy";
|
||||||
}
|
}
|
||||||
|
|
||||||
const char *
|
const char *
|
||||||
AudioOutputControl::GetLogName() const noexcept
|
AudioOutputControl::GetLogName() const noexcept
|
||||||
{
|
{
|
||||||
|
assert(!IsDummy());
|
||||||
|
|
||||||
return output->GetLogName();
|
return output->GetLogName();
|
||||||
}
|
}
|
||||||
|
|
||||||
Mixer *
|
Mixer *
|
||||||
AudioOutputControl::GetMixer() const noexcept
|
AudioOutputControl::GetMixer() const noexcept
|
||||||
{
|
{
|
||||||
return output->mixer;
|
return output ? output->mixer : nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::map<std::string, std::string>
|
const std::map<std::string, std::string>
|
||||||
AudioOutputControl::GetAttributes() const noexcept
|
AudioOutputControl::GetAttributes() const noexcept
|
||||||
{
|
{
|
||||||
return output->GetAttributes();
|
return output
|
||||||
|
? output->GetAttributes()
|
||||||
|
: std::map<std::string, std::string>{};
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
AudioOutputControl::SetAttribute(std::string &&name, std::string &&value)
|
AudioOutputControl::SetAttribute(std::string &&attribute_name,
|
||||||
|
std::string &&value)
|
||||||
{
|
{
|
||||||
output->SetAttribute(std::move(name), std::move(value));
|
if (!output)
|
||||||
|
throw std::runtime_error("Cannot set attribute on dummy output");
|
||||||
|
|
||||||
|
output->SetAttribute(std::move(attribute_name), std::move(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
|
@ -137,6 +186,9 @@ AudioOutputControl::LockCommandWait(Command cmd) noexcept
|
||||||
void
|
void
|
||||||
AudioOutputControl::EnableAsync()
|
AudioOutputControl::EnableAsync()
|
||||||
{
|
{
|
||||||
|
if (!output)
|
||||||
|
return;
|
||||||
|
|
||||||
if (!thread.IsDefined()) {
|
if (!thread.IsDefined()) {
|
||||||
if (!output->SupportsEnableDisable()) {
|
if (!output->SupportsEnableDisable()) {
|
||||||
/* don't bother to start the thread now if the
|
/* don't bother to start the thread now if the
|
||||||
|
@ -155,6 +207,9 @@ AudioOutputControl::EnableAsync()
|
||||||
void
|
void
|
||||||
AudioOutputControl::DisableAsync() noexcept
|
AudioOutputControl::DisableAsync() noexcept
|
||||||
{
|
{
|
||||||
|
if (!output)
|
||||||
|
return;
|
||||||
|
|
||||||
if (!thread.IsDefined()) {
|
if (!thread.IsDefined()) {
|
||||||
if (!output->SupportsEnableDisable())
|
if (!output->SupportsEnableDisable())
|
||||||
really_enabled = false;
|
really_enabled = false;
|
||||||
|
@ -234,6 +289,9 @@ AudioOutputControl::CloseWait(std::unique_lock<Mutex> &lock) noexcept
|
||||||
{
|
{
|
||||||
assert(allow_play);
|
assert(allow_play);
|
||||||
|
|
||||||
|
if (IsDummy())
|
||||||
|
return;
|
||||||
|
|
||||||
if (output->mixer != nullptr)
|
if (output->mixer != nullptr)
|
||||||
mixer_auto_close(output->mixer);
|
mixer_auto_close(output->mixer);
|
||||||
|
|
||||||
|
|
|
@ -49,6 +49,13 @@ class AudioOutputClient;
|
||||||
class AudioOutputControl {
|
class AudioOutputControl {
|
||||||
std::unique_ptr<FilteredAudioOutput> output;
|
std::unique_ptr<FilteredAudioOutput> output;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A copy of FilteredAudioOutput::name which we need just in
|
||||||
|
* case this is a "dummy" output (output==nullptr) because
|
||||||
|
* this output has been moved to another partitioncommands.
|
||||||
|
*/
|
||||||
|
const std::string name;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The PlayerControl object which "owns" this output. This
|
* The PlayerControl object which "owns" this output. This
|
||||||
* object is needed to signal command completion.
|
* object is needed to signal command completion.
|
||||||
|
@ -256,6 +263,10 @@ public:
|
||||||
gcc_pure
|
gcc_pure
|
||||||
Mixer *GetMixer() const noexcept;
|
Mixer *GetMixer() const noexcept;
|
||||||
|
|
||||||
|
bool IsDummy() const noexcept {
|
||||||
|
return !output;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Caller must lock the mutex.
|
* Caller must lock the mutex.
|
||||||
*/
|
*/
|
||||||
|
@ -294,6 +305,14 @@ public:
|
||||||
return last_error;
|
return last_error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detach and return the #FilteredAudioOutput instance and,
|
||||||
|
* replacing it here with a "dummy" object.
|
||||||
|
*/
|
||||||
|
std::unique_ptr<FilteredAudioOutput> Steal() noexcept;
|
||||||
|
void ReplaceDummy(std::unique_ptr<FilteredAudioOutput> new_output,
|
||||||
|
bool _enabled) noexcept;
|
||||||
|
|
||||||
void StartThread();
|
void StartThread();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "MultipleOutputs.hxx"
|
#include "MultipleOutputs.hxx"
|
||||||
|
#include "Client.hxx"
|
||||||
#include "Filtered.hxx"
|
#include "Filtered.hxx"
|
||||||
#include "Defaults.hxx"
|
#include "Defaults.hxx"
|
||||||
#include "MusicPipe.hxx"
|
#include "MusicPipe.hxx"
|
||||||
|
@ -142,6 +143,21 @@ MultipleOutputs::FindByName(const char *name) noexcept
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
MultipleOutputs::Add(std::unique_ptr<FilteredAudioOutput> output,
|
||||||
|
bool enable) noexcept
|
||||||
|
{
|
||||||
|
auto &client = GetAnyClient();
|
||||||
|
|
||||||
|
// TODO: this operation needs to be protected with a mutex
|
||||||
|
outputs.emplace_back(std::make_unique<AudioOutputControl>(std::move(output),
|
||||||
|
client));
|
||||||
|
|
||||||
|
outputs.back()->LockSetEnabled(enable);
|
||||||
|
|
||||||
|
client.ApplyEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
MultipleOutputs::EnableDisable()
|
MultipleOutputs::EnableDisable()
|
||||||
{
|
{
|
||||||
|
|
|
@ -119,6 +119,9 @@ public:
|
||||||
return FindByName(name) != nullptr;
|
return FindByName(name) != nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Add(std::unique_ptr<FilteredAudioOutput> output,
|
||||||
|
bool enable) noexcept;
|
||||||
|
|
||||||
void SetReplayGainMode(ReplayGainMode mode) noexcept;
|
void SetReplayGainMode(ReplayGainMode mode) noexcept;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -153,6 +156,10 @@ public:
|
||||||
void SetSoftwareVolume(unsigned volume) noexcept;
|
void SetSoftwareVolume(unsigned volume) noexcept;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
AudioOutputClient &GetAnyClient() noexcept {
|
||||||
|
return outputs.front()->GetClient();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Was Open() called successfully?
|
* Was Open() called successfully?
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in New Issue