Add support for Player.Properties

This commit is contained in:
badaix 2021-05-30 22:20:49 +02:00
parent 283c3d2c9b
commit 7c11cb7559
11 changed files with 577 additions and 154 deletions

View file

@ -268,7 +268,10 @@ private:
try
{
if (!source.has_value())
{
if (j.contains(tag))
j.erase(tag);
}
else
j[tag] = source.value();
}

232
common/properties.hpp Normal file
View file

@ -0,0 +1,232 @@
/***
This file is part of snapcast
Copyright (C) 2014-2021 Johannes Pohl
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 3 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, see <http://www.gnu.org/licenses/>.
***/
#ifndef PROPERTIES_HPP
#define PROPERTIES_HPP
#include <boost/optional.hpp>
#include <string>
#include "common/aixlog.hpp"
#include "common/json.hpp"
using json = nlohmann::json;
enum class PlaybackStatus
{
kPlaying = 0,
kPaused = 1,
kStopped = 2,
kUnknown = 3
};
static std::string to_string(PlaybackStatus playback_status)
{
switch (playback_status)
{
case PlaybackStatus::kPlaying:
return "playing";
case PlaybackStatus::kPaused:
return "paused";
case PlaybackStatus::kStopped:
return "stopped";
default:
return "unknown";
}
}
static std::ostream& operator<<(std::ostream& os, PlaybackStatus playback_status)
{
os << to_string(playback_status);
return os;
}
enum class LoopStatus
{
kNone = 0,
kTrack = 1,
kPlaylist = 2,
kUnknown = 3
};
static std::string to_string(LoopStatus loop_status)
{
switch (loop_status)
{
case LoopStatus::kNone:
return "none";
case LoopStatus::kTrack:
return "track";
case LoopStatus::kPlaylist:
return "playlist";
default:
return "unknown";
}
}
static std::ostream& operator<<(std::ostream& os, LoopStatus loop_status)
{
os << to_string(loop_status);
return os;
}
class Properties
{
public:
Properties() = default;
Properties(const json& j)
{
fromJson(j);
}
/// https://www.musicpd.org/doc/html/protocol.html#tags
/// The current playback status
boost::optional<PlaybackStatus> playback_status;
/// The current loop / repeat status
boost::optional<LoopStatus> loop_status;
/// A value of false indicates that playback is progressing linearly through a playlist, while true means playback is progressing through a playlist in some
/// other order.
boost::optional<bool> shuffle;
/// The volume level between 0-100
boost::optional<int> volume;
/// The current track position in seconds
boost::optional<float> position;
/// The current track duration in seconds
boost::optional<float> duration;
/// Whether the client can call the Next method on this interface and expect the current track to change
boost::optional<bool> can_go_next;
/// Whether the client can call the Previous method on this interface and expect the current track to change
boost::optional<bool> can_go_previous;
/// Whether playback can be started using "play" or "playPause"
boost::optional<bool> can_play;
/// Whether playback can be paused using "pause" or "playPause"
boost::optional<bool> can_pause;
/// Whether the client can control the playback position using "seek" and "setPosition". This may be different for different tracks
boost::optional<bool> can_seek;
/// Whether the media player may be controlled over this interface
boost::optional<bool> can_control;
json toJson() const
{
json j;
if (playback_status.has_value())
addTag(j, "playbackStatus", boost::optional<std::string>(to_string(playback_status.value())));
if (loop_status.has_value())
addTag(j, "loopStatus", boost::optional<std::string>(to_string(loop_status.value())));
addTag(j, "shuffle", shuffle);
addTag(j, "volume", volume);
addTag(j, "position", position);
addTag(j, "duration", duration);
addTag(j, "canGoNext", can_go_next);
addTag(j, "canGoPrevious", can_go_previous);
addTag(j, "canPlay", can_play);
addTag(j, "canPause", can_pause);
addTag(j, "canSeek", can_seek);
addTag(j, "canControl", can_control);
return j;
}
void fromJson(const json& j)
{
boost::optional<std::string> opt;
readTag(j, "playbackStatus", opt);
if (opt.has_value())
{
if (*opt == "playing")
playback_status = PlaybackStatus::kPlaying;
else if (*opt == "paused")
playback_status = PlaybackStatus::kPaused;
else if (*opt == "stopped")
playback_status = PlaybackStatus::kStopped;
else
playback_status = PlaybackStatus::kUnknown;
}
else
playback_status = boost::none;
readTag(j, "loopStatus", opt);
if (opt.has_value())
{
if (*opt == "none")
loop_status = LoopStatus::kNone;
else if (*opt == "track")
loop_status = LoopStatus::kTrack;
else if (*opt == "playlist")
loop_status = LoopStatus::kPlaylist;
else
loop_status = LoopStatus::kUnknown;
}
else
loop_status = boost::none;
readTag(j, "shuffle", shuffle);
readTag(j, "volume", volume);
readTag(j, "position", position);
readTag(j, "duration", duration);
readTag(j, "canGoNext", can_go_next);
readTag(j, "canGoPrevious", can_go_previous);
readTag(j, "canPlay", can_play);
readTag(j, "canPause", can_pause);
readTag(j, "canSeek", can_seek);
readTag(j, "canControl", can_control);
}
private:
template <typename T>
void readTag(const json& j, const std::string& tag, boost::optional<T>& dest) const
{
try
{
if (!j.contains(tag))
dest = boost::none;
else
dest = j[tag].get<T>();
}
catch (const std::exception& e)
{
LOG(ERROR) << "failed to read tag: '" << tag << "': " << e.what() << '\n';
}
}
template <typename T>
void addTag(json& j, const std::string& tag, const boost::optional<T>& source) const
{
try
{
if (!source.has_value())
{
if (j.contains(tag))
j.erase(tag);
}
else
j[tag] = source.value();
}
catch (const std::exception& e)
{
LOG(ERROR) << "failed to add tag: '" << tag << "': " << e.what() << '\n';
}
}
};
#endif

