Add "mute" to stream API

This commit is contained in:
badaix 2022-07-13 17:35:49 +02:00
parent 60dc1aa48a
commit 9e3c0fdf55
12 changed files with 47 additions and 10 deletions

View file

@ -519,6 +519,7 @@ See [Plugin.Stream.Player.SetProperty](stream_plugin.md#pluginstreamplayersetpro
{"id": 1, "jsonrpc": "2.0", "error": {"code": -32602, "message": "Value for loopStatus must be one of 'none', 'track', 'playlist'"}} {"id": 1, "jsonrpc": "2.0", "error": {"code": -32602, "message": "Value for loopStatus must be one of 'none', 'track', 'playlist'"}}
{"id": 1, "jsonrpc": "2.0", "error": {"code": -32602, "message": "Value for shuffle must be bool"}} {"id": 1, "jsonrpc": "2.0", "error": {"code": -32602, "message": "Value for shuffle must be bool"}}
{"id": 1, "jsonrpc": "2.0", "error": {"code": -32602, "message": "Value for volume must be an int"}} {"id": 1, "jsonrpc": "2.0", "error": {"code": -32602, "message": "Value for volume must be an int"}}
{"id": 1, "jsonrpc": "2.0", "error": {"code": -32602, "message": "Value for mute must be bool"}}
{"id": 1, "jsonrpc": "2.0", "error": {"code": -32602, "message": "Value for rate must be float"}} {"id": 1, "jsonrpc": "2.0", "error": {"code": -32602, "message": "Value for rate must be float"}}
``` ```

View file

@ -82,7 +82,7 @@ Any [json-rpc 2.0 conformant error](https://www.jsonrpc.org/specification#error_
Snapserver will send the `SetProperty` command to the plugin, if `canControl` is `true`. Snapserver will send the `SetProperty` command to the plugin, if `canControl` is `true`.
```json ```json
{"id": 1, "jsonrpc": "2.0", "method": "Plugin.Stream.Player.SetProperty", "params": {"<property>", <value>}} {"id": 1, "jsonrpc": "2.0", "method": "Plugin.Stream.Player.SetProperty", "params": {"<property>": <value>}}
``` ```
#### Supported `property`s #### Supported `property`s
@ -93,7 +93,8 @@ Snapserver will send the `SetProperty` command to the plugin, if `canControl` is
* `playlist`: the playback loops through a list of tracks * `playlist`: the playback loops through a list of tracks
* `shuffle`: [bool] play playlist in random order * `shuffle`: [bool] play playlist in random order
* `volume`: [int] voume in percent, valid range [0..100] * `volume`: [int] voume in percent, valid range [0..100]
* `rate`: [float] The current playback rate, valid range (0..) * `mute`: [bool] the current mute state
* `rate`: [float] the current playback rate, valid range (0..)
#### Expected response #### Expected response
@ -125,6 +126,7 @@ Any [json-rpc 2.0 conformant error](https://www.jsonrpc.org/specification#error_
* `playlist`: The playback loops through a list of tracks * `playlist`: The playback loops through a list of tracks
* `shuffle`: [bool] Traverse through the playlist in random order * `shuffle`: [bool] Traverse through the playlist in random order
* `volume`: [int] Voume in percent, valid range [0..100] * `volume`: [int] Voume in percent, valid range [0..100]
* `mute`: [bool] Current mute state
* `rate`: [float] The current playback rate, valid range (0..) * `rate`: [float] The current playback rate, valid range (0..)
* `position`: [float] Current playback position in seconds * `position`: [float] Current playback position in seconds
* `canGoNext`: [bool] Whether the client can call the `next` method on this interface and expect the current track to change * `canGoNext`: [bool] Whether the client can call the `next` method on this interface and expect the current track to change
@ -187,9 +189,9 @@ Any [json-rpc 2.0 conformant error](https://www.jsonrpc.org/specification#error_
##### Success ##### Success
```json ```json
{"id": 1, "jsonrpc": "2.0", "result": {"canControl":true,"canGoNext":true,"canGoPrevious":true,"canPause":true,"canPlay":true,"canSeek":false,"loopStatus":"none","playbackStatus":"playing","position":93.394,"shuffle":false,"volume":86}} {"id": 1, "jsonrpc": "2.0", "result": {"canControl":true,"canGoNext":true,"canGoPrevious":true,"canPause":true,"canPlay":true,"canSeek":false,"loopStatus":"none","playbackStatus":"playing","position":93.394,"shuffle":false,"volume":86,"mute":false}}
{"id": 1, "jsonrpc": "2.0", "result": {"canControl":true,"canGoNext":true,"canGoPrevious":true,"canPause":true,"canPlay":true,"canSeek":true,"loopStatus":"none","metadata":{"album":"Doldinger","albumArtist":["Klaus Doldinger's Passport"],"artUrl":"http://coverartarchive.org/release/0d4ff56b-2a2b-43b5-bf99-063cac1599e5/16940576164-250.jpg","artist":["Klaus Doldinger's Passport feat. Nils Landgren"],"contentCreated":"2016","duration":305.2929992675781,"genre":["Jazz"],"musicbrainzAlbumId":"0d4ff56b-2a2b-43b5-bf99-063cac1599e5","title":"Soul Town","trackId":"7","trackNumber":6,"url":"Klaus Doldinger's Passport - Doldinger (2016)/06 - Soul Town.mp3"},"playbackStatus":"playing","position":72.79499816894531,"shuffle":false,"volume":97}} {"id": 1, "jsonrpc": "2.0", "result": {"canControl":true,"canGoNext":true,"canGoPrevious":true,"canPause":true,"canPlay":true,"canSeek":true,"loopStatus":"none","metadata":{"album":"Doldinger","albumArtist":["Klaus Doldinger's Passport"],"artUrl":"http://coverartarchive.org/release/0d4ff56b-2a2b-43b5-bf99-063cac1599e5/16940576164-250.jpg","artist":["Klaus Doldinger's Passport feat. Nils Landgren"],"contentCreated":"2016","duration":305.2929992675781,"genre":["Jazz"],"musicbrainzAlbumId":"0d4ff56b-2a2b-43b5-bf99-063cac1599e5","title":"Soul Town","trackId":"7","trackNumber":6,"url":"Klaus Doldinger's Passport - Doldinger (2016)/06 - Soul Town.mp3"},"playbackStatus":"playing","position":72.79499816894531,"shuffle":false,"volume":97,"mute":false}}
``` ```
##### Error ##### Error
@ -201,7 +203,7 @@ Any [json-rpc 2.0 conformant error](https://www.jsonrpc.org/specification#error_
### Plugin.Stream.Player.Properties ### Plugin.Stream.Player.Properties
```json ```json
{"jsonrpc": "2.0", "method": "Plugin.Stream.Player.Properties", "params": {"canControl":true,"canGoNext":true,"canGoPrevious":true,"canPause":true,"canPlay":true,"canSeek":false,"loopStatus":"none","playbackStatus":"playing","position":593.394,"shuffle":false,"volume":86}} {"jsonrpc": "2.0", "method": "Plugin.Stream.Player.Properties", "params": {"canControl":true,"canGoNext":true,"canGoPrevious":true,"canPause":true,"canPlay":true,"canSeek":false,"loopStatus":"none","playbackStatus":"playing","position":593.394,"shuffle":false,"volume":86,"mute":false}}
``` ```
Same format as in [GetProperties](#pluginstreamplayergetproperties). If `metadata` is missing, the last known metadata will be used, so the plugin must not send the complete metadata if one of the properties is updated. Same format as in [GetProperties](#pluginstreamplayergetproperties). If `metadata` is missing, the last known metadata will be used, so the plugin must not send the complete metadata if one of the properties is updated.

View file

@ -185,6 +185,8 @@ class MopidyControl(object):
properties['shuffle'] = result properties['shuffle'] = result
elif request == 'core.mixer.get_volume': elif request == 'core.mixer.get_volume':
properties['volume'] = result properties['volume'] = result
elif request == 'core.mixer.get_mute':
properties['mute'] = result
elif request == 'core.playback.get_time_position': elif request == 'core.playback.get_time_position':
properties['position'] = float(result) / 1000 properties['position'] = float(result) / 1000
elif request == 'core.playback.get_current_track': elif request == 'core.playback.get_current_track':
@ -283,7 +285,7 @@ class MopidyControl(object):
jmsg['tl_track']['track']) jmsg['tl_track']['track'])
logger.debug(f'Meta: {self._metadata}') logger.debug(f'Meta: {self._metadata}')
self.send_batch_request([("core.playback.get_state", None), ("core.tracklist.get_repeat", None), ("core.tracklist.get_single", None), self.send_batch_request([("core.playback.get_state", None), ("core.tracklist.get_repeat", None), ("core.tracklist.get_single", None),
("core.tracklist.get_random", None), ("core.mixer.get_volume", None), ("core.playback.get_time_position", None), ('core.library.get_images', { ("core.tracklist.get_random", None), ("core.mixer.get_volume", None), ("core.mixer.get_mute", None), ("core.playback.get_time_position", None), ('core.library.get_images', {
'uris': [self._metadata['url']]})], self.onPropertiesResponse) 'uris': [self._metadata['url']]})], self.onPropertiesResponse)
elif event in ['tracklist_changed', 'track_playback_ended']: elif event in ['tracklist_changed', 'track_playback_ended']:
logger.debug("Nothing to do") logger.debug("Nothing to do")
@ -293,7 +295,7 @@ class MopidyControl(object):
logger.debug("Nothing to do") logger.debug("Nothing to do")
else: else:
self.send_batch_request([("core.playback.get_state", None), ("core.tracklist.get_repeat", None), ("core.tracklist.get_single", None), self.send_batch_request([("core.playback.get_state", None), ("core.tracklist.get_repeat", None), ("core.tracklist.get_single", None),
("core.tracklist.get_random", None), ("core.mixer.get_volume", None), ("core.playback.get_time_position", None)], self.onPropertiesResponse) ("core.tracklist.get_random", None), ("core.mixer.get_volume", None), ("core.mixer.get_mute", None), ("core.playback.get_time_position", None)], self.onPropertiesResponse)
def on_ws_error(self, ws, error): def on_ws_error(self, ws, error):
logger.error("Snapcast RPC websocket error") logger.error("Snapcast RPC websocket error")
@ -402,6 +404,9 @@ class MopidyControl(object):
if 'volume' in property: if 'volume' in property:
self.send_request("core.mixer.set_volume", { self.send_request("core.mixer.set_volume", {
"volume": int(property['volume'])}) "volume": int(property['volume'])})
if 'mute' in property:
self.send_request("core.mixer.set_mute", {
"mute": property['mute']})
elif cmd == 'GetProperties': elif cmd == 'GetProperties':
self.send_batch_request([("core.playback.get_state", None), ("core.tracklist.get_repeat", None), ("core.tracklist.get_single", None), self.send_batch_request([("core.playback.get_state", None), ("core.tracklist.get_repeat", None), ("core.tracklist.get_single", None),
("core.tracklist.get_random", None), ("core.mixer.get_volume", None), ("core.playback.get_current_track", None), ("core.playback.get_time_position", None)], lambda req_res: self.onSnapcastPropertiesResponse(id, req_res)) ("core.tracklist.get_random", None), ("core.mixer.get_volume", None), ("core.playback.get_current_track", None), ("core.playback.get_time_position", None)], lambda req_res: self.onSnapcastPropertiesResponse(id, req_res))

View file

@ -800,6 +800,7 @@ Usage: %(progname)s [OPTION]...
--snapcast-port=PORT Set the snapcast server port --snapcast-port=PORT Set the snapcast server port
--stream=ID Set the stream id --stream=ID Set the stream id
-h, --help Show this help message
-d, --debug Run in debug mode -d, --debug Run in debug mode
-v, --version meta_mpd version -v, --version meta_mpd version

View file

@ -564,6 +564,12 @@ void Server::processRequest(const jsonrpcpp::request_ptr request, const OnRespon
throw jsonrpcpp::InvalidParamsException("Value for volume must be an int", request->id()); throw jsonrpcpp::InvalidParamsException("Value for volume must be an int", request->id());
stream->setVolume(value.get<int16_t>(), [handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); }); stream->setVolume(value.get<int16_t>(), [handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); });
} }
else if (name == "mute")
{
if (!value.is_boolean())
throw jsonrpcpp::InvalidParamsException("Value for mute must be bool", request->id());
stream->setMute(value.get<bool>(), [handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); });
}
else if (name == "rate") else if (name == "rate")
{ {
if (!value.is_number_float()) if (!value.is_number_float())

View file

@ -226,6 +226,12 @@ void MetaStream::setVolume(uint16_t volume, ResultHandler handler)
active_stream_->setVolume(volume, std::move(handler)); active_stream_->setVolume(volume, std::move(handler));
} }
void MetaStream::setMute(bool mute, ResultHandler handler)
{
std::lock_guard<std::recursive_mutex> lock(mutex_);
active_stream_->setMute(mute, std::move(handler));
}
void MetaStream::setRate(float rate, ResultHandler handler) void MetaStream::setRate(float rate, ResultHandler handler)
{ {
std::lock_guard<std::recursive_mutex> lock(mutex_); std::lock_guard<std::recursive_mutex> lock(mutex_);

View file

@ -51,6 +51,7 @@ public:
void setShuffle(bool shuffle, ResultHandler handler) override; void setShuffle(bool shuffle, ResultHandler handler) override;
void setLoopStatus(LoopStatus status, ResultHandler handler) override; void setLoopStatus(LoopStatus status, ResultHandler handler) override;
void setVolume(uint16_t volume, ResultHandler handler) override; void setVolume(uint16_t volume, ResultHandler handler) override;
void setMute(bool mute, ResultHandler handler) override;
void setRate(float rate, ResultHandler handler) override; void setRate(float rate, ResultHandler handler) override;
// Control commands // Control commands

View file

@ -347,6 +347,15 @@ void PcmStream::setVolume(uint16_t volume, ResultHandler handler)
} }
void PcmStream::setMute(bool mute, ResultHandler handler)
{
LOG(DEBUG, LOG_TAG) << "setMute: " << mute << "\n";
if (!properties_.can_control)
return handler({ControlErrc::can_control_is_false});
sendRequest("Plugin.Stream.Player.SetProperty", {"mute", mute}, std::move(handler));
}
void PcmStream::setRate(float rate, ResultHandler handler) void PcmStream::setRate(float rate, ResultHandler handler)
{ {
LOG(DEBUG, LOG_TAG) << "setRate: " << rate << "\n"; LOG(DEBUG, LOG_TAG) << "setRate: " << rate << "\n";

View file

@ -141,6 +141,7 @@ public:
virtual void setShuffle(bool shuffle, ResultHandler handler); virtual void setShuffle(bool shuffle, ResultHandler handler);
virtual void setLoopStatus(LoopStatus status, ResultHandler handler); virtual void setLoopStatus(LoopStatus status, ResultHandler handler);
virtual void setVolume(uint16_t volume, ResultHandler handler); virtual void setVolume(uint16_t volume, ResultHandler handler);
virtual void setMute(bool mute, ResultHandler handler);
virtual void setRate(float rate, ResultHandler handler); virtual void setRate(float rate, ResultHandler handler);
// Control commands // Control commands

View file

@ -92,6 +92,7 @@ json Properties::toJson() const
addTag(j, "rate", rate); addTag(j, "rate", rate);
addTag(j, "shuffle", shuffle); addTag(j, "shuffle", shuffle);
addTag(j, "volume", volume); addTag(j, "volume", volume);
addTag(j, "mute", mute);
addTag(j, "position", position); addTag(j, "position", position);
addTag(j, "minimumRate", minimum_rate); addTag(j, "minimumRate", minimum_rate);
addTag(j, "maximumRate", maximum_rate); addTag(j, "maximumRate", maximum_rate);
@ -108,9 +109,9 @@ json Properties::toJson() const
void Properties::fromJson(const json& j) void Properties::fromJson(const json& j)
{ {
static std::set<std::string> rw_props = {"loopStatus", "shuffle", "volume", "rate"}; static std::set<std::string> rw_props = {"loopStatus", "shuffle", "volume", "mute", "rate"};
static std::set<std::string> ro_props = {"playbackStatus", "loopStatus", "shuffle", "volume", "position", "minimumRate", "maximumRate", static std::set<std::string> ro_props = {"playbackStatus", "loopStatus", "shuffle", "volume", "mute", "position", "minimumRate", "maximumRate",
"canGoNext", "canGoPrevious", "canPlay", "canPause", "canSeek", "canControl", "metadata"}; "canGoNext", "canGoPrevious", "canPlay", "canPause", "canSeek", "canControl", "metadata"};
for (const auto& element : j.items()) for (const auto& element : j.items())
{ {
bool is_rw = (rw_props.find(element.key()) != rw_props.end()); bool is_rw = (rw_props.find(element.key()) != rw_props.end());
@ -135,6 +136,7 @@ void Properties::fromJson(const json& j)
readTag(j, "rate", rate); readTag(j, "rate", rate);
readTag(j, "shuffle", shuffle); readTag(j, "shuffle", shuffle);
readTag(j, "volume", volume); readTag(j, "volume", volume);
readTag(j, "mute", mute);
readTag(j, "position", position); readTag(j, "position", position);
readTag(j, "minimumRate", minimum_rate); readTag(j, "minimumRate", minimum_rate);
readTag(j, "maximumRate", maximum_rate); readTag(j, "maximumRate", maximum_rate);

View file

@ -166,6 +166,8 @@ public:
std::optional<bool> shuffle; std::optional<bool> shuffle;
/// The volume level between 0-100 /// The volume level between 0-100
std::optional<int> volume; std::optional<int> volume;
/// The current mute state
std::optional<bool> mute;
/// The current track position in seconds /// The current track position in seconds
std::optional<float> position; std::optional<float> position;
/// The minimum value which the Rate property can take. Clients should not attempt to set the Rate property below this value /// The minimum value which the Rate property can take. Clients should not attempt to set the Rate property below this value

View file

@ -190,6 +190,7 @@ TEST_CASE("Properties")
"loopStatus": "track", "loopStatus": "track",
"shuffle": false, "shuffle": false,
"volume": 42, "volume": 42,
"mute": false,
"position": 23.0 "position": 23.0
} }
)"); )");