View file

@ -82,10 +82,13 @@ defaults = {
# Player.Status
status_mapping = {
# https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#properties
'state': ['playbackStatus', lambda val: {'play': 'playing', 'pause': 'paused', 'stop': 'stopped'}[val]], # R/O - play => playing, pause => paused, stop => stopped
'repeat': ['loopStatus', lambda val: {'0': 'none', '1': 'track', '2': 'playlist'}[val]], # R/W - 0 => none, 1 => track, n/a => playlist
# R/O - play => playing, pause => paused, stop => stopped
'state': ['playbackStatus', lambda val: {'play': 'playing', 'pause': 'paused', 'stop': 'stopped'}[val]],
# R/W - 0 => none, 1 => track, n/a => playlist
'repeat': ['loopStatus', lambda val: {'0': 'none', '1': 'track', '2': 'playlist'}[val]],
# 'Rate d (Playback_Rate) R/W
'random': ['shuffle', lambda val: {'0': False, '1': True}[val]], # R/W - 0 => false, 1 => true
# R/W - 0 => false, 1 => true
'random': ['shuffle', lambda val: {'0': False, '1': True}[val]],
# 'Metadata a{sv} (Metadata_Map) Read only
'volume': ['volume', int], # R/W - 0-100 => 0-100
'elapsed': ['position', float], # R/O - seconds? ms?
@ -109,7 +112,8 @@ status_mapping = {
# nextsong 2: playlist song number of the next song to be played
# nextsongid 2: playlist songid of the next song to be played
# time: total time elapsed (of current playing/paused song) in seconds (deprecated, use elapsed instead)
'duration': ['duration', float], # duration 5: Duration of the current song in seconds.
# duration 5: Duration of the current song in seconds.
'duration': ['duration', float],
# bitrate: instantaneous bitrate in kbps
# xfade: crossfade in seconds
# mixrampdb: mixramp threshold in dB
@ -119,7 +123,8 @@ status_mapping = {
# error: if there is an error, returns message here
# Snapcast
'mute': ['mute', lambda val: {'0': False, '1': True}[val]] # R/W true/false
# R/W true/false
'mute': ['mute', lambda val: {'0': False, '1': True}[val]]
}
# Player.Metadata
@ -207,6 +212,7 @@ class MPDWrapper(object):
self._temp_cover = None
self._position = 0
self._time = 0
self._currentsong = None
def run(self):
"""
@ -370,8 +376,9 @@ class MPDWrapper(object):
def control(self, cmd):
try:
request = json.loads(cmd)
cmd = request['method']
id = request['id']
[interface, cmd] = request['method'].split('.', 1)
if interface == 'Player':
success = True
if cmd == 'Next':
self.next()
@ -401,6 +408,29 @@ class MPDWrapper(object):
if offset >= 0:
strOffset = "+" + strOffset
self.seekcur(strOffset)
elif cmd == 'SetProperties':
properties = request['params']
logger.info(f'SetProperties: {properties}')
if 'shuffle' in properties:
self.random(int(properties['shuffle']))
if 'loopStatus' in properties:
value = properties['loopStatus']
if value == "playlist":
self.repeat(1)
if self._can_single:
self.single(0)
elif value == "track":
if self._can_single:
self.repeat(1)
self.single(1)
elif value == "none":
self.repeat(0)
if self._can_single:
self.single(0)
else:
send({"jsonrpc": "2.0", "error": {"code": -32601,
"message": "Method not found"}, "id": id})
success = False
else:
send({"jsonrpc": "2.0", "error": {"code": -32601,
"message": "Method not found"}, "id": id})
@ -446,7 +476,8 @@ class MPDWrapper(object):
if subsystem in ("player", "mixer", "options", "playlist"):
if not updated:
logger.info(f'Subsystem: {subsystem}')
self._update_properties(force=True)
self._update_properties(
force=subsystem == 'player')
updated = True
self.idle_enter()
return True
@ -455,6 +486,44 @@ class MPDWrapper(object):
self.reconnect()
return True
def update_albumart(self, snapmeta):
album_key = 'musicbrainzAlbumId'
try:
if not album_key in snapmeta:
mbartist = None
mbrelease = None
if 'artist' in snapmeta:
mbartist = snapmeta['artist'][0]
if 'album' in snapmeta:
mbrelease = snapmeta['album']
else:
if 'title' in snapmeta:
mbrelease = snapmeta['title']
if mbartist is not None and mbrelease is not None:
logger.info(
f'Querying album art for artist "{mbartist}", release: "{mbrelease}"')
result = musicbrainzngs.search_releases(artist=mbartist, release=mbrelease,
limit=1)
if result['release-list']:
snapmeta[album_key] = result['release-list'][0]['id']
if album_key in snapmeta:
data = musicbrainzngs.get_image_list(snapmeta[album_key])
for image in data["images"]:
if "Front" in image["types"] and image["approved"]:
snapmeta['artUrl'] = image["thumbnails"]["small"]
logger.debug(
f'{snapmeta["artUrl"]} is an approved front image')
logger.info(f'Snapmeta: {snapmeta}')
send(
{"jsonrpc": "2.0", "method": "Player.Metadata", "params": snapmeta})
break
except musicbrainzngs.musicbrainz.ResponseError as e:
logger.error(
f'Error while getting cover for {snapmeta[album_key]}: {e}')
def update_metadata(self):
"""
Translate metadata returned by MPD to the MPRIS v2 syntax.
@ -505,49 +574,16 @@ class MPDWrapper(object):
snapmeta['title'] = fields[1]
send({"jsonrpc": "2.0", "method": "Player.Metadata", "params": snapmeta})
album_key = 'musicbrainzAlbumId'
try:
if not album_key in snapmeta:
mbartist = None
mbrelease = None
if 'artist' in snapmeta:
mbartist = snapmeta['artist'][0]
if 'album' in snapmeta:
mbrelease = snapmeta['album']
else:
if 'title' in snapmeta:
mbrelease = snapmeta['title']
if mbartist is not None and mbrelease is not None:
logger.info(
f'Querying album art for artist "{mbartist}", release: "{mbrelease}"')
result = musicbrainzngs.search_releases(artist=mbartist, release=mbrelease,
limit=1)
if result['release-list']:
snapmeta[album_key] = result['release-list'][0]['id']
if album_key in snapmeta:
data = musicbrainzngs.get_image_list(snapmeta[album_key])
for image in data["images"]:
if "Front" in image["types"] and image["approved"]:
snapmeta['artUrl'] = image["thumbnails"]["small"]
logger.debug(
f'{snapmeta["artUrl"]} is an approved front image')
break
except musicbrainzngs.musicbrainz.ResponseError as e:
logger.error(
f'Error while getting cover for {snapmeta[album_key]}: {e}')
logger.info(f'Snapmeta: {snapmeta}')
send({"jsonrpc": "2.0", "method": "Player.Metadata", "params": snapmeta})
self.update_albumart(snapmeta)
def _update_properties(self, force=False):
old_status = self._status
old_position = self._position
old_time = self._time
self._currentsong = self.client.currentsong()
currentsong = self.client.currentsong()
if self._currentsong != currentsong:
self._currentsong = currentsong
force = True
new_status = self.client.status()
logger.info(f'new status: {new_status}')
self._time = new_time = int(time.time())
@ -566,7 +602,13 @@ class MPDWrapper(object):
logger.warning("Can't cast value %r to %s" %
(value, status_mapping[key][1]))
send({"jsonrpc": "2.0", "method": "Player.Status", "params": snapstatus})
snapstatus['canGoNext'] = True
snapstatus['canGoPrevious'] = True
snapstatus['canPlay'] = True
snapstatus['canPause'] = True
snapstatus['canSeek'] = True
snapstatus['canControl'] = True
send({"jsonrpc": "2.0", "method": "Player.Properties", "params": snapstatus})
if not new_status:
logger.debug("_update_properties: failed to get new status")

View file

@ -32,6 +32,7 @@ import sys
import re
import shlex
import getopt
import time
import socket
import dbus
import dbus.service
@ -306,7 +307,7 @@ class MPDWrapper(object):
'repeat': None,
}
self._metadata = {}
self._position = 0
self._properties = {}
self._time = 0
self._req_id = 0
@ -366,6 +367,30 @@ class MPDWrapper(object):
'Metadata')
logger.info(f'new meta {new_meta}')
elif jmsg["method"] == "Stream.OnProperties":
logger.info(
f'Stream properties changed for "{jmsg["params"]["id"]}"')
props = jmsg["params"]["properties"]
logger.info(f'Properties: "{props}"')
props['received'] = time.time()
changed_properties = self.update_properties(props)
logger.info(f'Changed properties: "{changed_properties}"')
if 'playbackStatus' in changed_properties:
self._dbus_service.update_property(
'org.mpris.MediaPlayer2.Player', 'PlaybackStatus')
if 'loopStatus' in changed_properties:
self._dbus_service.update_property(
'org.mpris.MediaPlayer2.Player', 'LoopStatus')
if 'shuffle' in changed_properties:
self._dbus_service.update_property(
'org.mpris.MediaPlayer2.Player', 'Shuffle')
if 'volume' in changed_properties:
self._dbus_service.update_property(
'org.mpris.MediaPlayer2.Player', 'Volume')
if 'canGoNext' in changed_properties:
self._dbus_service.update_property(
'org.mpris.MediaPlayer2.Player', 'CanGoNext')
def on_ws_error(self, ws, error):
logger.error("Snapcast RPC websocket error")
logger.error(error)
@ -382,8 +407,7 @@ class MPDWrapper(object):
# '/org/mpris/MediaPlayer2')
self._dbus_service.acquire_name()
self.websocket.send(json.dumps(
{"id": 1, "jsonrpc": "2.0", "method": "Server.GetStatus"}))
self.send({"id": 1, "jsonrpc": "2.0", "method": "Server.GetStatus"})
def on_ws_close(self, ws):
logger.info("Snapcast RPC websocket closed")
@ -391,6 +415,9 @@ class MPDWrapper(object):
self._dbus_service.release_name()
# self._dbus_service.remove_from_connection()
def send(self, json_msg):
self.websocket.send(json.dumps(json_msg))
def stop(self):
self.websocket.keep_running = False
logger.info("Waiting for websocket thread to exit")
@ -469,12 +496,54 @@ class MPDWrapper(object):
url = f'http://{self._params["host"]}:{self._params["port"]}/jsonrpc'
logger.info(f'url: {url}')
self._req_id += 1
self.websocket.send(json.dumps(j))
self.send(j)
def set_property(self, property, value):
properties = {}
properties[property] = value
logger.info(f'set_properties {properties}')
j = {"id": self._req_id, "jsonrpc": "2.0", "method": "Stream.SetProperties",
"params": {"id": "Pipe", "properties": properties}}
logger.info(f'Set properties: {properties}, json: {j}')
url = f'http://{self._params["host"]}:{self._params["port"]}/jsonrpc'
logger.info(f'url: {url}')
self._req_id += 1
self.send(j)
@property
def metadata(self):
return self._metadata
@property
def properties(self):
return self._properties
def update_properties(self, new_properties):
changed_properties = {}
for key, value in new_properties.items():
if not key in self._properties:
changed_properties[key] = [None, value]
elif value != self._properties[key]:
changed_properties[key] = [self._properties[key], value]
for key, value in self._properties.items():
if not key in self._properties:
changed_properties[key] = [value, None]
self._properties = new_properties
return changed_properties
def position(self):
if not 'position' in self._properties:
return 0
if not 'duration' in self._properties:
return 0
position = self._properties['position']
if 'received' in self._properties:
position += (time.time() - self._properties['received'])
return position * 1000000
def property(self, name, default):
return self._properties.get(name, default)
def notify_about_track(self, meta, state='play'):
uri = 'sound'
# if 'mpris:artUrl' in meta:
@ -763,82 +832,53 @@ class MPRISInterface(dbus.service.Object):
}
def __get_playback_status():
# status = mpd_wrapper.last_status()
# return {'play': 'Playing', 'pause': 'Paused', 'stop': 'Stopped'}[status['state']]
return 'Playing'
status = snapcast_wrapper.property('playbackStatus', 'stopped')
logger.debug(f'get_playback_status "{status}"')
return {'playing': 'Playing', 'paused': 'Paused', 'stopped': 'Stopped'}[status]
def __set_loop_status(value):
logger.debug(f'set_loop_status "{value}"')
# if value == "Playlist":
# mpd_wrapper.repeat(1)
# if mpd_wrapper._can_single:
# mpd_wrapper.single(0)
# elif value == "Track":
# if mpd_wrapper._can_single:
# mpd_wrapper.repeat(1)
# mpd_wrapper.single(1)
# elif value == "None":
# mpd_wrapper.repeat(0)
# if mpd_wrapper._can_single:
# mpd_wrapper.single(0)
# else:
# raise dbus.exceptions.DBusException("Loop mode %r not supported" %
# value)
snapcast_wrapper.set_property(
'loopStatus', {'None': 'none', 'Track': 'track', 'Playlist': 'playlist'}[value])
return
def __get_loop_status():
logger.debug(f'get_loop_status')
# status = mpd_wrapper.last_status()
# if int(status['repeat']) == 1:
# if int(status.get('single', 0)) == 1:
# return "Track"
# else:
# return "Playlist"
# else:
# return "None"
return "None"
status = snapcast_wrapper.property('loopStatus', 'none')
logger.debug(f'get_loop_status "{status}"')
return {'none': 'None', 'track': 'Track', 'playlist': 'Playlist'}[status]
def __set_shuffle(value):
logger.debug(f'set_shuffle "{value}"')
# mpd_wrapper.random(value)
snapcast_wrapper.set_property('shuffle', bool(value))
return
def __get_shuffle():
logger.debug(f'get_shuffle')
# if int(mpd_wrapper.last_status()['random']) == 1:
# return True
# else:
# return False
return False
shuffle = snapcast_wrapper.property('shuffle', False)
logger.debug(f'get_shuffle "{shuffle}"')
return shuffle
def __get_metadata():
logger.debug(f'get_metadata: {snapcast_wrapper.metadata}')
return dbus.Dictionary(snapcast_wrapper.metadata, signature='sv')
def __get_volume():
logger.debug(f'get_volume')
# vol = float(mpd_wrapper.last_status().get('volume', 0))
# if vol > 0:
# return vol / 100.0
# else:
# return 0.0
volume = snapcast_wrapper.property('volume', 100)
logger.debug(f'get_volume "{volume}"')
if volume > 0:
return volume / 100.0
else:
return 0.0
def __set_volume(value):
logger.debug(f'set_voume: {value}')
# if value >= 0 and value <= 1:
# mpd_wrapper.setvol(int(value * 100))
if value >= 0 and value <= 1:
snapcast_wrapper.set_property('volume', int(value * 100))
return
def __get_position():
logger.debug(f'get_position')
# status = mpd_wrapper.last_status()
# if 'time' in status:
# current, end = status['time'].split(':')
# return dbus.Int64((int(current) * 1000000))
# else:
# return dbus.Int64(0)
return dbus.Int64(0)
position = snapcast_wrapper.position()
logger.debug(f'get_position: {position}')
return dbus.Int64(position)
__player_interface = "org.mpris.MediaPlayer2.Player"
__player_props = {
@ -995,7 +1035,8 @@ class MPRISInterface(dbus.service.Object):
# Player signals
@ dbus.service.signal(__player_interface, signature='x')
def Seeked(self, position):
logger.debug("Seeked to %i" % position)
logger.debug(f'Seeked to {position}')
snapcast_wrapper.properties['position'] = float(position) / 1000000
return float(position)

View file

@ -68,6 +68,17 @@ void Server::onMetaChanged(const PcmStream* pcmStream)
}
void Server::onPropertiesChanged(const PcmStream* pcmStream)
{
LOG(INFO, LOG_TAG) << "onPropertiesChanged (" << pcmStream->getName() << ")\n";
const auto props = pcmStream->getProperties();
// Send propeties to all connected control clients
json notification = jsonrpcpp::Notification("Stream.OnProperties", jsonrpcpp::Parameter("id", pcmStream->getId(), "properties", props->toJson())).to_json();
controlServer_->send(notification.dump(), nullptr);
}
void Server::onStateChanged(const PcmStream* pcmStream, ReaderState state)
{
// clang-format off
@ -452,6 +463,28 @@ void Server::processRequest(const jsonrpcpp::request_ptr request, jsonrpcpp::ent
// Setup response
result["id"] = streamId;
}
else if (request->method().find("Stream.SetProperties") == 0)
{
// clang-format off
// clang-format on
LOG(INFO, LOG_TAG) << "Stream.SetProperties id: " << request->params().get<std::string>("id")
<< ", properties: " << request->params().get("properties") << "\n";
// Find stream
string streamId = request->params().get<std::string>("id");
PcmStreamPtr stream = streamManager_->getStream(streamId);
if (stream == nullptr)
throw jsonrpcpp::InternalErrorException("Stream not found", request->id());
Properties props(request->params().get("properties"));
// Set metadata from request
stream->setProperties(props);
// Setup response
result["id"] = streamId;
}
else if (request->method() == "Stream.AddStream")
{
// clang-format off

View file

@ -76,6 +76,7 @@ private:
/// Implementation of PcmListener
void onMetaChanged(const PcmStream* pcmStream) override;
void onPropertiesChanged(const PcmStream* pcmStream) override;
void onStateChanged(const PcmStream* pcmStream, ReaderState state) override;
void onChunkRead(const PcmStream* pcmStream, const msg::PcmChunk& chunk) override;
void onChunkEncoded(const PcmStream* pcmStream, std::shared_ptr<msg::PcmChunk> chunk, double duration) override;

View file

@ -93,6 +93,12 @@ void MetaStream::onMetaChanged(const PcmStream* pcmStream)
}
void MetaStream::onPropertiesChanged(const PcmStream* pcmStream)
{
LOG(DEBUG, LOG_TAG) << "onPropertiesChanged: " << pcmStream->getName() << "\n";
}
void MetaStream::onStateChanged(const PcmStream* pcmStream, ReaderState state)
{
LOG(DEBUG, LOG_TAG) << "onStateChanged: " << pcmStream->getName() << ", state: " << state << "\n";

View file

@ -47,6 +47,7 @@ public:
protected:
/// Implementation of PcmListener
void onMetaChanged(const PcmStream* pcmStream) override;
void onPropertiesChanged(const PcmStream* pcmStream) override;
void onStateChanged(const PcmStream* pcmStream, ReaderState state) override;
void onChunkRead(const PcmStream* pcmStream, const msg::PcmChunk& chunk) override;
void onChunkEncoded(const PcmStream* pcmStream, std::shared_ptr<msg::PcmChunk> chunk, double duration) override;

View file

@ -257,38 +257,33 @@ void PcmStream::onControlMsg(const std::string& msg)
LOG(INFO, LOG_TAG) << "Notification method: " << notification->method() << ", params: " << notification->params().to_json() << "\n";
if (notification->method() == "Player.Metadata")
{
LOG(DEBUG, LOG_TAG) << "Received metadata notification\n";
setMeta(notification->params().to_json());
}
else if (notification->method() == "Player.Properties")
{
LOG(DEBUG, LOG_TAG) << "Received properties notification\n";
properties_ = std::make_shared<Properties>(notification->params().to_json());
// Trigger a stream update
for (auto* listener : pcmListeners_)
{
if (listener != nullptr)
listener->onPropertiesChanged(this);
}
}
else
LOG(WARNING, LOG_TAG) << "Received unknown notification method: '" << notification->method() << "'\n";
}
else if (entity->is_request())
{
LOG(INFO, LOG_TAG) << "Request\n";
// jsonrpcpp::entity_ptr response(nullptr);
// jsonrpcpp::notification_ptr notification(nullptr);
// jsonrpcpp::request_ptr request = dynamic_pointer_cast<jsonrpcpp::Request>(entity);
// processRequest(request, response, notification);
// saveConfig();
// ////cout << "Request: " << request->to_json().dump() << "\n";
// if (notification)
// {
// ////cout << "Notification: " << notification->to_json().dump() << "\n";
// controlServer_->send(notification->to_json().dump(), controlSession);
// }
// if (response)
// {
// ////cout << "Response: " << response->to_json().dump() << "\n";
// return response->to_json().dump();
// }
// return "";
jsonrpcpp::request_ptr request = dynamic_pointer_cast<jsonrpcpp::Request>(entity);
LOG(INFO, LOG_TAG) << "Request: " << request->method() << ", id: " << request->id() << ", params: " << request->params().to_json() << "\n";
}
else if (entity->is_response())
{
jsonrpcpp::response_ptr response = dynamic_pointer_cast<jsonrpcpp::Response>(entity);
LOG(INFO, LOG_TAG) << "Response: " << response->result().dump() << ", id: " << response->id() << "\n";
}
// json j = json::parse(msg);
// setMeta(j["params"]["meta"]);
}
@ -403,6 +398,24 @@ std::shared_ptr<msg::StreamTags> PcmStream::getMeta() const
}
std::shared_ptr<Properties> PcmStream::getProperties() const
{
return properties_;
}
void PcmStream::setProperties(const Properties& props)
{
LOG(INFO, LOG_TAG) << "Stream '" << getId() << "' set properties: " << props.toJson() << "\n";
// TODO: queue commands, send next on timeout or after reception of the last command's response
if (ctrl_script_)
{
jsonrpcpp::Request request(++req_id_, "Player.SetProperties", props.toJson());
ctrl_script_->send(request.to_json().dump() + "\n"); //, params);
}
}
void PcmStream::control(const std::string& command, const json& params)
{
LOG(INFO, LOG_TAG) << "Stream '" << getId() << "' received command: '" << command << "', params: '" << params << "'\n";
@ -410,9 +423,12 @@ void PcmStream::control(const std::string& command, const json& params)
{
LOG(INFO, LOG_TAG) << "Stream " << getId() << " key: '" << it.key() << "', param: '" << it.value() << "'\n";
}
jsonrpcpp::Request request(++req_id_, command, params);
// TODO: queue commands, send next on timeout or after reception of the last command's response
if (ctrl_script_)
{
jsonrpcpp::Request request(++req_id_, "Player." + command, params);
ctrl_script_->send(request.to_json().dump() + "\n"); //, params);
}
}

View file

@ -36,6 +36,7 @@
#include <boost/optional.hpp>
#include "common/json.hpp"
#include "common/properties.hpp"
#include "common/sample_format.hpp"
#include "encoder/encoder.hpp"
#include "message/codec_header.hpp"
@ -102,6 +103,7 @@ class PcmListener
{
public:
virtual void onMetaChanged(const PcmStream* pcmStream) = 0;
virtual void onPropertiesChanged(const PcmStream* pcmStream) = 0;
virtual void onStateChanged(const PcmStream* pcmStream, ReaderState state) = 0;
virtual void onChunkRead(const PcmStream* pcmStream, const msg::PcmChunk& chunk) = 0;
virtual void onChunkEncoded(const PcmStream* pcmStream, std::shared_ptr<msg::PcmChunk> chunk, double duration) = 0;
@ -169,6 +171,9 @@ public:
std::shared_ptr<msg::StreamTags> getMeta() const;
void setMeta(const json& j);
std::shared_ptr<Properties> getProperties() const;
void setProperties(const Properties& props);
virtual void control(const std::string& command, const json& params);
virtual ReaderState getState() const;
@ -194,6 +199,7 @@ protected:
std::string name_;
ReaderState state_;
std::shared_ptr<msg::StreamTags> meta_;
std::shared_ptr<Properties> properties_;
boost::asio::io_context& ioc_;
ServerSettings server_settings_;
std::unique_ptr<CtrlScript> ctrl_script_;

View file

@ -20,6 +20,7 @@
#include "catch.hpp"
#include "common/aixlog.hpp"
#include "common/metatags.hpp"
#include "common/properties.hpp"
#include "common/utils/string_utils.hpp"
#include "server/streamreader/stream_uri.hpp"
@ -153,3 +154,44 @@ TEST_CASE("Metatags")
std::cout << out_json.dump(4) << "\n";
REQUIRE(in_json == out_json);
}
TEST_CASE("Properties")
{
REQUIRE(to_string(PlaybackStatus::kPaused) == "paused");
auto in_json = json::parse(R"(
{
"playbackStatus": "playing",
"loopStatus": "track",
"shuffle": false,
"volume": 42,
"elapsed": 23.0
}
)");
std::cout << in_json.dump(4) << "\n";
Properties props(in_json);
std::cout << props.toJson().dump(4) << "\n";
REQUIRE(props.playback_status.has_value());
auto out_json = props.toJson();
std::cout << out_json.dump(4) << "\n";
REQUIRE(in_json == out_json);
in_json = json::parse(R"(
{
"volume": 42
}
)");
std::cout << in_json.dump(4) << "\n";
props.fromJson(in_json);
std::cout << props.toJson().dump(4) << "\n";
REQUIRE(!props.playback_status.has_value());
out_json = props.toJson();
std::cout << out_json.dump(4) << "\n";
REQUIRE(in_json == out_json);
}