Codebase list snapcast / 9eb0036
New upstream version 0.18.0 Felix Geyer 4 years ago
153 changed file(s) with 4639 addition(s) and 2915 deletion(s). Raw diff Collapse all Expand all
66 [submodule "externals/tremor"]
77 path = externals/tremor
88 url = https://git.xiph.org/tremor.git
9 [submodule "externals/opus"]
10 path = externals/opus
11 url = https://git.xiph.org/opus.git
1414 - sourceline: 'ppa:mhier/libboost-latest'
1515 - ubuntu-toolchain-r-test
1616 packages:
17 - g++-4.9 boost1.70 libasound2-dev libvorbisidec-dev libvorbis-dev libflac-dev alsa-utils libavahi-client-dev avahi-daemon
17 - g++-4.9 boost1.70 libasound2-dev libvorbisidec-dev libvorbis-dev libflac-dev libopus-dev alsa-utils libavahi-client-dev avahi-daemon
1818
1919 - os: linux
2020 compiler: gcc
2626 - sourceline: 'ppa:mhier/libboost-latest'
2727 - ubuntu-toolchain-r-test
2828 packages:
29 - g++-5 boost1.70 libasound2-dev libvorbisidec-dev libvorbis-dev libflac-dev alsa-utils libavahi-client-dev avahi-daemon
29 - g++-5 boost1.70 libasound2-dev libvorbisidec-dev libvorbis-dev libflac-dev libopus-dev alsa-utils libavahi-client-dev avahi-daemon
3030
3131 - os: linux
3232 compiler: gcc
3838 - sourceline: 'ppa:mhier/libboost-latest'
3939 - ubuntu-toolchain-r-test
4040 packages:
41 - g++-6 boost1.70 libasound2-dev libvorbisidec-dev libvorbis-dev libflac-dev alsa-utils libavahi-client-dev avahi-daemon
41 - g++-6 boost1.70 libasound2-dev libvorbisidec-dev libvorbis-dev libflac-dev libopus-dev alsa-utils libavahi-client-dev avahi-daemon
4242
4343 - os: linux
4444 compiler: gcc
5050 - sourceline: 'ppa:mhier/libboost-latest'
5151 - ubuntu-toolchain-r-test
5252 packages:
53 - g++-7 boost1.70 libasound2-dev libvorbisidec-dev libvorbis-dev libflac-dev alsa-utils libavahi-client-dev avahi-daemon
53 - g++-7 boost1.70 libasound2-dev libvorbisidec-dev libvorbis-dev libflac-dev libopus-dev alsa-utils libavahi-client-dev avahi-daemon
5454
5555 - os: linux
5656 compiler: gcc
6262 - sourceline: 'ppa:mhier/libboost-latest'
6363 - ubuntu-toolchain-r-test
6464 packages:
65 - g++-8 boost1.70 libasound2-dev libvorbisidec-dev libvorbis-dev libflac-dev alsa-utils libavahi-client-dev avahi-daemon
65 - g++-8 boost1.70 libasound2-dev libvorbisidec-dev libvorbis-dev libflac-dev libopus-dev alsa-utils libavahi-client-dev avahi-daemon
6666
6767 - os: linux
6868 compiler: gcc
7474 - sourceline: 'ppa:mhier/libboost-latest'
7575 - ubuntu-toolchain-r-test
7676 packages:
77 - g++-9 boost1.70 libasound2-dev libvorbisidec-dev libvorbis-dev libflac-dev alsa-utils libavahi-client-dev avahi-daemon
77 - g++-9 boost1.70 libasound2-dev libvorbisidec-dev libvorbis-dev libflac-dev libopus-dev alsa-utils libavahi-client-dev avahi-daemon
7878
7979
8080 - os: linux
8989 - sourceline: 'ppa:mhier/libboost-latest'
9090 - llvm-toolchain-trusty-3.9
9191 packages:
92 - clang-3.9 boost1.70 libasound2-dev libvorbisidec-dev libvorbis-dev libflac-dev alsa-utils libavahi-client-dev avahi-daemon
92 - clang-3.9 boost1.70 libasound2-dev libvorbisidec-dev libvorbis-dev libflac-dev libopus-dev alsa-utils libavahi-client-dev avahi-daemon
9393
9494 - os: linux
9595 compiler: clang
103103 - sourceline: 'ppa:mhier/libboost-latest'
104104 - llvm-toolchain-trusty-4.0
105105 packages:
106 - clang-4.0 boost1.70 libasound2-dev libvorbisidec-dev libvorbis-dev libflac-dev alsa-utils libavahi-client-dev avahi-daemon
106 - clang-4.0 boost1.70 libasound2-dev libvorbisidec-dev libvorbis-dev libflac-dev libopus-dev alsa-utils libavahi-client-dev avahi-daemon
107107
108108 - os: linux
109109 compiler: clang
117117 - sourceline: 'ppa:mhier/libboost-latest'
118118 - llvm-toolchain-trusty-5.0
119119 packages:
120 - clang-5.0 boost1.70 libasound2-dev libvorbisidec-dev libvorbis-dev libflac-dev alsa-utils libavahi-client-dev avahi-daemon
120 - clang-5.0 boost1.70 libasound2-dev libvorbisidec-dev libvorbis-dev libflac-dev libopus-dev alsa-utils libavahi-client-dev avahi-daemon
121121
122122 - os: linux
123123 compiler: clang
131131 - llvm-toolchain-trusty-6.0
132132 - ubuntu-toolchain-r-test
133133 packages:
134 - clang-6.0 boost1.70 libasound2-dev libvorbisidec-dev libvorbis-dev libflac-dev alsa-utils libavahi-client-dev avahi-daemon
134 - clang-6.0 boost1.70 libasound2-dev libvorbisidec-dev libvorbis-dev libflac-dev libopus-dev alsa-utils libavahi-client-dev avahi-daemon
135135
136136 - os: linux
137137 compiler: clang
145145 - llvm-toolchain-trusty-7
146146 - ubuntu-toolchain-r-test
147147 packages:
148 - clang-7 boost1.70 libasound2-dev libvorbisidec-dev libvorbis-dev libflac-dev alsa-utils libavahi-client-dev avahi-daemon
148 - clang-7 boost1.70 libasound2-dev libvorbisidec-dev libvorbis-dev libflac-dev libopus-dev alsa-utils libavahi-client-dev avahi-daemon
149149
150150 # build on osx
151151 - os: osx
152152 osx_image: xcode9.4
153153 env:
154 - MATRIX_EVAL="brew update && brew upgrade boost && brew install flac libvorbis"
154 - MATRIX_EVAL="brew update && brew upgrade boost && brew install flac opus libvorbis"
155155
156156 - os: osx
157157 osx_image: xcode10.3
158158 env:
159 - MATRIX_EVAL="brew update && brew upgrade boost && brew install flac libvorbis"
159 - MATRIX_EVAL="brew update && brew install flac opus libvorbis"
160160
161161 - os: osx
162162 osx_image: xcode11
163163 env:
164 - MATRIX_EVAL="brew update && brew install flac libvorbis"
164 - MATRIX_EVAL="brew update && brew install flac opus libvorbis"
165165
166166 before_install:
167167 - eval "${MATRIX_EVAL}"
172172
173173 - mkdir build
174174 - cd build
175 - cmake .. && make && sudo make install
175 - cmake -DCMAKE_CXX_FLAGS="$CXXFLAGS -Werror -Wall -Wextra -pedantic -Wno-unused-parameter -Wno-unused-function -O2" .. && make && sudo make install
00 cmake_minimum_required(VERSION 3.2)
11
2 project(snapcast LANGUAGES CXX VERSION 0.16.0)
2 project(snapcast LANGUAGES CXX VERSION 0.18.0)
33 set(PROJECT_DESCRIPTION "Multi-room client-server audio player")
44 set(PROJECT_URL "https://github.com/badaix/snapcast")
55
1313 option(BUILD_WITH_FLAC "Build with FLAC support" ON)
1414 option(BUILD_WITH_VORBIS "Build with VORBIS support" ON)
1515 option(BUILD_WITH_TREMOR "Build with vorbis using TREMOR" ON)
16 option(BUILD_WITH_OPUS "Build with OPUS support" ON)
1617 option(BUILD_WITH_AVAHI "Build with AVAHI support" ON)
1718
1819
7677 #message(STATUS "Architecture: ${ARCH}")
7778 #message(STATUS "System processor: ${CMAKE_SYSTEM_PROCESSOR}")
7879
79 INCLUDE(CheckLibraryExists)
80 check_library_exists(atomic __atomic_fetch_add_4 "" HAS_LIBATOMIC)
81 if(HAS_LIBATOMIC)
82 set(CMAKE_CXX_LINK_FLAGS "${CMAKE_CXX_LINK_FLAGS} -latomic")
83 endif()
80 include(CheckAtomic)
8481
8582 INCLUDE(TestBigEndian)
8683 TEST_BIG_ENDIAN(BIGENDIAN)
172169 if(BUILD_WITH_VORBIS)
173170 pkg_search_module(VORBISENC vorbisenc)
174171 if (VORBISENC_FOUND)
175 add_definitions("-DHAS_VORBISENC")
172 add_definitions("-DHAS_VORBIS_ENC")
176173 endif(VORBISENC_FOUND)
177174 endif()
178175
179 find_package(Boost 1.66 REQUIRED)
176 if(BUILD_WITH_OPUS)
177 pkg_search_module(OPUS opus)
178 if (OPUS_FOUND)
179 add_definitions("-DHAS_OPUS")
180 endif (OPUS_FOUND)
181 endif()
182
183 find_package(Boost 1.70 REQUIRED)
184 add_definitions("-DBOOST_ERROR_CODE_HEADER_ONLY")
180185
181186 add_subdirectory(common)
182187
22
33 ![Snapcast](https://raw.githubusercontent.com/badaix/snapcast/master/doc/Snapcast_800.png)
44
5 **S**y**n**chronous **a**udio **p**layer
6
5 **S**y**n**chronous **a**udio **p**layer
6
77 [![Build Status](
88 https://travis-ci.org/badaix/snapcast.svg?branch=master)](https://travis-ci.org/badaix/snapcast)
99 [![Github Releases](https://img.shields.io/github/release/badaix/snapcast.svg)](https://github.com/badaix/snapcast/releases)
10 [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.me/badaix)
1011
1112 Snapcast is a multi-room client-server audio player, where all clients are time synchronized with the server to play perfectly synced audio. It's not a standalone player, but an extension that turns your existing audio player into a Sonos-like multi-room solution.
12 The server's audio input is a named pipe `/tmp/snapfifo`. All data that is fed into this file will be send to the connected clients. One of the most generic ways to use Snapcast is in conjunction with the music player daemon ([MPD](http://www.musicpd.org/)) or [Mopidy](https://www.mopidy.com/), which can be configured to use a named pipe as audio output.
13 The server's audio input is a named pipe `/tmp/snapfifo`. All data that is fed into this file will be sent to the connected clients. One of the most generic ways to use Snapcast is in conjunction with the music player daemon ([MPD](http://www.musicpd.org/)) or [Mopidy](https://www.mopidy.com/), which can be configured to use a named pipe as audio output.
1314
1415 How does it work
1516 ----------------
1718 * **PCM** lossless uncompressed
1819 * **FLAC** lossless compressed [default]
1920 * **Vorbis** lossy compression
21 * **Opus** lossy low-latency compression
2022
2123 The encoded chunk is sent via a TCP connection to the Snapclients.
22 Each client does continuos time synchronization with the server, so that the client is always aware of the local server time.
24 Each client does continuous time synchronization with the server, so that the client is always aware of the local server time.
2325 Every received chunk is first decoded and added to the client's chunk-buffer. Knowing the server's time, the chunk is played out using ALSA at the appropriate time. Time deviations are corrected by
2426 * skipping parts or whole chunks
2527 * playing silence
2729
2830 Typically the deviation is smaller than 1ms.
2931
32 For more information on the binary protocol, please see the [documentation](doc/binary_protocol.md).
33
3034 Installation
3135 ------------
32 You can either build and install snapcast from source, or on debian systems install a prebuild .deb package
36 You can either build and install snapcast from source, or on Debian systems install a prebuilt .deb package
3337
3438 ### Installation from source
3539 Please follow this [guide](doc/build.md) to build Snapcast for
3943 * [Android](doc/build.md#android-cross-compile)
4044 * [OpenWrt](doc/build.md#openwrtlede-cross-compile)
4145 * [Buildroot](doc/build.md#buildroot-cross-compile)
42 * [Raspberry Pi](doc/build.md#raspberry-pi-cross-compile)
46 * [Raspberry Pi](doc/build.md#raspberry-pi-cross-compile)
4347
44 ### Install linux packages
45 For Debian download the package for your CPU architecture from the [latest release page](https://github.com/badaix/snapcast/releases/latest), e.g. for Raspberry pi `snapclient_0.x.x_armhf.deb`
48 ### Install Linux packages
49 For Debian download the package for your CPU architecture from the [latest release page](https://github.com/badaix/snapcast/releases/latest), e.g. for Raspberry Pi `snapclient_0.x.x_armhf.deb`
4650 Install the package:
4751
4852 $ sudo dpkg -i snapclient_0.x.x_armhf.deb
5559
5660 $ opkg install snapclient_0.x.x_ar71xx.ipk
5761
58 On Alpine Linux (testing repository) do:
62 On Alpine Linux do:
5963
6064 $ apk add snapcast
65
66 # Or for just the client:
67
68 $ apk add snapcast-client
69
70 # Or for just the server:
71
72 $ apk add snapcast-server
6173
6274 On Gentoo Linux do:
6375
6476 $ emerge --ask media-sound/snapcast
65
77
78 On Archlinux, snapcast is available through the AUR. To install, use your favorite AUR helper, or do:
79
80 $ git clone https://aur.archlinux.org/snapcast
81 $ cd snapcast
82 $ makepkg -si
83
6684 SnapOS
6785 ------
68 For the brave of you, there is a guide with buildfiles available to build [SnapOS](https://github.com/badaix/snapos), a small and fast-booting OS to run Snapcast, comming in two flavors: [Buildroot](https://github.com/badaix/snapos/blob/master/buildroot-external/README.md) based, or [OpenWrt](https://github.com/badaix/snapos/tree/master/openwrt) based. Please note that there are no pre-build firmware packages available.
86 For the brave of you, there is a guide with buildfiles available to build [SnapOS](https://github.com/badaix/snapos), a small and fast-booting OS to run Snapcast, coming in two flavors: [Buildroot](https://github.com/badaix/snapos/blob/master/buildroot-external/README.md) based, or [OpenWrt](https://github.com/badaix/snapos/tree/master/openwrt) based. Please note that there are no pre-built firmware packages available.
6987
7088 Configuration
7189 -------------
8199 ```
82100
83101 The pipe stream (`stream = pipe`) will per default create the pipe. Sometimes your audio source might insist in creating the pipe itself. So the pipe creation mode can by changed to "not create, but only read mode", using the `mode` option set to `create` or `read`:
84
102
85103 stream = pipe:///tmp/snapfifo?name=Radio&mode=read"
86
104
87105 Test
88106 ----
89107 You can test your installation by copying random data into the server's fifo file
91109 $ sudo cat /dev/urandom > /tmp/snapfifo
92110
93111 All connected clients should play random noise now. You might raise the client's volume with "alsamixer".
94 It's also possible to let the server play a wave file. Simply configure a `file` stream in `/etc/snapserver.conf`, and restart the server:
112 It's also possible to let the server play a WAV file. Simply configure a `file` stream in `/etc/snapserver.conf`, and restart the server:
95113
96114 ```
97115 [stream]
98116 stream = file:///home/user/Musik/Some%20wave%20file.wav?name=test
99117 ```
100118
101 When you are using a Raspberry pi, you might have to change your audio output to the 3.5mm jack:
119 When you are using a Raspberry Pi, you might have to change your audio output to the 3.5mm jack:
102120
103121 #The last number is the audio output with 1 being the 3.5 jack, 2 being HDMI and 0 being auto.
104122 $ amixer cset numid=3 1
105123
106 To setup WiFi on a raspberry pi, you can follow this guide:
124 To setup WiFi on a Raspberry Pi, you can follow this guide:
107125 https://www.raspberrypi.org/documentation/configuration/wireless/wireless-cli.md
108126
109127 Control
120138 ![Snapcast for Android](https://raw.githubusercontent.com/badaix/snapcast/master/doc/snapcast_android_scaled.png)
121139
122140 There is also an unofficial WebApp from @atoomic [atoomic/snapcast-volume-ui](https://github.com/atoomic/snapcast-volume-ui).
123 This app list all clients connected to a server and allow to control individualy the volume of each client.
141 This app lists all clients connected to a server and allows you to control individually the volume of each client.
124142 Once installed, you can use any mobile device, laptop, desktop, or browser.
125143
126 There is also an [unofficial FHEM module](https://forum.fhem.de/index.php/topic,62389.0.html) from @unimatrix27 which integrates a snapcast controller in to the [FHEM](https://fhem.de/fhem.html) home automation system.
144 There is also an [unofficial FHEM module](https://forum.fhem.de/index.php/topic,62389.0.html) from @unimatrix27 which integrates a snapcast controller into the [FHEM](https://fhem.de/fhem.html) home automation system.
127145
128146 There is a [snapcast component for Home Assistant](https://home-assistant.io/components/media_player.snapcast/) which integrates a snapcast controller in to the [Home Assistant](https://home-assistant.io/) home automation system.
129147
130 For a webinterface in python, see [snapcastr](https://github.com/xkonni/snapcastr), based on [python-snapcast](https://github.com/happyleavesaoc/python-snapcast). This interface controls client volume and assigns streams to groups.
148 For a web interface in Python, see [snapcastr](https://github.com/xkonni/snapcastr), based on [python-snapcast](https://github.com/happyleavesaoc/python-snapcast). This interface controls client volume and assigns streams to groups.
131149
132 Another webinterface running on any device, see [snapcast-websockets-ui](https://github.com/derglaus/snapcast-websockets-ui), running entirely in the browser, needs [websockify](https://github.com/novnc/websockify). No configuration needed, features almost all functions, still needs some tuning for the optics.
150 Another web interface running on any device is [snapcast-websockets-ui](https://github.com/derglaus/snapcast-websockets-ui), running entirely in the browser, which needs [websockify](https://github.com/novnc/websockify). No configuration needed; features almost all functions; still needs some tuning for the optics.
133151
134 A webinterface called [HydraPlay](https://github.com/mariolukas/HydraPlay) which integrates Snapcast and multiple Mopidy instances. It is JavaScript based and uses Angular 7. A Snapcast websocket proxy server is needed to connect Snapcast to HydraPlay over web sockets.
152 A web interface called [HydraPlay](https://github.com/mariolukas/HydraPlay) integrates Snapcast and multiple Mopidy instances. It is JavaScript based and uses Angular 7. A Snapcast web socket proxy server is needed to connect Snapcast to HydraPlay over web sockets.
135153
136154 Setup of audio players/server
137155 -----------------------------
158176 Roadmap
159177 -------
160178 Unordered list of features that should make it into the v1.0
161 - [X] **Remote control** JSON-RPC API to change client latency, volume, zone, ...
179 - [X] **Remote control** JSON-RPC API to change client latency, volume, zone,...
162180 - [X] **Android client** JSON-RPC client and Snapclient
163181 - [X] **Streams** Support multiple streams
164182 - [X] **Debian packages** prebuild deb packages
168186 - [X] **Groups** support multiple Groups of clients ("Zones")
169187 - [ ] **JSON-RPC** Possibility to add, remove, rename streams
170188 - [ ] **Protocol specification** Snapcast binary streaming protocol, JSON-RPC protocol
171 - [ ] **Ports** Snapclient for Windows, ~~Mac OS X~~, ...
189 - [ ] **Ports** Snapclient for Windows, ~~Mac OS X~~,...
0 TODO
1 ====
2
3 General
4 -------
5
6 - Server ping client?
7 - LastSeen: relative time [s] or [ms]?
8 - Android crash: Empty latency => app restart => empty client list
9 - Android clean data structures after changing the Server
10 - UDP based audio streaming
11
12 Server
13 ------
14
15 - Provide io_context to stream-readers
16 - [fd stream](https://gstreamer.freedesktop.org/data/doc/gstreamer/head/gstreamer-plugins/html/gstreamer-plugins-fdsink.html)
17 - UDP/TCP stream
18 - gstreamer snapcast sink plugin?
19 - "sync" option for streams (realtime reading vs read as much as available)
20 - Override conf file settings on command line
21
22 Client
23 ------
24
25 - revise asio stuff
00 # Snapcast changelog
1
2 ## Version 0.18.0
3
4 ### Features
5
6 - Add TCP stream reader
7
8 ### Bugfixes
9
10 - Client: fix hostname reporting on Android
11 - Fix some small memory leaks
12 - Fix Librespot stream causing zombie processes (Issue #530)
13 - Process stream watchdog is configurable (Issue #517)
14 - Fix Makefile for macOS (Issues #510, #514)
15
16 ### General
17
18 - Refactored stream readers
19 - Server can run on a single thread
20 - Configurable number of server worker threads
21
22 _Johannes Pohl <snapcast@badaix.de> Wed, 22 Jan 2020 00:13:37 +0200_
23
24 ## Version 0.17.1
25
26 ### Bugfixes
27
28 - Fix compile error if u_char is not defined (Issue #506)
29 - Fix error "exception unknown codec ogg" (Issue #504)
30 - Fix random crash during client disconnect
31
32 _Johannes Pohl <snapcast@badaix.de> Sat, 23 Nov 2019 00:13:37 +0200_
33
34 ## Version 0.17.0
35
36 ### Features
37
38 - Support for Opus low-latency codec (PR #4)
39
40 ### Bugfixes
41
42 - CMake: fix check for libatomic (Issue #490, PR #494)
43 - WebUI: interface.html uses the server's IP for the websocket connection
44 - Fix warnings (Issue #484)
45 - Fix lock order inversions and data races identified by thread sanitizer
46 - Makefiles: fix install targets (PR #493)
47 - Makefiles: LDFLAGS are added from environment (PR #492)
48 - CMake: required Boost version is raised to 1.70 (Issue #488)
49 - Fix crash in websocket server
50
51 ### General
52
53 - Stream server uses less threads (one in total, instead of 1+2n)
54 - Changing group volume is much more responsive for larger groups
55 - Unknown snapserver.conf options are logged as warning (Issue #487)
56 - debian scripts: change usernames back to snapclient and snapserver
57
58 _Johannes Pohl <snapcast@badaix.de> Wed, 20 Nov 2019 00:13:37 +0200_
159
260 ## Version 0.16.0
361
2280 - Snapcast depends on boost::asio and boost::beast (header only)
2381 - Tidy up code base
2482 - Makefile doesn't statically link libgcc and libstdc++
25
26 _Johannes Pohl <snapcast@badaix.de> Sat, 13 Oct 2018 00:13:37 +0200_
83 - debian scripts: change usernames to _snapclient and _snapserver
84
85 _Johannes Pohl <snapcast@badaix.de> Sat, 13 Oct 2019 00:13:37 +0200_
2786
2887 ## Version 0.15.0
2988
6767 list(APPEND CLIENT_INCLUDE ${FLAC_INCLUDE_DIRS})
6868 endif (FLAC_FOUND)
6969
70 if (OPUS_FOUND)
71 list(APPEND CLIENT_SOURCES decoder/opus_decoder.cpp)
72 list(APPEND CLIENT_LIBRARIES ${OPUS_LIBRARIES})
73 list(APPEND CLIENT_INCLUDE ${OPUS_INCLUDE_DIRS})
74 endif (OPUS_FOUND)
75
7076 include_directories(${CLIENT_INCLUDE})
7177 add_executable(snapclient ${CLIENT_SOURCES})
7278 target_link_libraries(snapclient ${CLIENT_LIBRARIES})
00 # This file is part of snapcast
1 # Copyright (C) 2014-2019 Johannes Pohl
1 # Copyright (C) 2014-2020 Johannes Pohl
22 #
33 # This program is free software: you can redistribute it and/or modify
44 # it under the terms of the GNU General Public License as published by
1313 # You should have received a copy of the GNU General Public License
1414 # along with this program. If not, see <http://www.gnu.org/licenses/>.
1515
16 VERSION = 0.16.0
16 VERSION = 0.18.0
1717 BIN = snapclient
1818
1919 ifeq ($(TARGET), FREEBSD)
2929 TARGET_DIR ?= /usr
3030 endif
3131
32 # Simplify building debuggable executables 'make DEBUG=-g STRIP=echo'
33 DEBUG=-O3
34
35
36 CXXFLAGS += $(ADD_CFLAGS) -std=c++0x -Wall -Wextra -Wpedantic -Wno-unused-function $(DEBUG) -DHAS_FLAC -DHAS_OGG -DVERSION=\"$(VERSION)\" -I. -I.. -I../common
37 LDFLAGS = $(ADD_LDFLAGS) -logg -lFLAC
38 OBJ = snapclient.o stream.o client_connection.o time_provider.o player/player.o decoder/pcm_decoder.o decoder/ogg_decoder.o decoder/flac_decoder.o controller.o ../common/sample_format.o
32 DEBUG ?= 0
33 ifeq ($(DEBUG), 1)
34 CXXFLAGS += -g
35 else
36 CXXFLAGS += -O2
37 endif
38
39 ifneq ($(SANITIZE), )
40 CXXFLAGS += -fsanitize=$(SANITIZE) -g
41 LDFLAGS += -fsanitize=$(SANITIZE)
42 endif
43
44 CXXFLAGS += $(ADD_CFLAGS) -std=c++14 -Wall -Wextra -Wpedantic -Wno-unused-function -DBOOST_ERROR_CODE_HEADER_ONLY -DHAS_FLAC -DHAS_OGG -DHAS_OPUS -DVERSION=\"$(VERSION)\" -I. -I.. -I../common
45 LDFLAGS += $(ADD_LDFLAGS) -logg -lFLAC -lopus
46 OBJ = snapclient.o stream.o client_connection.o time_provider.o player/player.o decoder/pcm_decoder.o decoder/ogg_decoder.o decoder/flac_decoder.o decoder/opus_decoder.o controller.o ../common/sample_format.o
3947
4048
4149 ifneq (,$(TARGET))
4957 ifeq ($(TARGET), ANDROID)
5058
5159 CXX = $(PROGRAM_PREFIX)clang++
52 STRIP = $(PROGRAM_PREFIX)strip
5360 CXXFLAGS += -pthread -fPIC -DHAS_TREMOR -DHAS_OPENSL -I$(NDK_DIR)/include
54 LDFLAGS = -L$(NDK_DIR)/lib -pie -lvorbisidec -logg -lFLAC -lOpenSLES -latomic -llog -static-libstdc++
61 LDFLAGS = -L$(NDK_DIR)/lib -pie -lvorbisidec -logg -lopus -lFLAC -lOpenSLES -latomic -llog -static-libstdc++
5562 OBJ += player/opensl_player.o
5663
5764 else ifeq ($(TARGET), OPENWRT)
5865
59 STRIP = echo
6066 CXXFLAGS += -pthread -DNO_CPP11_STRING -DHAS_TREMOR -DHAS_ALSA -DHAS_AVAHI -DHAS_DAEMON
6167 LDFLAGS += -lasound -lvorbisidec -lavahi-client -lavahi-common -latomic
6268 OBJ += ../common/daemon.o player/alsa_player.o browseZeroConf/browse_avahi.o
7076 else ifeq ($(TARGET), MACOS)
7177
7278 CXX = g++
73 STRIP = strip
7479 CXXFLAGS += -DHAS_COREAUDIO -DHAS_VORBIS -DFREEBSD -DHAS_BONJOUR -DHAS_DAEMON -I/usr/local/include -Wno-unused-local-typedef -Wno-deprecated
7580 LDFLAGS += -lvorbis -lFLAC -L/usr/local/lib -framework AudioToolbox -framework CoreAudio -framework CoreFoundation -framework IOKit
7681 OBJ += ../common/daemon.o player/coreaudio_player.o browseZeroConf/browse_bonjour.o
7883 else
7984
8085 CXX = g++
81 STRIP = strip
8286 CXXFLAGS += -pthread -DHAS_VORBIS -DHAS_ALSA -DHAS_AVAHI -DHAS_DAEMON
8387 LDFLAGS += -lrt -lasound -lvorbis -lavahi-client -lavahi-common -latomic
8488 OBJ += ../common/daemon.o player/alsa_player.o browseZeroConf/browse_avahi.o
101105 else ifeq ($(ARCH), mips)
102106 $(eval CXXFLAGS:=$(CXXFLAGS) -DIS_BIG_ENDIAN)
103107 $(eval PROGRAM_PREFIX:=$(NDK_DIR)/bin/mipsel-linux-android-)
104 else
108 else ifeq ($(ARCH), arm)
105109 $(eval CXXFLAGS:=$(CXXFLAGS) -march=armv7)
106110 $(eval PROGRAM_PREFIX:=$(NDK_DIR)/bin/arm-linux-androideabi-)
111 else ifeq ($(ARCH), arm64)
112 $(eval PROGRAM_PREFIX:=$(NDK_DIR)/bin/aarch64-linux-android-)
107113 endif
108114 endif
109115
110116 $(BIN): $(OBJ)
111117 $(CXX) $(CXXFLAGS) -o $(BIN) $(OBJ) $(LDFLAGS)
112 $(STRIP) $(BIN)
113118
114119 %.o: %.cpp
115120 $(CXX) $(CXXFLAGS) -c $< -o $@
126131
127132 install:
128133 echo macOS
129 install -g wheel -o root $(BIN) $(TARGET_DIR)/local/bin/$(BIN)
134 install -s -g wheel -o root $(BIN) $(TARGET_DIR)/local/bin/$(BIN)
130135 install -g wheel -o root $(BIN).1 $(TARGET_DIR)/local/share/man/man1/$(BIN).1
131 install -g wheel -o root debian/$(BIN).plist /Library/LaunchAgents/de.badaix.snapcast.$(BIN).plist
136 install -g wheel -o root etc/$(BIN).plist /Library/LaunchAgents/de.badaix.snapcast.$(BIN).plist
132137 launchctl load /Library/LaunchAgents/de.badaix.snapcast.$(BIN).plist
133138
134139 else
151156 endif
152157
153158 installfiles:
154 install -D -g root -o root $(BIN) $(TARGET_DIR)/bin/$(BIN)
159 install -s -D -g root -o root $(BIN) $(TARGET_DIR)/bin/$(BIN)
155160 install -D -g root -o root $(BIN).1 $(TARGET_DIR)/share/man/man1/$(BIN).1
156161
157162 installsystemd:
158163 @echo using systemd; \
159 cp debian/$(BIN).service /lib/systemd/system/$(BIN).service; \
160 cp -n debian/$(BIN).default /etc/default/$(BIN); \
164 cp ../debian/$(BIN).service /lib/systemd/system/$(BIN).service; \
165 cp -n ../debian/$(BIN).default /etc/default/$(BIN); \
161166 systemctl daemon-reload; \
162167 systemctl enable $(BIN); \
163168 systemctl start $(BIN); \
164169
165170 installsysv:
166171 @echo using sysv; \
167 cp debian/$(BIN).init /etc/init.d/$(BIN); \
168 cp -n debian/$(BIN).default /etc/default/$(BIN); \
172 cp ../debian/$(BIN).init /etc/init.d/$(BIN); \
173 cp -n ../debian/$(BIN).default /etc/default/$(BIN); \
169174 update-rc.d $(BIN) defaults; \
170175 /etc/init.d/$(BIN) start; \
171176
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
166166 }
167167 }
168168
169 bool BrowseBonjour::browse(const string& serviceName, mDNSResult& result, int timeout)
169 bool BrowseBonjour::browse(const string& serviceName, mDNSResult& result, int /*timeout*/)
170170 {
171171 result.valid = false;
172172 // Discover
174174 {
175175 DNSServiceHandle service(new DNSServiceRef(NULL));
176176 CHECKED(DNSServiceBrowse(service.get(), 0, 0, serviceName.c_str(), "local.",
177 [](DNSServiceRef service, DNSServiceFlags flags, uint32_t interfaceIndex, DNSServiceErrorType errorCode,
177 [](DNSServiceRef /*service*/, DNSServiceFlags /*flags*/, uint32_t /*interfaceIndex*/, DNSServiceErrorType errorCode,
178178 const char* serviceName, const char* regtype, const char* replyDomain, void* context) {
179179 auto replyCollection = static_cast<deque<mDNSReply>*>(context);
180180
191191 {
192192 DNSServiceHandle service(new DNSServiceRef(NULL));
193193 for (auto& reply : replyCollection)
194 CHECKED(
195 DNSServiceResolve(service.get(), 0, 0, reply.name.c_str(), reply.regtype.c_str(), reply.domain.c_str(),
196 [](DNSServiceRef service, DNSServiceFlags flags, uint32_t interfaceIndex, DNSServiceErrorType errorCode, const char* fullName,
197 const char* hosttarget, uint16_t port, uint16_t txtLen, const unsigned char* txtRecord, void* context) {
198 auto resultCollection = static_cast<deque<mDNSResolve>*>(context);
199
200 CHECKED(errorCode);
201 resultCollection->push_back(mDNSResolve{string(hosttarget), ntohs(port)});
202 },
203 &resolveCollection));
194 CHECKED(DNSServiceResolve(service.get(), 0, 0, reply.name.c_str(), reply.regtype.c_str(), reply.domain.c_str(),
195 [](DNSServiceRef /*service*/, DNSServiceFlags /*flags*/, uint32_t /*interfaceIndex*/, DNSServiceErrorType errorCode,
196 const char* /*fullName*/, const char* hosttarget, uint16_t port, uint16_t /*txtLen*/,
197 const unsigned char* /*txtRecord*/, void* context) {
198 auto resultCollection = static_cast<deque<mDNSResolve>*>(context);
199
200 CHECKED(errorCode);
201 resultCollection->push_back(mDNSResolve{string(hosttarget), ntohs(port)});
202 },
203 &resolveCollection));
204204
205205 runService(service);
206206 }
214214 {
215215 resultCollection[i].port = resolve.port;
216216 CHECKED(DNSServiceGetAddrInfo(service.get(), kDNSServiceFlagsLongLivedQuery, 0, kDNSServiceProtocol_IPv4, resolve.fullName.c_str(),
217 [](DNSServiceRef service, DNSServiceFlags flags, uint32_t interfaceIndex, DNSServiceErrorType errorCode,
218 const char* hostname, const sockaddr* address, uint32_t ttl, void* context) {
217 [](DNSServiceRef /*service*/, DNSServiceFlags /*flags*/, uint32_t interfaceIndex, DNSServiceErrorType /*errorCode*/,
218 const char* hostname, const sockaddr* address, uint32_t /*ttl*/, void* context) {
219219 auto result = static_cast<mDNSResult*>(context);
220220
221221 result->host = string(hostname);
00 #/bin/sh
11
2 if [ -z "$NDK_DIR_ARM" ] && [ -z "$NDK_DIR_X86" ]; then
3 echo "Specify at least one NDK_DIR_[ARM|X86]"
2 if [ -z "$NDK_DIR_ARM" ] && [ -z "$NDK_DIR_ARM64" ] && [ -z "$NDK_DIR_X86" ]; then
3 echo "Specify at least one NDK_DIR_[ARM|ARM64|X86]"
44 exit
55 fi
66
7 if [ -z "$ASSETS_DIR" ]; then
8 echo "Specify the snapdroid assets root dir ASSETS_DIR"
7 if [ -z "$JNI_LIBS_DIR" ]; then
8 echo "Specify the snapdroid jniLibs dir JNI_LIBS_DIR"
99 exit
1010 fi
1111
1212 if [ -n "$NDK_DIR_ARM" ]; then
1313 export NDK_DIR="$NDK_DIR_ARM"
1414 export ARCH=arm
15 make clean; make TARGET=ANDROID -j 4; mv ./snapclient "$ASSETS_DIR/bin/armeabi/"
15 make clean; make TARGET=ANDROID -j 4; $NDK_DIR/bin/arm-linux-androideabi-strip ./snapclient; mv ./snapclient "$JNI_LIBS_DIR/armeabi/libsnapclient.so"
16 fi
17
18 if [ -n "$NDK_DIR_ARM64" ]; then
19 export NDK_DIR="$NDK_DIR_ARM64"
20 export ARCH=arm64
21 make clean; make TARGET=ANDROID -j 4; $NDK_DIR/bin/aarch64-linux-android-strip ./snapclient; mv ./snapclient "$JNI_LIBS_DIR/arm64-v8a/libsnapclient.so"
1622 fi
1723
1824 if [ -n "$NDK_DIR_X86" ]; then
1925 export NDK_DIR="$NDK_DIR_X86"
2026 export ARCH=x86
21 make clean; make TARGET=ANDROID -j 4; mv ./snapclient "$ASSETS_DIR/bin/x86/"
27 make clean; make TARGET=ANDROID -j 4; $NDK_DIR/bin/i686-linux-android-strip ./snapclient; mv ./snapclient "$JNI_LIBS_DIR/x86/libsnapclient.so"
2228 fi
00 #/bin/sh
11
22 export NDK_DIR_ARM="$1-arm"
3 export NDK_DIR_ARM64="$1-arm64"
34 export NDK_DIR_X86="$1-x86"
4 export ASSETS_DIR="$2"
5 export JNI_LIBS_DIR="$2"
56
67 ./build_android.sh
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
1919 #include "common/aixlog.hpp"
2020 #include "common/snap_exception.hpp"
2121 #include "common/str_compat.hpp"
22 #include "message/factory.hpp"
2223 #include "message/hello.hpp"
2324 #include <iostream>
2425 #include <mutex>
2829
2930
3031 ClientConnection::ClientConnection(MessageReceiver* receiver, const std::string& host, size_t port)
31 : socket_(nullptr), active_(false), connected_(false), messageReceiver_(receiver), reqId_(1), host_(host), port_(port), readerThread_(nullptr),
32 : socket_(io_context_), active_(false), messageReceiver_(receiver), reqId_(1), host_(host), port_(port), readerThread_(nullptr),
3233 sumTimeout_(chronos::msec(0))
3334 {
35 base_msg_size_ = base_message_.getSize();
36 buffer_.resize(base_msg_size_);
3437 }
3538
3639
4750 size_t len = 0;
4851 do
4952 {
50 len += socket_->read_some(boost::asio::buffer((char*)_to + len, toRead));
53 len += socket_.read_some(boost::asio::buffer((char*)_to + len, toRead));
5154 // cout << "len: " << len << ", error: " << error << endl;
5255 toRead = _bytes - len;
5356 } while (toRead > 0);
5457 }
5558
5659
57 std::string ClientConnection::getMacAddress() const
58 {
59 if (socket_ == nullptr)
60 throw SnapException("socket not connected");
61
62 std::string mac = ::getMacAddress(socket_->native_handle());
60 std::string ClientConnection::getMacAddress()
61 {
62 std::string mac = ::getMacAddress(socket_.native_handle());
6363 if (mac.empty())
6464 mac = "00:00:00:00:00:00";
65 LOG(INFO) << "My MAC: \"" << mac << "\", socket: " << socket_->native_handle() << "\n";
65 LOG(INFO) << "My MAC: \"" << mac << "\", socket: " << socket_.native_handle() << "\n";
6666 return mac;
6767 }
6868
7373 tcp::resolver::query query(host_, cpt::to_string(port_), boost::asio::ip::resolver_query_base::numeric_service);
7474 auto iterator = resolver.resolve(query);
7575 LOG(DEBUG) << "Connecting\n";
76 socket_.reset(new tcp::socket(io_context_));
7776 // struct timeval tv;
7877 // tv.tv_sec = 5;
7978 // tv.tv_usec = 0;
8079 // cout << "socket: " << socket->native_handle() << "\n";
8180 // setsockopt(socket->native_handle(), SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
8281 // setsockopt(socket->native_handle(), SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
83 socket_->connect(*iterator);
84 connected_ = true;
85 SLOG(NOTICE) << "Connected to " << socket_->remote_endpoint().address().to_string() << endl;
82 socket_.connect(*iterator);
83 SLOG(NOTICE) << "Connected to " << socket_.remote_endpoint().address().to_string() << endl;
8684 active_ = true;
8785 sumTimeout_ = chronos::msec(0);
8886 readerThread_ = new thread(&ClientConnection::reader, this);
9189
9290 void ClientConnection::stop()
9391 {
94 connected_ = false;
9592 active_ = false;
9693 try
9794 {
9895 boost::system::error_code ec;
99 if (socket_)
100 {
101 socket_->shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
102 if (ec)
103 LOG(ERROR) << "Error in socket shutdown: " << ec.message() << endl;
104 socket_->close(ec);
105 if (ec)
106 LOG(ERROR) << "Error in socket close: " << ec.message() << endl;
107 }
96 socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
97 if (ec)
98 LOG(ERROR) << "Error in socket shutdown: " << ec.message() << endl;
99 socket_.close(ec);
100 if (ec)
101 LOG(ERROR) << "Error in socket close: " << ec.message() << endl;
108102 if (readerThread_)
109103 {
110104 LOG(DEBUG) << "joining readerThread\n";
116110 {
117111 }
118112 readerThread_ = nullptr;
119 socket_.reset();
120113 LOG(DEBUG) << "readerThread terminated\n";
121114 }
122115
123116
124 bool ClientConnection::send(const msg::BaseMessage* message) const
117 bool ClientConnection::send(const msg::BaseMessage* message)
125118 {
126119 // std::unique_lock<std::mutex> mlock(mutex_);
127120 // LOG(DEBUG) << "send: " << message->type << ", size: " << message->getSize() << "\n";
128121 std::lock_guard<std::mutex> socketLock(socketMutex_);
129 if (!connected())
122 if (!socket_.is_open())
130123 return false;
131124 // LOG(DEBUG) << "send: " << message->type << ", size: " << message->getSize() << "\n";
132125 boost::asio::streambuf streambuf;
134127 tv t;
135128 message->sent = t;
136129 message->serialize(stream);
137 boost::asio::write(*socket_.get(), streambuf);
130 boost::asio::write(socket_, streambuf);
138131 return true;
139132 }
140133
141134
142 shared_ptr<msg::SerializedMessage> ClientConnection::sendRequest(const msg::BaseMessage* message, const chronos::msec& timeout)
143 {
144 shared_ptr<msg::SerializedMessage> response(nullptr);
135
136 unique_ptr<msg::BaseMessage> ClientConnection::sendRequest(const msg::BaseMessage* message, const chronos::msec& timeout)
137 {
138 unique_ptr<msg::BaseMessage> response(nullptr);
145139 if (++reqId_ >= 10000)
146140 reqId_ = 1;
147141 message->id = reqId_;
148142 // LOG(INFO) << "Req: " << message->id << "\n";
149 shared_ptr<PendingRequest> pendingRequest(new PendingRequest(reqId_));
150
151 std::unique_lock<std::mutex> lock(pendingRequestsMutex_);
152 pendingRequests_.insert(pendingRequest);
153 send(message);
154 if (pendingRequest->cv.wait_for(lock, std::chrono::milliseconds(timeout)) == std::cv_status::no_timeout)
155 {
156 response = pendingRequest->response;
143 shared_ptr<PendingRequest> pendingRequest = make_shared<PendingRequest>(reqId_);
144
145 { // scope for lock
146 std::unique_lock<std::mutex> lock(pendingRequestsMutex_);
147 pendingRequests_.insert(pendingRequest);
148 send(message);
149 }
150
151 if ((response = pendingRequest->waitForResponse(std::chrono::milliseconds(timeout))) != nullptr)
152 {
157153 sumTimeout_ = chronos::msec(0);
158154 // LOG(INFO) << "Resp: " << pendingRequest->id << "\n";
159155 }
164160 if (sumTimeout_ > chronos::sec(10))
165161 throw SnapException("sum timeout exceeded 10s");
166162 }
167 pendingRequests_.erase(pendingRequest);
163
164 { // scope for lock
165 std::unique_lock<std::mutex> lock(pendingRequestsMutex_);
166 pendingRequests_.erase(pendingRequest);
167 }
168168 return response;
169169 }
170170
171171
172172 void ClientConnection::getNextMessage()
173173 {
174 msg::BaseMessage baseMessage;
175 size_t baseMsgSize = baseMessage.getSize();
176 vector<char> buffer(baseMsgSize);
177 socketRead(&buffer[0], baseMsgSize);
178 baseMessage.deserialize(&buffer[0]);
174 socketRead(&buffer_[0], base_msg_size_);
175 base_message_.deserialize(buffer_.data());
179176 // LOG(DEBUG) << "getNextMessage: " << baseMessage.type << ", size: " << baseMessage.size << ", id: " << baseMessage.id << ", refers: " <<
180177 // baseMessage.refersTo << "\n";
181 if (baseMessage.size > buffer.size())
182 buffer.resize(baseMessage.size);
178 if (base_message_.size > buffer_.size())
179 buffer_.resize(base_message_.size);
183180 // {
184181 // std::lock_guard<std::mutex> socketLock(socketMutex_);
185 socketRead(&buffer[0], baseMessage.size);
182 socketRead(buffer_.data(), base_message_.size);
183 tv t;
184 base_message_.received = t;
186185 // }
187 tv t;
188 baseMessage.received = t;
189
190 {
186
187 { // scope for lock
191188 std::unique_lock<std::mutex> lock(pendingRequestsMutex_);
192 // LOG(DEBUG) << "got lock - getNextMessage: " << baseMessage.type << ", size: " << baseMessage.size << ", id: " << baseMessage.id << ",
193 // refers: " << baseMessage.refersTo << "\n";
189 for (auto req : pendingRequests_)
194190 {
195 for (auto req : pendingRequests_)
191 if (req->id() == base_message_.refersTo)
196192 {
197 if (req->id == baseMessage.refersTo)
198 {
199 req->response.reset(new msg::SerializedMessage());
200 req->response->message = baseMessage;
201 req->response->buffer = (char*)malloc(baseMessage.size);
202 memcpy(req->response->buffer, &buffer[0], baseMessage.size);
203 lock.unlock();
204 req->cv.notify_one();
205 return;
206 }
193 auto response = msg::factory::createMessage(base_message_, buffer_.data());
194 req->setValue(std::move(response));
195 return;
207196 }
208197 }
209198 }
210199
211200 if (messageReceiver_ != nullptr)
212 messageReceiver_->onMessageReceived(this, baseMessage, &buffer[0]);
201 messageReceiver_->onMessageReceived(this, base_message_, buffer_.data());
213202 }
214203
215204
231220 catch (...)
232221 {
233222 }
234 connected_ = false;
235223 active_ = false;
236224 }
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
3737
3838
3939 /// Used to synchronize server requests (wait for server response)
40 struct PendingRequest
40 class PendingRequest
4141 {
42 PendingRequest(uint16_t reqId) : id(reqId), response(nullptr){};
42 public:
43 PendingRequest(uint16_t reqId) : id_(reqId)
44 {
45 future_ = promise_.get_future();
46 };
4347
44 uint16_t id;
45 std::shared_ptr<msg::SerializedMessage> response;
46 std::condition_variable cv;
48 template <typename Rep, typename Period>
49 std::unique_ptr<msg::BaseMessage> waitForResponse(const std::chrono::duration<Rep, Period>& timeout)
50 {
51 try
52 {
53 if (future_.wait_for(timeout) == std::future_status::ready)
54 return future_.get();
55 }
56 catch (...)
57 {
58 }
59 return nullptr;
60 }
61
62 void setValue(std::unique_ptr<msg::BaseMessage> value)
63 {
64 promise_.set_value(std::move(value));
65 }
66
67 uint16_t id() const
68 {
69 return id_;
70 }
71
72 private:
73 uint16_t id_;
74
75 std::promise<std::unique_ptr<msg::BaseMessage>> promise_;
76 std::future<std::unique_ptr<msg::BaseMessage>> future_;
4777 };
4878
4979
75105 virtual ~ClientConnection();
76106 virtual void start();
77107 virtual void stop();
78 virtual bool send(const msg::BaseMessage* message) const;
108 virtual bool send(const msg::BaseMessage* message);
79109
80110 /// Send request to the server and wait for answer
81 virtual std::shared_ptr<msg::SerializedMessage> sendRequest(const msg::BaseMessage* message, const chronos::msec& timeout = chronos::msec(1000));
111 virtual std::unique_ptr<msg::BaseMessage> sendRequest(const msg::BaseMessage* message, const chronos::msec& timeout = chronos::msec(1000));
82112
83113 /// Send request to the server and wait for answer of type T
84114 template <typename T>
85 std::shared_ptr<T> sendReq(const msg::BaseMessage* message, const chronos::msec& timeout = chronos::msec(1000))
115 std::unique_ptr<T> sendReq(const msg::BaseMessage* message, const chronos::msec& timeout = chronos::msec(1000))
86116 {
87 std::shared_ptr<msg::SerializedMessage> reply = sendRequest(message, timeout);
88 if (!reply)
117 std::unique_ptr<msg::BaseMessage> response = sendRequest(message, timeout);
118 if (!response)
89119 return nullptr;
90 std::shared_ptr<T> msg(new T);
91 msg->deserialize(reply->message, reply->buffer);
92 return msg;
120
121 T* tmp = dynamic_cast<T*>(response.get());
122 std::unique_ptr<T> result;
123 if (tmp != nullptr)
124 {
125 response.release();
126 result.reset(tmp);
127 }
128 return result;
93129 }
94130
95 std::string getMacAddress() const;
131 std::string getMacAddress();
96132
97133 virtual bool active() const
98134 {
99135 return active_;
100 }
101
102 virtual bool connected() const
103 {
104 return (socket_ != nullptr);
105 // return (connected_ && socket);
106136 }
107137
108138 protected:
111141 void socketRead(void* to, size_t bytes);
112142 void getNextMessage();
113143
144 msg::BaseMessage base_message_;
145 std::vector<char> buffer_;
146 size_t base_msg_size_;
147
114148 boost::asio::io_context io_context_;
115149 mutable std::mutex socketMutex_;
116 std::shared_ptr<tcp::socket> socket_;
150 tcp::socket socket_;
117151 std::atomic<bool> active_;
118 std::atomic<bool> connected_;
119152 MessageReceiver* messageReceiver_;
120153 mutable std::mutex pendingRequestsMutex_;
121154 std::set<std::shared_ptr<PendingRequest>> pendingRequests_;
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
2626 #if defined(HAS_FLAC)
2727 #include "decoder/flac_decoder.hpp"
2828 #endif
29 #if defined(HAS_OPUS)
30 #include "decoder/opus_decoder.hpp"
31 #endif
2932 #include "common/aixlog.hpp"
3033 #include "common/snap_exception.hpp"
3134 #include "message/hello.hpp"
5861 {
5962 auto* pcmChunk = new msg::PcmChunk(sampleFormat_, 0);
6063 pcmChunk->deserialize(baseMessage, buffer);
61 // LOG(DEBUG) << "chunk: " << pcmChunk->payloadSize << ", sampleFormat: " << sampleFormat_.rate << "\n";
64 // LOG(DEBUG) << "chunk: " << pcmChunk->payloadSize << ", sampleFormat: " << sampleFormat_.rate << "\n";
6265 if (decoder_->decode(pcmChunk))
6366 {
6467 // TODO: do decoding in thread?
7881 }
7982 else if (baseMessage.type == message_type::kServerSettings)
8083 {
81 serverSettings_.reset(new msg::ServerSettings());
84 serverSettings_ = make_unique<msg::ServerSettings>();
8285 serverSettings_->deserialize(baseMessage, buffer);
8386 LOG(INFO) << "ServerSettings - buffer: " << serverSettings_->getBufferMs() << ", latency: " << serverSettings_->getLatency()
8487 << ", volume: " << serverSettings_->getVolume() << ", muted: " << serverSettings_->isMuted() << "\n";
9194 }
9295 else if (baseMessage.type == message_type::kCodecHeader)
9396 {
94 headerChunk_.reset(new msg::CodecHeader());
97 headerChunk_ = make_unique<msg::CodecHeader>();
9598 headerChunk_->deserialize(baseMessage, buffer);
9699
97100 LOG(INFO) << "Codec: " << headerChunk_->codec << "\n";
100103 player_.reset(nullptr);
101104
102105 if (headerChunk_->codec == "pcm")
103 decoder_.reset(new PcmDecoder());
106 decoder_ = make_unique<decoder::PcmDecoder>();
104107 #if defined(HAS_OGG) && (defined(HAS_TREMOR) || defined(HAS_VORBIS))
105108 else if (headerChunk_->codec == "ogg")
106 decoder_.reset(new OggDecoder());
109 decoder_ = make_unique<decoder::OggDecoder>();
107110 #endif
108111 #if defined(HAS_FLAC)
109112 else if (headerChunk_->codec == "flac")
110 decoder_.reset(new FlacDecoder());
113 decoder_ = make_unique<decoder::FlacDecoder>();
114 #endif
115 #if defined(HAS_OPUS)
116 else if (headerChunk_->codec == "opus")
117 decoder_ = make_unique<decoder::OpusDecoder>();
111118 #endif
112119 else
113120 throw SnapException("codec not supported: \"" + headerChunk_->codec + "\"");
119126 stream_->setBufferLen(serverSettings_->getBufferMs() - latency_);
120127
121128 #ifdef HAS_ALSA
122 player_.reset(new AlsaPlayer(pcmDevice_, stream_));
129 player_ = make_unique<AlsaPlayer>(pcmDevice_, stream_);
123130 #elif HAS_OPENSL
124 player_.reset(new OpenslPlayer(pcmDevice_, stream_));
131 player_ = make_unique<OpenslPlayer>(pcmDevice_, stream_);
125132 #elif HAS_COREAUDIO
126 player_.reset(new CoreAudioPlayer(pcmDevice_, stream_));
133 player_ = make_unique<CoreAudioPlayer>(pcmDevice_, stream_);
127134 #else
128135 throw SnapException("No audio player support");
129136 #endif
133140 }
134141 else if (baseMessage.type == message_type::kStreamTags)
135142 {
136 streamTags_.reset(new msg::StreamTags());
137 streamTags_->deserialize(baseMessage, buffer);
138
139143 if (meta_)
140 meta_->push(streamTags_->msg);
144 {
145 msg::StreamTags streamTags_;
146 streamTags_.deserialize(baseMessage, buffer);
147 meta_->push(streamTags_.msg);
148 }
141149 }
142150
143151 if (baseMessage.type != message_type::kTime)
166174 latency_ = latency;
167175 clientConnection_.reset(new ClientConnection(this, host, port));
168176 controllerThread_ = thread(&Controller::worker, this);
177 }
178
179
180 void Controller::run(const PcmDevice& pcmDevice, const std::string& host, size_t port, int latency)
181 {
182 pcmDevice_ = pcmDevice;
183 latency_ = latency;
184 clientConnection_.reset(new ClientConnection(this, host, port));
185 worker();
186 // controllerThread_ = thread(&Controller::worker, this);
169187 }
170188
171189
206224 throw SnapException(async_exception_->what());
207225 }
208226
209 shared_ptr<msg::Time> reply = clientConnection_->sendReq<msg::Time>(&timeReq, chronos::msec(2000));
227 auto reply = clientConnection_->sendReq<msg::Time>(&timeReq, chronos::msec(2000));
210228 if (reply)
211229 {
212230 TimeProvider::getInstance().setDiff(reply->latency, reply->received - reply->sent);
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
4949 public:
5050 Controller(const std::string& clientId, size_t instance, std::shared_ptr<MetadataAdapter> meta);
5151 void start(const PcmDevice& pcmDevice, const std::string& host, size_t port, int latency);
52 void run(const PcmDevice& pcmDevice, const std::string& host, size_t port, int latency);
5253 void stop();
5354
5455 /// Implementation of MessageReceiver.
7273 int latency_;
7374 std::unique_ptr<ClientConnection> clientConnection_;
7475 std::shared_ptr<Stream> stream_;
75 std::unique_ptr<Decoder> decoder_;
76 std::unique_ptr<decoder::Decoder> decoder_;
7677 std::unique_ptr<Player> player_;
7778 std::shared_ptr<MetadataAdapter> meta_;
78 std::shared_ptr<msg::ServerSettings> serverSettings_;
79 std::shared_ptr<msg::StreamTags> streamTags_;
80 std::shared_ptr<msg::CodecHeader> headerChunk_;
79 std::unique_ptr<msg::ServerSettings> serverSettings_;
80 std::unique_ptr<msg::CodecHeader> headerChunk_;
8181 std::mutex receiveMutex_;
8282
8383 shared_exception_ptr async_exception_;
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
2222 #include "message/pcm_chunk.hpp"
2323 #include <mutex>
2424
25 namespace decoder
26 {
2527
2628 class Decoder
2729 {
3638 std::mutex mutex_;
3739 };
3840
41 } // namespace decoder
3942
4043 #endif
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
2626
2727 using namespace std;
2828
29
30 static FLAC__StreamDecoderReadStatus read_callback(const FLAC__StreamDecoder* decoder, FLAC__byte buffer[], size_t* bytes, void* client_data);
31 static FLAC__StreamDecoderWriteStatus write_callback(const FLAC__StreamDecoder* decoder, const FLAC__Frame* frame, const FLAC__int32* const buffer[],
32 void* client_data);
33 static void metadata_callback(const FLAC__StreamDecoder* decoder, const FLAC__StreamMetadata* metadata, void* client_data);
34 static void error_callback(const FLAC__StreamDecoder* decoder, FLAC__StreamDecoderErrorStatus status, void* client_data);
35
36
37 static msg::CodecHeader* flacHeader = nullptr;
38 static msg::PcmChunk* flacChunk = nullptr;
39 static msg::PcmChunk* pcmChunk = nullptr;
40 static SampleFormat sampleFormat;
41 static FLAC__StreamDecoder* decoder = nullptr;
42
29 namespace decoder
30 {
31
32 namespace callback
33 {
34 FLAC__StreamDecoderReadStatus read_callback(const FLAC__StreamDecoder* decoder, FLAC__byte buffer[], size_t* bytes, void* client_data);
35 FLAC__StreamDecoderWriteStatus write_callback(const FLAC__StreamDecoder* decoder, const FLAC__Frame* frame, const FLAC__int32* const buffer[],
36 void* client_data);
37 void metadata_callback(const FLAC__StreamDecoder* decoder, const FLAC__StreamMetadata* metadata, void* client_data);
38 void error_callback(const FLAC__StreamDecoder* decoder, FLAC__StreamDecoderErrorStatus status, void* client_data);
39 } // namespace callback
40
41 namespace
42 {
43 msg::CodecHeader* flacHeader = nullptr;
44 msg::PcmChunk* flacChunk = nullptr;
45 msg::PcmChunk* pcmChunk = nullptr;
46 SampleFormat sampleFormat;
47 FLAC__StreamDecoder* decoder = nullptr;
48 } // namespace
4349
4450
4551 FlacDecoder::FlacDecoder() : Decoder(), lastError_(nullptr)
5157 FlacDecoder::~FlacDecoder()
5258 {
5359 std::lock_guard<std::mutex> lock(mutex_);
60 FLAC__stream_decoder_delete(decoder);
5461 delete flacChunk;
55 delete decoder;
5662 }
5763
5864
104110 throw SnapException("ERROR: allocating decoder");
105111
106112 // (void)FLAC__stream_decoder_set_md5_checking(decoder, true);
107 init_status =
108 FLAC__stream_decoder_init_stream(decoder, read_callback, nullptr, nullptr, nullptr, nullptr, write_callback, metadata_callback, error_callback, this);
113 init_status = FLAC__stream_decoder_init_stream(decoder, callback::read_callback, nullptr, nullptr, nullptr, nullptr, callback::write_callback,
114 callback::metadata_callback, callback::error_callback, this);
109115 if (init_status != FLAC__STREAM_DECODER_INIT_STATUS_OK)
110116 throw SnapException("ERROR: initializing decoder: " + string(FLAC__StreamDecoderInitStatusString[init_status]));
111117
117123 return sampleFormat;
118124 }
119125
120
126 namespace callback
127 {
121128 FLAC__StreamDecoderReadStatus read_callback(const FLAC__StreamDecoder* /*decoder*/, FLAC__byte buffer[], size_t* bytes, void* client_data)
122129 {
123130 if (flacHeader != nullptr)
145152 }
146153
147154
148 FLAC__StreamDecoderWriteStatus write_callback(const FLAC__StreamDecoder* decoder, const FLAC__Frame* frame, const FLAC__int32* const buffer[],
155 FLAC__StreamDecoderWriteStatus write_callback(const FLAC__StreamDecoder* /*decoder*/, const FLAC__Frame* frame, const FLAC__int32* const buffer[],
149156 void* client_data)
150157 {
151 (void)decoder;
152
153158 if (pcmChunk != nullptr)
154159 {
155160 size_t bytes = frame->header.blocksize * sampleFormat.frameSize;
194199 }
195200
196201
197 void metadata_callback(const FLAC__StreamDecoder* decoder, const FLAC__StreamMetadata* metadata, void* client_data)
198 {
199 (void)decoder;
202 void metadata_callback(const FLAC__StreamDecoder* /*decoder*/, const FLAC__StreamMetadata* metadata, void* client_data)
203 {
200204 /* print some stats */
201205 if (metadata->type == FLAC__METADATA_TYPE_STREAMINFO)
202206 {
206210 }
207211
208212
209 void error_callback(const FLAC__StreamDecoder* decoder, FLAC__StreamDecoderErrorStatus status, void* client_data)
210 {
211 (void)decoder, (void)client_data;
213 void error_callback(const FLAC__StreamDecoder* /*decoder*/, FLAC__StreamDecoderErrorStatus status, void* client_data)
214 {
212215 SLOG(ERROR) << "Got error callback: " << FLAC__StreamDecoderErrorStatusString[status] << "\n";
213216 static_cast<FlacDecoder*>(client_data)->lastError_ = std::unique_ptr<FLAC__StreamDecoderErrorStatus>(new FLAC__StreamDecoderErrorStatus(status));
214217 }
218 } // namespace callback
219
220 } // namespace decoder
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
2424 #include <atomic>
2525 #include <memory>
2626
27
27 namespace decoder
28 {
2829
2930 struct CacheInfo
3031 {
5758 std::unique_ptr<FLAC__StreamDecoderErrorStatus> lastError_;
5859 };
5960
61 } // namespace decoder
6062
6163 #endif
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
2727
2828 using namespace std;
2929
30 namespace decoder
31 {
3032
3133 OggDecoder::OggDecoder() : Decoder()
3234 {
237239
238240 return sampleFormat_;
239241 }
242
243 } // namespace decoder
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
2424 #include <vorbis/codec.h>
2525 #endif
2626 #include <ogg/ogg.h>
27
28 namespace decoder
29 {
2730
2831 class OggDecoder : public Decoder
2932 {
5861 SampleFormat sampleFormat_;
5962 };
6063
64 } // namespace decoder
6165
6266 #endif
0 /***
1 This file is part of snapcast
2 Copyright (C) 2015 Hannes Ellinger
3
4 This program is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, either version 3 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see <http://www.gnu.org/licenses/>.
16 ***/
17
18 #include "opus_decoder.hpp"
19 #include "common/aixlog.hpp"
20 #include "common/snap_exception.hpp"
21 #include "common/str_compat.hpp"
22
23 namespace decoder
24 {
25
26 #define ID_OPUS 0x4F505553
27
28 /// int: Number of samples per channel in the input signal.
29 /// This must be an Opus frame size for the encoder's sampling rate. For example, at 48 kHz the
30 /// permitted values are 120, 240, 480, 960, 1920, and 2880.
31 /// Passing in a duration of less than 10 ms (480 samples at 48 kHz) will prevent the encoder from using the LPC or hybrid modes.
32 static constexpr int const_max_frame_size = 2880;
33
34
35 OpusDecoder::OpusDecoder() : Decoder(), dec_(nullptr)
36 {
37 pcm_.resize(120);
38 }
39
40
41 OpusDecoder::~OpusDecoder()
42 {
43 if (dec_ != nullptr)
44 opus_decoder_destroy(dec_);
45 }
46
47
48 bool OpusDecoder::decode(msg::PcmChunk* chunk)
49 {
50 int frame_size = 0;
51
52 while ((frame_size = opus_decode(dec_, (unsigned char*)chunk->payload, chunk->payloadSize, pcm_.data(), pcm_.size() / sample_format_.channels, 0)) ==
53 OPUS_BUFFER_TOO_SMALL)
54 {
55 if (pcm_.size() < const_max_frame_size * sample_format_.channels)
56 {
57 pcm_.resize(pcm_.size() * 2);
58 LOG(INFO) << "OPUS encoding buffer too small, resizing to " << pcm_.size() / sample_format_.channels << " samples per channel\n";
59 }
60 else
61 break;
62 }
63
64 if (frame_size < 0)
65 {
66 LOG(ERROR) << "Failed to decode chunk: " << opus_strerror(frame_size) << ", IN size: " << chunk->payloadSize << ", OUT size: " << pcm_.size() << '\n';
67 return false;
68 }
69 else
70 {
71 LOG(DEBUG) << "Decoded chunk: size " << chunk->payloadSize << " bytes, decoded " << frame_size << " samples" << '\n';
72
73 // copy encoded data to chunk
74 chunk->payloadSize = frame_size * sample_format_.channels * sizeof(opus_int16);
75 chunk->payload = (char*)realloc(chunk->payload, chunk->payloadSize);
76 memcpy(chunk->payload, (char*)pcm_.data(), chunk->payloadSize);
77 return true;
78 }
79 }
80
81
82 SampleFormat OpusDecoder::setHeader(msg::CodecHeader* chunk)
83 {
84 // decode the opus pseudo header
85 if (chunk->payloadSize < 12)
86 throw SnapException("OPUS header too small");
87
88 // decode the "opus id" magic number, this is our constant part that must match
89 uint32_t id_opus;
90 memcpy(&id_opus, chunk->payload, sizeof(id_opus));
91 if (SWAP_32(id_opus) != ID_OPUS)
92 throw SnapException("Not an Opus pseudo header");
93
94 // decode the sampleformat
95 uint32_t rate;
96 memcpy(&rate, chunk->payload + 4, sizeof(id_opus));
97 uint16_t bits;
98 memcpy(&bits, chunk->payload + 8, sizeof(bits));
99 uint16_t channels;
100 memcpy(&channels, chunk->payload + 10, sizeof(channels));
101
102 sample_format_.setFormat(SWAP_32(rate), SWAP_16(bits), SWAP_16(channels));
103 LOG(DEBUG) << "Opus sampleformat: " << sample_format_.getFormat() << "\n";
104
105 // create the decoder
106 int error;
107 dec_ = opus_decoder_create(sample_format_.rate, sample_format_.channels, &error);
108 if (error != 0)
109 throw SnapException("Failed to initialize Opus decoder: " + std::string(opus_strerror(error)));
110
111 return sample_format_;
112 }
113
114 } // namespace decoder
0 /***
1 This file is part of snapcast
2 Copyright (C) 2015 Hannes Ellinger
3
4 This program is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, either version 3 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see <http://www.gnu.org/licenses/>.
16 ***/
17
18 #pragma once
19
20 #include "decoder/decoder.hpp"
21 #include <opus/opus.h>
22
23 namespace decoder
24 {
25
26 class OpusDecoder : public Decoder
27 {
28 public:
29 OpusDecoder();
30 ~OpusDecoder();
31 bool decode(msg::PcmChunk* chunk) override;
32 SampleFormat setHeader(msg::CodecHeader* chunk) override;
33
34 private:
35 ::OpusDecoder* dec_;
36 std::vector<opus_int16> pcm_;
37 SampleFormat sample_format_;
38 };
39
40 } // namespace decoder
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
2020 #include "common/endian.hpp"
2121 #include "common/snap_exception.hpp"
2222
23 namespace decoder
24 {
2325
2426 #define ID_RIFF 0x46464952
2527 #define ID_WAVE 0x45564157
117119
118120 return sampleFormat;
119121 }
122
123 } // namespace decoder
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
2020 #include "decoder.hpp"
2121
2222
23 namespace decoder
24 {
25
2326 class PcmDecoder : public Decoder
2427 {
2528 public:
2831 SampleFormat setHeader(msg::CodecHeader* chunk) override;
2932 };
3033
34 } // namespace decoder
3135
3236 #endif
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
0 <?xml version="1.0" encoding="UTF-8"?>
1 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2 <plist version="1.0">
3 <dict>
4 <key>Label</key>
5 <string>de.badaix.snapcast.snapclient</string>
6 <key>ProgramArguments</key>
7 <array>
8 <string>/usr/local/bin/snapclient</string>
9 <!-- <string>-d</string> -->
10 </array>
11 <key>RunAtLoad</key>
12 <true/>
13 <key>KeepAlive</key>
14 <true/>
15 </dict>
16 </plist>
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
4343
4444 void reset()
4545 {
46 msg_.reset(new json);
46 msg_ = json{};
4747 }
4848
4949 std::string serialize()
5050 {
51 return METADATA + ":" + msg_->dump();
51 return METADATA + ":" + msg_.dump();
5252 }
5353
54 void tag(std::string name, std::string value)
54 void tag(const std::string& name, const std::string& value)
5555 {
56 (*msg_)[name] = value;
56 msg_[name] = value;
5757 }
5858
59 std::string operator[](std::string key)
59 std::string operator[](const std::string& key)
6060 {
6161 try
6262 {
63 return (*msg_)[key];
63 return msg_[key];
6464 }
6565 catch (std::domain_error&)
6666 {
7474 return 0;
7575 }
7676
77 int push(json& jtag)
77 int push(const json& jtag)
7878 {
79 msg_.reset(new json(jtag));
79 msg_ = jtag;
8080 return push();
8181 }
8282
8383 protected:
84 std::shared_ptr<json> msg_;
84 json msg_;
8585 };
8686
8787 /*
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
224224 adjustVolume(buff_, frames_);
225225 if ((pcm = snd_pcm_writei(handle_, buff_, frames_)) == -EPIPE)
226226 {
227 LOG(ERROR) << "XRUN\n";
227 LOG(ERROR) << "XRUN: " << snd_strerror(pcm) << "\n";
228228 snd_pcm_prepare(handle_);
229229 }
230230 else if (pcm < 0)
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
7575 continue;
7676
7777 UInt32 maxlen = 1024;
78 char buf[maxlen];
78 char buf[1024];
7979 theAddress = {kAudioDevicePropertyDeviceName, kAudioDevicePropertyScopeOutput, 0};
8080 AudioObjectGetPropertyData(devids[i], &theAddress, 0, NULL, &maxlen, buf);
8181 LOG(DEBUG) << "device: " << i << ", name: " << buf << ", channels: " << channels << "\n";
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
2424 #include "opensl_player.hpp"
2525
2626 using namespace std;
27
28
29 static constexpr auto kPhaseInit = "Init";
30 static constexpr auto kPhaseStart = "Start";
31 static constexpr auto kPhaseStop = "Stop";
32
2733
2834 // http://stackoverflow.com/questions/35730050/android-with-nexus-6-how-to-avoid-decreased-opensl-audio-thread-priority-rela?rq=1
2935
164170
165171
166172
167 void OpenslPlayer::throwUnsuccess(const std::string& what, SLresult result)
173 void OpenslPlayer::throwUnsuccess(const std::string& phase, const std::string& what, SLresult result)
168174 {
169175 if (SL_RESULT_SUCCESS == result)
170176 return;
171177 stringstream ss;
172 ss << what << " failed: " << resultToString(result) << "(" << result << ")";
178 ss << phase << " failed, operation: " << what << ", result: " << resultToString(result) << "(" << result << ")";
173179 throw SnapException(ss.str());
174180 }
175181
191197 // create engine
192198 SLEngineOption engineOption[] = {{(SLuint32)SL_ENGINEOPTION_THREADSAFE, (SLuint32)SL_BOOLEAN_TRUE}};
193199 result = slCreateEngine(&engineObject, 1, engineOption, 0, NULL, NULL);
194 throwUnsuccess("slCreateEngine", result);
200 throwUnsuccess(kPhaseInit, "slCreateEngine", result);
195201 result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
196 throwUnsuccess("EngineObject::Realize", result);
202 throwUnsuccess(kPhaseInit, "EngineObject::Realize", result);
197203 result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
198 throwUnsuccess("EngineObject::GetInterface", result);
204 throwUnsuccess(kPhaseInit, "EngineObject::GetInterface", result);
199205 result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 0, 0, 0);
200 throwUnsuccess("EngineEngine::CreateOutputMix", result);
206 throwUnsuccess(kPhaseInit, "EngineEngine::CreateOutputMix", result);
201207 result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
202 throwUnsuccess("OutputMixObject::Realize", result);
208 throwUnsuccess(kPhaseInit, "OutputMixObject::Realize", result);
203209
204210 SLuint32 samplesPerSec = SL_SAMPLINGRATE_48;
205211 switch (format.rate)
284290 const SLInterfaceID ids[3] = {SL_IID_ANDROIDCONFIGURATION, SL_IID_PLAY, SL_IID_BUFFERQUEUE}; //, SL_IID_VOLUME};
285291 const SLboolean req[3] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE}; //, SL_BOOLEAN_TRUE};
286292 result = (*engineEngine)->CreateAudioPlayer(engineEngine, &bqPlayerObject, &audioSrc, &audioSnk, 3, ids, req);
287 throwUnsuccess("Engine::CreateAudioPlayer", result);
293 throwUnsuccess(kPhaseInit, "Engine::CreateAudioPlayer", result);
288294
289295 SLAndroidConfigurationItf playerConfig;
290296 result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_ANDROIDCONFIGURATION, &playerConfig);
291 throwUnsuccess("PlayerObject::GetInterface", result);
297 throwUnsuccess(kPhaseInit, "PlayerObject::GetInterface", result);
292298 SLint32 streamType = SL_ANDROID_STREAM_MEDIA;
293299 //// SLint32 streamType = SL_ANDROID_STREAM_VOICE;
294300 result = (*playerConfig)->SetConfiguration(playerConfig, SL_ANDROID_KEY_STREAM_TYPE, &streamType, sizeof(SLint32));
295 throwUnsuccess("PlayerConfig::SetConfiguration", result);
301 throwUnsuccess(kPhaseInit, "PlayerConfig::SetConfiguration", result);
296302
297303 result = (*bqPlayerObject)->Realize(bqPlayerObject, SL_BOOLEAN_FALSE);
298 throwUnsuccess("PlayerObject::Realize", result);
304 throwUnsuccess(kPhaseInit, "PlayerObject::Realize", result);
299305 result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_PLAY, &bqPlayerPlay);
300 throwUnsuccess("PlayerObject::GetInterface", result);
306 throwUnsuccess(kPhaseInit, "PlayerObject::GetInterface", result);
301307 result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_BUFFERQUEUE, &bqPlayerBufferQueue);
302 throwUnsuccess("PlayerObject::GetInterface", result);
308 throwUnsuccess(kPhaseInit, "PlayerObject::GetInterface", result);
303309 result = (*bqPlayerBufferQueue)->RegisterCallback(bqPlayerBufferQueue, bqPlayerCallback, this);
304 throwUnsuccess("PlayerBufferQueue::RegisterCallback", result);
310 throwUnsuccess(kPhaseInit, "PlayerBufferQueue::RegisterCallback", result);
305311 // result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_VOLUME, &bqPlayerVolume);
306312 // throwUnsuccess("PlayerObject::GetInterface", result);
307313 result = (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_PAUSED);
308 throwUnsuccess("PlayerPlay::SetPlayState", result);
314 throwUnsuccess(kPhaseInit, "PlayerPlay::SetPlayState", result);
309315
310316 // Render and enqueue a first buffer. (or should we just play the buffer empty?)
311317 curBuffer = 0;
316322
317323 memset(buffer[curBuffer], 0, buff_size);
318324 result = (*bqPlayerBufferQueue)->Enqueue(bqPlayerBufferQueue, buffer[curBuffer], sizeof(buffer[curBuffer]));
319 throwUnsuccess("PlayerBufferQueue::Enqueue", result);
325 throwUnsuccess(kPhaseInit, "PlayerBufferQueue::Enqueue", result);
320326 curBuffer ^= 1;
321327 }
322328
378384 void OpenslPlayer::start()
379385 {
380386 SLresult result = (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_PLAYING);
381 throwUnsuccess("PlayerPlay::SetPlayState", result);
387 throwUnsuccess(kPhaseStart, "PlayerPlay::SetPlayState", result);
382388 }
383389
384390
385391 void OpenslPlayer::stop()
386392 {
387393 SLresult result = (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_STOPPED);
388 throwUnsuccess("PlayerPlay::SetPlayState", result);
394 (*bqPlayerBufferQueue)->Clear(bqPlayerBufferQueue);
395 throwUnsuccess(kPhaseStop, "PlayerPlay::SetPlayState", result);
389396 }
390397
391398
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
4747 void uninitOpensl();
4848
4949 virtual void worker();
50 void throwUnsuccess(const std::string& what, SLresult result);
50 void throwUnsuccess(const std::string& phase, const std::string& what, SLresult result);
5151 std::string resultToString(SLresult result) const;
5252
5353 // engine interfaces
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
00 .\"groff -Tascii -man snapclient.1
1 .TH SNAPCLIENT 1 "July 2018"
1 .TH SNAPCLIENT 1 "January 2020"
22 .SH NAME
33 snapclient - Snapcast client
44 .SH SYNOPSIS
5555 \fI/etc/default/snapclient\fR
5656 the daemon default configuration file
5757 .SH "COPYRIGHT"
58 Copyright (C) 2014-2019 Johannes Pohl (snapcast@badaix.de).
58 Copyright (C) 2014-2020 Johannes Pohl (snapcast@badaix.de).
5959 License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
6060 This is free software: you are free to change and redistribute it.
6161 There is NO WARRANTY, to the extent permitted by law.
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
1515 along with this program. If not, see <http://www.gnu.org/licenses/>.
1616 ***/
1717
18 #include <chrono>
1819 #include <iostream>
1920 #include <sys/resource.h>
2021
3839 using namespace std;
3940 using namespace popl;
4041
41 volatile sig_atomic_t g_terminated = false;
42 using namespace std::chrono_literals;
4243
4344 PcmDevice getPcmDevice(const std::string& soundcard)
4445 {
5960 for (auto dev : pcmDevices)
6061 if (dev.name.find(soundcard) != string::npos)
6162 return dev;
62 #endif
63 #else
64 std::ignore = soundcard;
65 #endif
66
6367 PcmDevice pcmDevice;
6468 return pcmDevice;
6569 }
116120 if (versionSwitch->is_set())
117121 {
118122 cout << "snapclient v" << VERSION << "\n"
119 << "Copyright (C) 2014-2019 BadAix (snapcast@badaix.de).\n"
123 << "Copyright (C) 2014-2020 BadAix (snapcast@badaix.de).\n"
120124 << "License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.\n"
121125 << "This is free software: you are free to change and redistribute it.\n"
122126 << "There is NO WARRANTY, to the extent permitted by law.\n\n"
167171 {
168172 AixLog::Log::instance().add_logsink<AixLog::SinkCout>(AixLog::Severity::info, AixLog::Type::all, "%Y-%m-%d %H-%M-%S [#severity]");
169173 }
170
171
172 signal(SIGHUP, signal_handler);
173 signal(SIGTERM, signal_handler);
174 signal(SIGINT, signal_handler);
175174
176175 #ifdef HAS_DAEMON
177176 std::unique_ptr<Daemon> daemon;
214213 }
215214 #endif
216215
216 bool active = true;
217 std::shared_ptr<Controller> controller;
218 auto signal_handler = install_signal_handler({SIGHUP, SIGTERM, SIGINT},
219 [&active, &controller](int signal, const std::string& strsignal) {
220 SLOG(INFO) << "Received signal " << signal << ": " << strsignal << "\n";
221 active = false;
222 if (controller)
223 {
224 LOG(INFO) << "Stopping controller\n";
225 controller->stop();
226 }
227 });
217228 if (host.empty())
218229 {
219230 #if defined(HAS_AVAHI) || defined(HAS_BONJOUR)
220231 BrowseZeroConf browser;
221232 mDNSResult avahiResult;
222 while (!g_terminated)
233 while (active)
223234 {
235 signal_handler.wait_for(500ms);
236 if (!active)
237 break;
224238 try
225239 {
226240 if (browser.browse("_snapcast._tcp", avahiResult, 5000))
237251 {
238252 SLOG(ERROR) << "Exception: " << e.what() << std::endl;
239253 }
240 chronos::sleep(500);
241254 }
242255 #endif
243256 }
244257
245 // Setup metadata handling
246 std::shared_ptr<MetadataAdapter> meta;
247 meta.reset(new MetadataAdapter);
248 if (metaStderr)
249 meta.reset(new MetaStderrAdapter);
250
251 std::unique_ptr<Controller> controller(new Controller(hostIdValue->value(), instance, meta));
252 if (!g_terminated)
253 {
258 if (active)
259 {
260 // Setup metadata handling
261 std::shared_ptr<MetadataAdapter> meta;
262 meta.reset(new MetadataAdapter);
263 if (metaStderr)
264 meta.reset(new MetaStderrAdapter);
265
266 controller = make_shared<Controller>(hostIdValue->value(), instance, meta);
254267 LOG(INFO) << "Latency: " << latency << "\n";
255 controller->start(pcmDevice, host, port, latency);
256 while (!g_terminated)
257 chronos::sleep(100);
258 controller->stop();
268 controller->run(pcmDevice, host, port, latency);
269 // signal_handler.wait();
270 // controller->stop();
259271 }
260272 }
261273 catch (const std::exception& e)
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
177177 return tp;
178178 }
179179
180
180 /*
181 2020-01-12 20-25-26 [Info] Chunk: 7 7 11 15 179 120
182 2020-01-12 20-25-27 [Info] Chunk: 6 6 8 15 212 122
183 2020-01-12 20-25-28 [Info] Chunk: 6 6 7 12 245 123
184 2020-01-12 20-25-29 [Info] Chunk: 5 6 6 9 279 117
185 2020-01-12 20-25-30 [Info] Chunk: 4 5 6 8 312 117
186 2020-01-12 20-25-30 [Error] Controller::onException: read_some: End of file
187 2020-01-12 20-25-30 [Error] Exception in Controller::worker(): read_some: End of file
188 2020-01-12 20-25-31 [Error] Exception in Controller::worker(): connect: Connection refused
189 2020-01-12 20-25-31 [Error] Error in socket shutdown: Transport endpoint is not connected
190 2020-01-12 20-25-32 [Error] Exception in Controller::worker(): connect: Connection refused
191 2020-01-12 20-25-32 [Error] Error in socket shutdown: Transport endpoint is not connected
192 ^C2020-01-12 20-25-32 [Info] Received signal 2: Interrupt
193 2020-01-12 20-25-32 [Info] Stopping controller
194 2020-01-12 20-25-32 [Error] Error in socket shutdown: Bad file descriptor
195 2020-01-12 20-25-32 [Error] Exception: Invalid argument
196 2020-01-12 20-25-32 [Notice] daemon terminated.
197
198 =================================================================
199 ==22383==ERROR: LeakSanitizer: detected memory leaks
200
201 Direct leak of 5756 byte(s) in 1 object(s) allocated from:
202 #0 0x7f3d60635602 in malloc (/usr/lib/x86_64-linux-gnu/libasan.so.2+0x98602)
203 #1 0x448fc2 in Stream::getNextPlayerChunk(void*, std::chrono::duration<long, std::ratio<1l, 1000000l> > const&, unsigned long, long)
204 /home/johannes/Develop/snapcast/client/stream.cpp:163
205
206 SUMMARY: AddressSanitizer: 5756 byte(s) leaked in 1 allocation(s).
207 */
181208
182209 void Stream::updateBuffers(int age)
183210 {
311338 LOG(INFO) << "pBuffer->full() && (abs(median_) > 1): " << median_ << "\n";
312339 sleep_ = cs::usec(median_);
313340 }
314 /* else if (cs::usec(median_) > cs::usec(300))
315 {
316 setRealSampleRate(format_.rate - format_.rate / 1000);
317 }
318 else if (cs::usec(median_) < -cs::usec(300))
319 {
320 setRealSampleRate(format_.rate + format_.rate / 1000);
321 }
322 */ }
323 else if (shortBuffer_.full())
324 {
325 if (cs::usec(abs(shortMedian_)) > cs::msec(5))
326 {
327 LOG(INFO) << "pShortBuffer->full() && (abs(shortMedian_) > 5): " << shortMedian_ << "\n";
328 sleep_ = cs::usec(shortMedian_);
329 }
330 /* else
331 {
332 setRealSampleRate(format_.rate + -shortMedian_ / 100);
333 }
334 */ }
335 else if (miniBuffer_.full() && (cs::usec(abs(miniBuffer_.median())) > cs::msec(50)))
336 {
337 LOG(INFO) << "pMiniBuffer->full() && (abs(pMiniBuffer->mean()) > 50): " << miniBuffer_.median() << "\n";
338 sleep_ = cs::usec((cs::msec::rep)miniBuffer_.mean());
339 }
341 // else if (cs::usec(median_) > cs::usec(300))
342 // {
343 // setRealSampleRate(format_.rate - format_.rate / 1000);
344 // }
345 // else if (cs::usec(median_) < -cs::usec(300))
346 // {
347 // setRealSampleRate(format_.rate + format_.rate / 1000);
348 // }
349 }
350 else if (shortBuffer_.full())
351 {
352 if (cs::usec(abs(shortMedian_)) > cs::msec(5))
353 {
354 LOG(INFO) << "pShortBuffer->full() && (abs(shortMedian_) > 5): " << shortMedian_ << "\n";
355 sleep_ = cs::usec(shortMedian_);
356 }
357 // else
358 // {
359 // setRealSampleRate(format_.rate + -shortMedian_ / 100);
360 // }
361 }
362 else if (miniBuffer_.full() && (cs::usec(abs(miniBuffer_.median())) > cs::msec(50)))
363 {
364 LOG(INFO) << "pMiniBuffer->full() && (abs(pMiniBuffer->mean()) > 50): " << miniBuffer_.median() << "\n";
365 sleep_ = cs::usec((cs::msec::rep)miniBuffer_.mean());
366 }
340367 }
341368
342369 if (sleep_.count() != 0)
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
0 # ==============================================================================
1 # LLVM Release License
2 # ==============================================================================
3 # University of Illinois/NCSA
4 # Open Source License
5 #
6 # Copyright (c) 2003-2018 University of Illinois at Urbana-Champaign.
7 # All rights reserved.
8 #
9 # Developed by:
10 #
11 # LLVM Team
12 #
13 # University of Illinois at Urbana-Champaign
14 #
15 # http://llvm.org
16 #
17 # Permission is hereby granted, free of charge, to any person obtaining a copy of
18 # this software and associated documentation files (the "Software"), to deal with
19 # the Software without restriction, including without limitation the rights to
20 # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
21 # of the Software, and to permit persons to whom the Software is furnished to do
22 # so, subject to the following conditions:
23 #
24 # * Redistributions of source code must retain the above copyright notice,
25 # this list of conditions and the following disclaimers.
26 #
27 # * Redistributions in binary form must reproduce the above copyright notice,
28 # this list of conditions and the following disclaimers in the
29 # documentation and/or other materials provided with the distribution.
30 #
31 # * Neither the names of the LLVM Team, University of Illinois at
32 # Urbana-Champaign, nor the names of its contributors may be used to
33 # endorse or promote products derived from this Software without specific
34 # prior written permission.
35 #
36 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
37 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
38 # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
39 # CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
40 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
41 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE
42 # SOFTWARE.
43
44 INCLUDE(CheckCXXSourceCompiles)
45 INCLUDE(CheckLibraryExists)
46
47 # Sometimes linking against libatomic is required for atomic ops, if
48 # the platform doesn't support lock-free atomics.
49
50
51 function(check_working_cxx_atomics varname)
52 set(OLD_CMAKE_REQUIRED_FLAGS ${CMAKE_REQUIRED_FLAGS})
53 set(CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS} -std=c++11")
54 CHECK_CXX_SOURCE_COMPILES("
55 #include <atomic>
56 std::atomic<long long> x;
57 int main() {
58 return std::atomic_is_lock_free(&x);
59 }
60 " ${varname})
61 set(CMAKE_REQUIRED_FLAGS ${OLD_CMAKE_REQUIRED_FLAGS})
62 endfunction(check_working_cxx_atomics)
63
64
65 function(check_working_cxx_atomics64 varname)
66 set(OLD_CMAKE_REQUIRED_FLAGS ${CMAKE_REQUIRED_FLAGS})
67 set(CMAKE_REQUIRED_FLAGS "-std=c++11 ${CMAKE_REQUIRED_FLAGS}")
68 CHECK_CXX_SOURCE_COMPILES("
69 #include <atomic>
70 #include <cstdint>
71 std::atomic<uint64_t> x (0);
72 int main() {
73 uint64_t i = x.load(std::memory_order_relaxed);
74 return std::atomic_is_lock_free(&x);
75 }
76 " ${varname})
77 set(CMAKE_REQUIRED_FLAGS ${OLD_CMAKE_REQUIRED_FLAGS})
78 endfunction(check_working_cxx_atomics64)
79
80
81 function(check_working_cxx_atomics_2args varname)
82 set(OLD_CMAKE_REQUIRED_FLAGS ${CMAKE_REQUIRED_FLAGS})
83 set(CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS} -latomic")
84 CHECK_CXX_SOURCE_COMPILES("
85 int main() {
86 __atomic_load(nullptr, 0);
87 return 0;
88 }
89 " ${varname})
90 set(CMAKE_REQUIRED_FLAGS ${OLD_CMAKE_REQUIRED_FLAGS})
91 endfunction(check_working_cxx_atomics_2args)
92
93
94 function(check_working_cxx_atomics64_2args varname)
95 set(OLD_CMAKE_REQUIRED_FLAGS ${CMAKE_REQUIRED_FLAGS})
96 set(CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS} -latomic")
97 CHECK_CXX_SOURCE_COMPILES("
98 int main() {
99 __atomic_load_8(nullptr, 0);
100 return 0;
101 }
102 " ${varname})
103 set(CMAKE_REQUIRED_FLAGS ${OLD_CMAKE_REQUIRED_FLAGS})
104 endfunction(check_working_cxx_atomics64_2args)
105
106
107 # First check if atomics work without the library.
108 check_working_cxx_atomics(HAVE_CXX_ATOMICS_WITHOUT_LIB)
109
110 # If not, check if the library exists, and atomics work with it.
111 if(NOT HAVE_CXX_ATOMICS_WITHOUT_LIB)
112 check_library_exists(atomic __atomic_fetch_add_4 "" HAVE_LIBATOMIC)
113 if( NOT HAVE_LIBATOMIC )
114 check_working_cxx_atomics_2args(HAVE_LIBATOMIC_2ARGS)
115 endif()
116 if( HAVE_LIBATOMIC OR HAVE_LIBATOMIC_2ARGS )
117 list(APPEND CMAKE_REQUIRED_LIBRARIES "atomic")
118 set(CMAKE_CXX_LINK_FLAGS "${CMAKE_CXX_LINK_FLAGS} -latomic")
119 check_working_cxx_atomics(HAVE_CXX_ATOMICS_WITH_LIB)
120 if (NOT HAVE_CXX_ATOMICS_WITH_LIB)
121 message(FATAL_ERROR "Host compiler must support std::atomic!")
122 endif()
123 else()
124 # Check for 64 bit atomic operations.
125 if(MSVC)
126 set(HAVE_CXX_ATOMICS64_WITHOUT_LIB True)
127 else()
128 check_working_cxx_atomics64(HAVE_CXX_ATOMICS64_WITHOUT_LIB)
129 endif()
130
131 # If not, check if the library exists, and atomics work with it.
132 if( NOT HAVE_CXX_ATOMICS64_WITHOUT_LIB )
133 check_library_exists(atomic __atomic_load_8 "" HAVE_CXX_LIBATOMICS64)
134 if( NOT HAVE_CXX_LIBATOMICS64 )
135 check_working_cxx_atomics64_2args(HAVE_CXX_LIBATOMICS64_2ARGS)
136 endif()
137 if( HAVE_CXX_LIBATOMICS64 OR HAVE_CXX_LIBATOMICS64_2ARGS )
138 list(APPEND CMAKE_REQUIRED_LIBRARIES "atomic")
139 set(CMAKE_CXX_LINK_FLAGS "${CMAKE_CXX_LINK_FLAGS} -latomic")
140 check_working_cxx_atomics64(HAVE_CXX_ATOMICS64_WITH_LIB)
141 if (NOT HAVE_CXX_ATOMICS64_WITH_LIB)
142 message(FATAL_ERROR "Host compiler must support std::atomic!")
143 endif()
144 else()
145 message(FATAL_ERROR "Host compiler appears to require libatomic, but cannot find it.")
146 endif()
147 endif()
148
149 endif()
150 endif()
151
152
00 # This file is part of snapcast
1 # Copyright (C) 2014-2019 Johannes Pohl
1 # Copyright (C) 2014-2020 Johannes Pohl
22
33 # This program is free software: you can redistribute it and/or modify
44 # it under the terms of the GNU General Public License as published by
22 / _\ ( )( \/ )( ) / \ / __)
33 / \ )( ) ( / (_/\( O )( (_ \
44 \_/\_/(__)(_/\_)\____/ \__/ \___/
5 version 1.2.2
5 version 1.2.5
66 https://github.com/badaix/aixlog
77
88 This file is part of aixlog
9 Copyright (C) 2017-2019 Johannes Pohl
9 Copyright (C) 2017-2020 Johannes Pohl
1010
1111 This software may be modified and distributed under the terms
1212 of the MIT license. See the LICENSE file for details.
7878 #endif
7979
8080 /// Internal helper macros (exposed, but shouldn't be used directly)
81 #define AIXLOG_INTERNAL__LOG_SEVERITY(SEVERITY_) std::clog << static_cast<AixLog::Severity>(SEVERITY_)
81 #define AIXLOG_INTERNAL__LOG_SEVERITY(SEVERITY_) std::clog << static_cast<AixLog::Severity>(SEVERITY_) << TAG()
8282 #define AIXLOG_INTERNAL__LOG_SEVERITY_TAG(SEVERITY_, TAG_) std::clog << static_cast<AixLog::Severity>(SEVERITY_) << TAG(TAG_)
8383
8484 #define AIXLOG_INTERNAL__ONE_COLOR(FG_) AixLog::Color::FG_
274274 std::string to_string(const std::string& format = "%Y-%m-%d %H-%M-%S.#ms") const
275275 {
276276 std::time_t now_c = std::chrono::system_clock::to_time_t(time_point);
277 struct ::tm* now_tm = std::localtime(&now_c);
277 struct ::tm now_tm = localtime_xp(now_c);
278278 char buffer[256];
279 strftime(buffer, sizeof buffer, format.c_str(), now_tm);
279 strftime(buffer, sizeof buffer, format.c_str(), &now_tm);
280280 std::string result(buffer);
281281 size_t pos = result.find("#ms");
282282 if (pos != std::string::npos)
283283 {
284284 int ms_part = std::chrono::time_point_cast<std::chrono::milliseconds>(time_point).time_since_epoch().count() % 1000;
285285 char ms_str[4];
286 sprintf(&ms_str[0], "%03d", ms_part);
287 result.replace(pos, 3, ms_str);
286 if (snprintf(ms_str, 4, "%03d", ms_part) >= 0)
287 result.replace(pos, 3, ms_str);
288288 }
289289 return result;
290290 }
293293
294294 private:
295295 bool is_null_;
296
297 inline std::tm localtime_xp(std::time_t timer) const
298 {
299 std::tm bt;
300 #if defined(__unix__)
301 localtime_r(&timer, &bt);
302 #elif defined(_MSC_VER)
303 localtime_s(&bt, &timer);
304 #else
305 static std::mutex mtx;
306 std::lock_guard<std::mutex> lock(mtx);
307 bt = *std::localtime(&timer);
308 #endif
309 return bt;
310 }
296311 };
297312
298313 /**
499514 case Severity::warning:
500515 return "Warn";
501516 case Severity::error:
502 return "Err";
517 return "Error";
503518 case Severity::fatal:
504519 return "Fatal";
505520 default:
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
2929 class CodecHeader : public BaseMessage
3030 {
3131 public:
32 CodecHeader(const std::string& codecName = "", size_t size = 0) : BaseMessage(message_type::kCodecHeader), payloadSize(size), codec(codecName)
32 CodecHeader(const std::string& codecName = "", size_t size = 0)
33 : BaseMessage(message_type::kCodecHeader), payloadSize(size), payload(nullptr), codec(codecName)
3334 {
34 payload = (char*)malloc(size);
35 if (size > 0)
36 payload = (char*)malloc(size * sizeof(char));
3537 }
3638
3739 ~CodecHeader() override
6163 writeVal(stream, payload, payloadSize);
6264 }
6365 };
64 }
66 } // namespace msg
6567
6668
6769 #endif
0 /***
1 This file is part of snapcast
2 Copyright (C) 2014-2020 Johannes Pohl
3
4 This program is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, either version 3 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see <http://www.gnu.org/licenses/>.
16 ***/
17
18 #ifndef MESSAGE_FACTORY_HPP
19 #define MESSAGE_FACTORY_HPP
20
21 #include "codec_header.hpp"
22 #include "hello.hpp"
23 #include "server_settings.hpp"
24 #include "stream_tags.hpp"
25 #include "time.hpp"
26 #include "wire_chunk.hpp"
27
28 #include "common/str_compat.hpp"
29 #include "common/utils.hpp"
30 #include "json_message.hpp"
31 #include <string>
32
33
34 namespace msg
35 {
36 namespace factory
37 {
38
39 template <typename T>
40 static std::unique_ptr<T> createMessage(const BaseMessage& base_message, char* buffer)
41 {
42 std::unique_ptr<T> result = std::make_unique<T>();
43 if (!result)
44 return nullptr;
45 result->deserialize(base_message, buffer);
46 return result;
47 }
48
49
50 static std::unique_ptr<BaseMessage> createMessage(const BaseMessage& base_message, char* buffer)
51 {
52 std::unique_ptr<BaseMessage> result;
53 switch (base_message.type)
54 {
55 case kCodecHeader:
56 return createMessage<CodecHeader>(base_message, buffer);
57 case kHello:
58 return createMessage<Hello>(base_message, buffer);
59 case kServerSettings:
60 return createMessage<ServerSettings>(base_message, buffer);
61 case kStreamTags:
62 return createMessage<StreamTags>(base_message, buffer);
63 case kTime:
64 return createMessage<Time>(base_message, buffer);
65 case kWireChunk:
66 return createMessage<WireChunk>(base_message, buffer);
67 default:
68 return nullptr;
69 }
70 }
71
72
73 } // namespace factory
74 } // namespace msg
75
76 #endif
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
290290 virtual void doserialize(std::ostream& /*stream*/) const {};
291291 };
292292
293
294 struct SerializedMessage
295 {
296 ~SerializedMessage()
297 {
298 free(buffer);
299 }
300
301 BaseMessage message;
302 char* buffer;
303 };
304 }
293 } // namespace msg
305294
306295 #endif
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
3939 {
4040 }
4141
42 PcmChunk(const PcmChunk& pcmChunk) : WireChunk(pcmChunk), format(pcmChunk.format), idx_(0)
43 {
44 }
45
4642 PcmChunk() : WireChunk(), idx_(0)
4743 {
4844 }
4945
5046 ~PcmChunk() override = default;
47
48 #if 0
49 template <class Rep, class Period>
50 int readFrames(void* outputBuffer, const std::chrono::duration<Rep, Period>& duration)
51 {
52 auto us = std::chrono::microseconds(duration).count();
53 auto frames = (us * 48000) / std::micro::den;
54 // return readFrames(outputBuffer, (us * 48000) / std::micro::den);
55 return frames;
56 }
57 #endif
5158
5259 int readFrames(void* outputBuffer, size_t frameCount)
5360 {
122129 private:
123130 uint32_t idx_;
124131 };
125 }
132 } // namespace msg
126133
127134 #endif
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
3838 class WireChunk : public BaseMessage
3939 {
4040 public:
41 WireChunk(size_t size = 0) : BaseMessage(message_type::kWireChunk), payloadSize(size)
41 WireChunk(size_t size = 0) : BaseMessage(message_type::kWireChunk), payloadSize(size), payload(nullptr)
4242 {
43 payload = (char*)malloc(size);
43 if (size > 0)
44 payload = (char*)malloc(size * sizeof(char));
4445 }
4546
4647 WireChunk(const WireChunk& wireChunk) : BaseMessage(message_type::kWireChunk), timestamp(wireChunk.timestamp), payloadSize(wireChunk.payloadSize)
8384 writeVal(stream, payload, payloadSize);
8485 }
8586 };
86 }
87 } // namespace msg
8788
8889
8990 #endif
937937 std::string line;
938938
939939 auto trim = [](std::string& s) {
940 s.erase(s.begin(), std::find_if(s.begin(), s.end(), std::not1(std::ptr_fun<int, int>(std::isspace))));
941 s.erase(std::find_if(s.rbegin(), s.rend(), std::not1(std::ptr_fun<int, int>(std::isspace))).base(), s.end());
940 s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int ch) { return !std::isspace(ch); }));
941 s.erase(std::find_if(s.rbegin(), s.rend(), [](int ch) { return !std::isspace(ch); }).base(), s.end());
942942 return s;
943943 };
944944
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
1515 along with this program. If not, see <http://www.gnu.org/licenses/>.
1616 ***/
1717
18 #ifndef SIGNAL_HANDLER_H
19 #define SIGNAL_HANDLER_H
18 #ifndef SIGNAL_HANDLER_HPP
19 #define SIGNAL_HANDLER_HPP
2020
21 #include <functional>
22 #include <future>
23 #include <set>
2124 #include <signal.h>
22 #include <syslog.h>
2325
24 extern volatile sig_atomic_t g_terminated;
26 using signal_callback = std::function<void(int signal, const std::string& name)>;
2527
26 void signal_handler(int sig)
28 static std::future<int> install_signal_handler(std::set<int> signals, const signal_callback& on_signal = nullptr)
2729 {
30 static std::promise<int> promise;
31 std::future<int> future = promise.get_future();
32 static signal_callback callback = on_signal;
2833
29 switch (sig)
34 for (auto signal : signals)
3035 {
31 case SIGHUP:
32 syslog(LOG_WARNING, "Received SIGHUP signal.");
33 break;
34 case SIGTERM:
35 syslog(LOG_WARNING, "Received SIGTERM signal.");
36 g_terminated = true;
37 break;
38 case SIGINT:
39 syslog(LOG_WARNING, "Received SIGINT signal.");
40 g_terminated = true;
41 break;
42 default:
43 syslog(LOG_WARNING, "Unhandled signal ");
44 break;
36 ::signal(signal, [](int sig) {
37 if (callback)
38 callback(sig, strsignal(sig));
39 try
40 {
41 promise.set_value(sig);
42 }
43 catch (const std::future_error&)
44 {
45 }
46 });
4547 }
48 return future;
4649 }
4750
4851 #endif
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
2525 // text_exception uses a dynamically-allocated internal c-string for what():
2626 class SnapException : public std::exception
2727 {
28 char* text_;
28 std::string text_;
2929
3030 public:
31 SnapException(const char* text)
31 SnapException(const char* text) : text_(text)
3232 {
33 text_ = new char[std::strlen(text) + 1];
34 std::strcpy(text_, text);
3533 }
3634
3735 SnapException(const std::string& text) : SnapException(text.c_str())
3836 {
3937 }
4038
41 SnapException(const SnapException& e) : SnapException(e.what())
42 {
43 }
44
45 ~SnapException() throw() override
46 {
47 delete[] text_;
48 }
39 ~SnapException() throw() override = default;
4940
5041 const char* what() const noexcept override
5142 {
52 return text_;
43 return text_.c_str();
5344 }
5445 };
5546
6657 {
6758 }
6859
69 AsyncSnapException(const AsyncSnapException& e) : SnapException(e.what())
70 {
71 }
72
73
7460 ~AsyncSnapException() throw() override = default;
7561 };
7662
6565 #endif
6666 }
6767
68 static int stoi(const std::string& str, int def)
69 {
70 try
71 {
72 return cpt::stoi(str);
73 }
74 catch (...)
75 {
76 return def;
77 }
78 }
79
6880 static double stod(const std::string& str)
6981 {
7082 #ifdef NO_CPP11_STRING
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
4949 gettimeofday(tv, nullptr);
5050 // timeofday<std::chrono::system_clock>(tv);
5151 }
52
53 template <class ToDuration>
54 inline ToDuration diff(const timeval& tv1, const timeval& tv2)
55 {
56 auto sec = tv1.tv_sec - tv2.tv_sec;
57 auto usec = tv1.tv_usec - tv2.tv_usec;
58 while (usec < 0)
59 {
60 sec -= 1;
61 usec += 1000000;
62 }
63 return std::chrono::duration_cast<ToDuration>(std::chrono::seconds(sec) + std::chrono::microseconds(usec));
64 }
65
5266
5367 inline static void addUs(timeval& tv, int us)
5468 {
114128 return;
115129 sleep(usec(microseconds));
116130 }
117 }
131 } // namespace chronos
118132
119133
120134 #endif
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
3232 // trim from start
3333 static inline std::string& ltrim(std::string& s)
3434 {
35 s.erase(s.begin(), std::find_if(s.begin(), s.end(), std::not1(std::ptr_fun<int, int>(std::isspace))));
35 s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int ch) { return !std::isspace(ch); }));
3636 return s;
3737 }
3838
3939 // trim from end
4040 static inline std::string& rtrim(std::string& s)
4141 {
42 s.erase(std::find_if(s.rbegin(), s.rend(), std::not1(std::ptr_fun<int, int>(std::isspace))).base(), s.end());
42 s.erase(std::find_if(s.rbegin(), s.rend(), [](int ch) { return !std::isspace(ch); }).base(), s.end());
4343 return s;
4444 }
4545
9494 }
9595
9696
97 static void split_left(const std::string& s, char delim, std::string& left, std::string& right)
98 {
99 auto pos = s.find(delim);
100 if (pos != std::string::npos)
101 {
102 left = s.substr(0, pos);
103 right = s.substr(pos + 1);
104 }
105 else
106 {
107 left = s;
108 right = "";
109 }
110 }
111
112
97113
98114 static std::vector<std::string>& split(const std::string& s, char delim, std::vector<std::string>& elems)
99115 {
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
123123 std::string result = getProp("net.hostname");
124124 if (!result.empty())
125125 return result;
126 result = getProp("ro.product.model");
127 if (!result.empty())
128 return result;
126129 #endif
127130 char hostname[1024];
128131 hostname[1023] = '\0';
11 import sys
22 import telnetlib
33 import json
4 import threading
5 import time
64
75 if len(sys.argv) < 3:
86 print("usage: control.py <SERVER HOST> [setVolume|setName]")
2119 if jResponse['id'] == requestId:
2220 print("recv: " + response)
2321 return jResponse;
24 return;
2522
2623 def setVolume(client, volume):
2724 global requestId
11 import sys
22 import telnetlib
33 import json
4 import threading
5 import time
64
75 telnet = telnetlib.Telnet(sys.argv[1], 1705)
86 requestId = 1
1715 if jResponse['id'] == requestId:
1816 # print("recv: " + response)
1917 return jResponse;
20 return;
2118
2219 def setVolume(client, volume):
2320 global requestId
0 snapcast (0.18.0-1) unstable; urgency=medium
1
2 * Features
3 -Add TCP stream reader
4 * Bugfixes
5 -Client: fix hostname reporting on Android
6 -Fix some small memory leaks
7 -Fix Librespot stream causing zombie processes (Issue #530)
8 -Process stream watchdog is configurable (Issue #517)
9 -Fix Makefile for macOS (Issues #510, #514)
10 * General
11 -Refactored stream readers
12 -Server can run on a single thread
13 -Configurable number of server worker threads
14
15 -- Johannes Pohl <snapcast@badaix.de> Wed, 22 Jan 2020 00:13:37 +0200
16
17 snapcast (0.17.1-1) unstable; urgency=medium
18
19 * Bugfixes
20 -Fix compile error if u_char is not defined (Issue #506)
21 -Fix error "exception unknown codec ogg" (Issue #504)
22 -Fix random crash during client disconnect
23
24 -- Johannes Pohl <snapcast@badaix.de> Sat, 23 Nov 2019 00:13:37 +0200
25
26 snapcast (0.17.0-1) unstable; urgency=medium
27
28 * Features
29 -Support for Opus low-latency codec (PR #4)
30 * Bugfixes
31 -CMake: fix check for libatomic (Issue #490, PR #494)
32 -WebUI: interface.html uses the server's IP for the websocket connection
33 -Fix warnings (Issue #484)
34 -Fix lock order inversions and data races identified by thread sanitizer
35 -Makefiles: fix install targets (PR #493)
36 -Makefiles: LDFLAGS are added from environment (PR #492)
37 -CMake: required Boost version is raised to 1.70 (Issue #488)
38 -Fix crash in websocket server
39 * General
40 -Stream server uses less threads (one in total, instead of 1+2n)
41 -Changing group volume is much more responsive for larger groups
42 -Unknown snapserver.conf options are logged as warning (Issue #487)
43 -debian scripts: change usernames back to snapclient and snapserver
44
45 -- Johannes Pohl <snapcast@badaix.de> Wed, 20 Nov 2019 00:13:37 +0200
46
047 snapcast (0.16.0-1) unstable; urgency=medium
148
249 * Features
1663 -Tidy up code base
1764 -Makefile doesn't statically link libgcc and libstdc++
1865
19 -- Johannes Pohl <snapcast@badaix.de> Tue, 15 Oct 2018 00:13:37 +0200
20
21 snapcast (0.15.0-1) unstable; urgency=medium
22
23 * New upstream release.
24 * Drop add_ldflags.patch, fixed upstream.
25 * Disable statically linking libgcc and libstdc++
26 - Add disable_static_linking.patch
27 * Enable all hardening compiler flags.
28
29 -- Felix Geyer <fgeyer@debian.org> Sat, 28 Jul 2018 11:48:26 +0200
30
31 snapcast (0.14.0-1) unstable; urgency=medium
32
33 * New upstream release.
34 * Install the documentation.
35 * Stop repackaging the main tarball and add the aixlog and popl libraries
36 as additional upstream tarballs.
37
38 -- Felix Geyer <fgeyer@debian.org> Sun, 01 Jul 2018 20:01:05 +0200
39
40 snapcast (0.13.0-1) unstable; urgency=medium
41
42 * Initial package. (Closes: #895241)
43
44 -- Felix Geyer <fgeyer@debian.org> Sun, 08 Apr 2018 22:05:46 +0200
66 -- Johannes Pohl <snapcast@badaix.de> Tue, 15 Oct 2019 00:13:37 +0200
67
68 snapcast (0.15.0) unstable; urgency=low
69
70 * Bugfixes
71 -Snapclient: make systemd dependeny to avahi-daemon optional
72 -cmake: fix build on FreeBSD
73 -Snapserver: fix occasional deadlock
74 * General
75 -additional linker flags "ADD_LDFLAGS" can be passed to makefile
76 -update man pages, fix lintian warning
77 -Support Android NDK r17
78
79 -- Johannes Pohl <snapcast@badaix.de> Sat, 07 Jul 2018 00:13:37 +0200
80
81 snapcast (0.14.0) unstable; urgency=low
82
83 * Features
84 -Snapserver supports IPv4v6 dual stack and IPv4 + IPv6
85 * Bugfixes
86 -cmake: fix check for big endian (Issue #367)
87 -cmake: fix linking against libatomic (PR #368)
88 -Snapclient compiles with Android NDK API level 16 (Issue #365)
89 -Fix compilation errors on FreeBSD (Issue #374, PR #375)
90 * General
91 -cmake: make dependency on avahi optional (PR #378)
92 -cmake: Options to build snapserver and snapclient
93 -cmake: works on FreeBSD
94 -Update external libs (JSON for modern C++, ASIO, AixLog)
95 -Restructured source tree
96 -Moved Android app into separate project "snapdroid"
97
98 -- Johannes Pohl <snapcast@badaix.de> Fri, 27 Apr 2018 00:13:37 +0200
99
100 snapcast (0.13.0) unstable; urgency=low
101
102 * Features
103 -Support "volume" parameter for Librespot (PR #273)
104 -Add support for metatags (PR #319)
105 * Bugfixes
106 -Fix overflow in timesync when the system time is many years off
107 -Zeroconf works with IPv6
108 -Snapclient unit file depends on avahi-daemon.service (PR #348)
109 * General
110 -Drop dependency to external "jsonrpc++"
111 -Move OpenWrt and Buildroot support into separate project "SnapOS"
112 -Add CMake support (not fully replacing Make yet) (PR #212)
113 -Remove MIPS support for Android (deprecated by Google)
114 -Use MAC address as default client id (override with "--hostID")
115
116 -- Johannes Pohl <snapcast@badaix.de> Tue, 04 Mar 2018 00:13:37 +0200
117
118 snapcast (0.12.0) unstable; urgency=low
119
120 * Features
121 -Support for IPv6 (PR #273, #279)
122 -Spotify plugin: onstart and onstop parameter (PR #225)
123 -Snapclient: configurable client id (Issue #249)
124 -Android: Snapclient support for ARM, MIPS and X86
125 -Snapserver: Play some silence before going idle (PR #284)
126 * Bugfixes
127 -Fix linker error (Issue #255, #274)
128 -Snapclient: more reliable unique client id (Issue #249, #246)
129 -Snapserver: fix config file permissions (Issue #251)
130 -Snapserver: fix crash on "bye" from control client (Issue #238)
131 -Snapserver: fix crash on port scan (Issue #267)
132 -Snapserver: fix crash when a malformed message is received (Issue #264)
133 * General
134 -Improved logging: Use "--debug" for debug logging
135 -Log to file: Use "--debug=<filename>"
136 -Improved exception handling and error logging (Issue #276)
137 -Android: update to NDK r16 and clang++
138 -hide spotify credentials in json control message (Issue #282)
139
140 -- Johannes Pohl <snapcast@badaix.de> Tue, 17 Oct 2017 00:13:37 +0200
141
142 snapcast (0.11.1) unstable; urgency=low
143
144 * Bugfixes
145 -Snapserver produces high CPU load on some systems (Issue #174)
146 -Snapserver compile error on FreeBSD
147 * General
148 -Updated Markdown files (PR #216, #218)
149
150 -- Johannes Pohl <snapcast@badaix.de> Tue, 21 Mar 2017 00:13:37 +0200
151
152 snapcast (0.11.0) unstable; urgency=low
153
154 * Features
155 -Don't send audio to muted clients (Issue #109, #150)
156 * Bugfixes
157 -Compilation error on recent GCC versions (Issues #146, #170)
158 -Crash when frequently connecting to the control port (Issue #200)
159 -Snapcast App crashes on Android 4.x (Issue #207)
160
161 -- Johannes Pohl <snapcast@badaix.de> Thu, 16 Mar 2017 00:13:37 +0200
162
163 snapcast (0.11.0~beta-1) unstable; urgency=low
164
165 * Features
166 -Snapclient multi-instance support (Issue #73)
167 -daemon can run as different user (default: snapclient, Issue #89)
168 -Spotify plugin does not require username and password (PR #181)
169 -Spotify plugin is compatible to librespot's pipe backend (PR #158)
170 -Clients are organized in Groups
171 -JSON RPC API supports batch requests
172 * Bugfixes
173 -Resync issue on macOS (Issue #156)
174 -Id in JSON RPC API can be String, Number or NULL (Issue #161)
175 -Use "which" instead of "whereis" to find binaries (PR #196, Issue #185)
176 -Compiles on lede (Issue #203)
177 * General
178 -JSON RPC API is versionized (current version is 2.0.0)
179 -Restructured Android App to support Groups
180
181 -- Johannes Pohl <snapcast@badaix.de> Sat, 04 Mar 2017 00:13:37 +0200
182
183 snapcast (0.10.0) unstable; urgency=low
184
185 * Features
186 -Added support "process" streams:
187 Snapserver starts a process and reads PCM data from stdout
188 -Specialized versions for Spotify "spotify" and AirPlay "airplay"
189 * Bugfixes
190 -Fixed crash during server shutdown
191 -Fixed Makefile for FreeBSD
192 -Fixed building of dpk (unsigned .changes file)
193 * General
194 -Speed up client and server shutdown
195
196 -- Johannes Pohl <snapcast@badaix.de> Wed, 16 Nov 2016 00:13:37 +0200
197
198 snapcast (0.9.0) unstable; urgency=low
199
200 * Features
201 -Added (experimental) support for macOS (make TARGET=MACOS)
202 * Bugfixes
203 -Android client: Fixed crash on Nougat (Issue #97)
204 -Fixed FreeBSD compile error for Snapserver (Issue #107)
205 -Snapserver randomly dropped JSON control messages
206 -Long command line options (like --sampleformat)
207 didn't work on some systems (Issue #103)
208 * General
209 -Updated Android NDK to revision 13
210
211 -- Johannes Pohl <snapcast@badaix.de> Sat, 22 Oct 2016 00:13:37 +0200
212
213 snapcast (0.8.0) unstable; urgency=low
214
215 * Features
216 -Added support for FreeBSD (Issue #67)
217 -Android client: Added Japanese and German translation
218 -Android client: Added support for ogg (Issue #83)
219 * Bugfixes
220 -OpenWRT: makefile automatically sets correct endian (Issue #91)
221
222 -- Johannes Pohl <snapcast@badaix.de> Sun, 24 Jul 2016 00:13:37 +0200
223
224 snapcast (0.7.0) unstable; urgency=low
225
226 * Features
227 -Support for HiRes audio (e.g. 96000:24:2) (Issue #13)
228 Bitdepth must be one of 8, 16, 24 (=24 bit padded to 32) or 32
229 -Auto start option for Android (Issue #49)
230 -creation mode for the fifo can be configured (Issue #52)
231 "-s pipe:///tmp/snapfifo?mode=[read|create]"
232 * Bugfixes
233 -Server was sometimes crashing during shutdown
234 -Exceptions were not properly logged (e.g. unsupported sample rates)
235 -Fixed default sound card detection on OpenWrt
236
237 -- Johannes Pohl <snapcast@badaix.de> Sat, 07 May 2016 00:13:37 +0200
238
239 snapcast (0.6.0) unstable; urgency=low
240
241 * Features
242 -Port to OpenWrt
243 * Bugfixes
244 -Android client: fixed crash if more than two streams are active
245 * General
246 -Support Tremor, an integer only Ogg-Vorbis implementation
247 -Endian-independent code
248 -Cleaned up build process
249
250 -- Johannes Pohl <snapcast@badaix.de> Sun, 10 Apr 2016 00:02:00 +0200
251
252 snapcast (0.5.0) unstable; urgency=low
253
254 * Features
255 -Android client: Fast switching of clients between streams
256 * Bugfixes
257 -Server: Fixed reading of server.json config file
258 * General
259 -Source code cleanups
260
261 -- Johannes Pohl <snapcast@badaix.de> Fri, 25 Mar 2016 00:02:00 +0200
262
263 snapcast (0.5.0~beta-2) unstable; urgency=low
264
265 * Features
266 -Remote control API (JSON)
267 Added version information
268 Stream playing state (unknown, idle, playing, inactive) (Issue #34)
269 -Android client: manually configure snapserver host name
270 -Android client compatibility improved: armeabi and armeabi-v7 binaries
271 -Android client: configurable latency
272 -Improved compatibility to Mopidy (GStreamer) (Issue #23)
273 * Bugfixes
274 -Android client: fixed "hide offline" on start
275 -Store config in /var/lib/snapcast/ when running as daemon (Issue #33)
276 * General
277 -README.md: Added resampling command to the Mopidy section (Issue #32)
278
279 -- Johannes Pohl <snapcast@badaix.de> Wed, 09 Mar 2016 00:02:00 +0200
280
281 snapcast (0.5.0~beta-1) unstable; urgency=low
282
283 * Features
284 -Remote control API (JSON)
285 Get server status, get streams, get active clients
286 assign volume, assign stream, rename client, ...
287 -Android port of the Snapclient with a remote control app (Issue #9)
288 -Multiple streams ("zones") can be configured (Issue #21)
289 Use "-s, --stream" to add a stream URI: path, name, codec, sample format
290 E.g. "pipe:///tmp/snapfifo?name=Radio&sampleformat=48000:16:2&codec=flac"
291 or "file:///home/user/some_pcm_file.wav?name=file"
292 -Added .default file for the service (/etc/default/snapserver).
293 Default program options should be configured here (e.g. streams)
294 * Bugfixes
295 -pipe reader recovers if the pipe has been reopened
296 * General
297 -SnapCast is renamed to Snapcast
298 SnapClient => Snapclient
299 SnapServer => Snapserver
300 -Snapcast protocol:
301 Less messaging: SampleFormat, Command, Ack, String, not yet final
302 -Removed dependency to boost
303
304 -- Johannes Pohl <snapcast@badaix.de> Tue, 09 Feb 2016 13:25:00 +0200
305
306 snapcast (0.4.1) unstable; urgency=low
307
308 * General
309 -Debian packages (.deb) are linked statically against libgcc and libstdc++
310 to improve compatibility
311
312 -- Johannes Pohl <snapcast@badaix.de> Sat, 12 Mar 2016 12:00:00 +0200
313
314 snapcast (0.4.0) unstable; urgency=low
315
316 * Features
317 -Debian packages (.deb) for amd64 and armhf
318 -Added man pages
319 * Bugfixes
320 -Snapserver and Snapclient are started as daemon on systemd systems
321 (e.g. ARCH, Debian Jessie)
322 * General
323 -Snapserver is started with normal process priority
324 (changed nice from -3 to 0)
325
326 -- Johannes Pohl <snapcast@badaix.de> Mon, 28 Dec 2015 12:00:00 +0200
327
328 snapcast (0.3.4) unstable; urgency=low
329
330 * Bugfixes
331 -Fix synchronization bug in FLAC decoder that could cause audible dropouts
332
333 -- Johannes Pohl <snapcast@badaix.de> Wed, 23 Dec 2015 12:00:00 +0200
334
335 snapcast (0.3.3) unstable; urgency=low
336
337 * Bugfixes
338 -Fix Segfault when ALSA device has no description
339
340 -- Johannes Pohl <snapcast@badaix.de> Sun, 15 Nov 2015 12:00:00 +0200
341
342 snapcast (0.3.2) unstable; urgency=low
343
344 * General
345 -Makefile uses CXX instead of CC to invoke the c++ compiler
346 * Bugfixes
347 -Time calculation for PCM chunk play-out was wrong on some gcc versions
348
349 -- Johannes Pohl <snapcast@badaix.de> Wed, 30 Sep 2015 12:00:00 +0200
350
351 snapcast (0.3.1) unstable; urgency=low
352
353 * General
354 -Improved stability over WiFi by avoiding simultaneous reads/writes on the
355 socket connection
356 * Bugfixes
357 -Fixed a bug in avahi browser
358
359 -- Johannes Pohl <snapcast@badaix.de> Wed, 26 Aug 2015 12:00:00 +0200
360
361 snapcast (0.3.0) unstable; urgency=low
362
363 * Features
364 -Configurable codec options. Run snapserver -c [flac|ogg|pcm]:? to get
365 supported options for the codec
366 -Configurable buffer size for the pipe reader
367 (default 20ms, was 50ms before)
368 -Process priority can be changed as argument to the daemon option -d<proi>
369 Default priority is -3
370 * Bugfixes
371 -Fixed deadlock in logger
372 -Fixed occasional timeouts for client to server requests
373 (e.g. time sync commands)
374 -Client didn't connect to a local server if the loopback device is the only
375 device with an address
376 * General
377 -Code clean up
378 -Refactored encoding for lower latency
379
380 -- Johannes Pohl <snapcast@badaix.de> Sun, 16 Aug 2015 19:25:51 +0100
381
382 snapcast (0.2.1) unstable; urgency=low
383
384 * Features
385 -Arch Linux compatibility
386
387 -- Johannes Pohl <snapcast@badaix.de> Fri, 24 Jul 2015 15:47:00 +0100
55 libasound2-dev,
66 libvorbis-dev,
77 libflac-dev,
8 libopus-dev,
89 libavahi-client-dev,
910 libasio-dev
1011 Standards-Version: 4.1.4
0 START_SNAPCLIENT=true
01 SNAPCLIENT_OPTS=""
1515 PATH=/sbin:/usr/sbin:/bin:/usr/bin
1616 DESC="Snapcast client"
1717 NAME=snapclient
18 USERNAME=snapclient
1819 DAEMON=/usr/bin/$NAME
1920 PIDFILE=/var/run/$NAME/pid
2021 SCRIPTNAME=/etc/init.d/$NAME
4647 PIDDIR=$(dirname "$PIDFILE")
4748 if [ ! -d "$PIDDIR" ]; then
4849 mkdir -m 0755 $PIDDIR
49 chown _snapclient:_snapclient $PIDDIR
50 chown $USERNAME:$USERNAME $PIDDIR
5051 fi
5152
5253 # Return
5354 # 0 if daemon has been started
5455 # 1 if daemon was already running
5556 # 2 if daemon could not be started
56 start-stop-daemon --start --quiet --pidfile "$PIDFILE" --chuid "_snapclient:_snapclient" --exec "$DAEMON" --test > /dev/null || return 1
57 start-stop-daemon --start --quiet --pidfile "$PIDFILE" --chuid "_snapclient:_snapclient" --exec "$DAEMON" -- $SNAPCLIENT_OPTS > /dev/null || return 2
57 start-stop-daemon --start --quiet --pidfile "$PIDFILE" --chuid "$USERNAME:$USERNAME" --exec "$DAEMON" --test > /dev/null || return 1
58 start-stop-daemon --start --quiet --pidfile "$PIDFILE" --chuid "$USERNAME:$USERNAME" --exec "$DAEMON" -- $SNAPCLIENT_OPTS > /dev/null || return 2
5859 # Add code here, if necessary, that waits for the process to be ready
5960 # to handle requests from services started subsequently which depend
6061 # on this one. As a last resort, sleep for some time.
11
22 set -e
33
4 USERNAME=_snapclient
4 USERNAME=snapclient
55 HOMEDIR=/var/lib/snapclient
66
77 if [ "$1" = configure ]; then
8 if ! getent passwd _snapclient >/dev/null; then
9 adduser --system --quiet --group --home /var/lib/snapclient --no-create-home --force-badname $USERNAME
8 if ! getent passwd $USERNAME >/dev/null; then
9 adduser --system --quiet --group --home $HOMEDIR --no-create-home --force-badname $USERNAME
1010 adduser $USERNAME audio
1111 fi
1212
33
44 #DEBHELPER#
55
6 USERNAME=_snapclient
6 USERNAME=snapclient
77 HOMEDIR=/var/lib/snapclient
88
99 if [ "$1" = "purge" ]; then
11 Description=Snapcast client
22 Documentation=man:snapclient(1)
33 Wants=avahi-daemon.service
4 After=network.target time-sync.target sound.target avahi-daemon.service
4 After=network-online.target time-sync.target sound.target avahi-daemon.service
55
66 [Service]
77 EnvironmentFile=-/etc/default/snapclient
88 ExecStart=/usr/bin/snapclient $SNAPCLIENT_OPTS
9 User=_snapclient
10 Group=_snapclient
9 User=snapclient
10 Group=snapclient
1111 # very noisy on stdout
1212 StandardOutput=null
1313 Restart=on-failure
0 START_SNAPSERVER=true
01 SNAPSERVER_OPTS=""
1515 PATH=/sbin:/usr/sbin:/bin:/usr/bin
1616 DESC="Snapcast server"
1717 NAME=snapserver
18 USERNAME=snapserver
1819 DAEMON=/usr/bin/$NAME
1920 PIDFILE=/var/run/$NAME/pid
2021 SCRIPTNAME=/etc/init.d/$NAME
4647 PIDDIR=$(dirname "$PIDFILE")
4748 if [ ! -d "$PIDDIR" ]; then
4849 mkdir -m 0755 $PIDDIR
49 chown _snapserver:_snapserver $PIDDIR
50 chown $USERNAME:$USERNAME $PIDDIR
5051 fi
5152
5253 # Return
5354 # 0 if daemon has been started
5455 # 1 if daemon was already running
5556 # 2 if daemon could not be started
56 start-stop-daemon --start --quiet --pidfile "$PIDFILE" --chuid "_snapclient:_snapclient" --exec "$DAEMON" --test > /dev/null || return 1
57 start-stop-daemon --start --quiet --pidfile "$PIDFILE" --chuid "_snapclient:_snapclient" --exec "$DAEMON" -- $SNAPSERVER_OPTS || return 2
57 start-stop-daemon --start --quiet --pidfile "$PIDFILE" --chuid "$USERNAME:$USERNAME" --exec "$DAEMON" --test > /dev/null || return 1
58 start-stop-daemon --start --quiet --pidfile "$PIDFILE" --chuid "$USERNAME:$USERNAME" --exec "$DAEMON" -- $SNAPSERVER_OPTS || return 2
5859 # Add code here, if necessary, that waits for the process to be ready
5960 # to handle requests from services started subsequently which depend
6061 # on this one. As a last resort, sleep for some time.
11
22 set -e
33
4 USERNAME=_snapserver
4 USERNAME=snapserver
55 HOMEDIR=/var/lib/snapserver
66
77 if [ "$1" = configure ]; then
8 adduser --system --quiet --group --home /var/lib/snapserver --no-create-home --force-badname $USERNAME
8 adduser --system --quiet --group --home $HOMEDIR --no-create-home --force-badname $USERNAME
99
1010 if [ ! -d $HOMEDIR ]; then
1111 mkdir -m 0750 $HOMEDIR
33
44 #DEBHELPER#
55
6 USERNAME=_snapserver
6 USERNAME=snapserver
77 HOMEDIR=/var/lib/snapserver
88
99 if [ "$1" = "purge" ]; then
66 [Service]
77 EnvironmentFile=-/etc/default/snapserver
88 ExecStart=/usr/bin/snapserver $SNAPSERVER_OPTS
9 User=_snapserver
10 Group=_snapserver
9 User=snapserver
10 Group=snapserver
1111 Restart=on-failure
1212
1313 [Install]
0 # Snapcast binary protocol
1
2 Each message sent with the Snapcast binary protocol is split up into two parts:
3 - A base message that provides general information like time sent/received, type of the message, message size, etc
4 - A typed message that carries the rest of the information
5
6 ## Client joining process
7
8 When a client joins a server, the following exchanges happen
9
10 1. Client opens a TCP socket to the server (default port is 1704)
11 1. Client sends a [Hello](#hello) message
12 1. Server sends a [Server Settings](#server-settings) message
13 1. Server sends a [Stream Tags](#stream-tags) message
14 1. Server sends a [Codec Header](#codec-header) message
15 1. Until the server sends this, the client shouldn't play any [Wire Chunk](#wire-chunk) messages
16 1. The server will now send [Wire Chunk](#wire-chunk) messages, which can be fed to the audio decoder.
17 1. When it comes time for the client to disconnect, the socket can just be closed.
18
19 ## Messages
20
21 | Typed Message ID | Name | Notes |
22 |------------------|--------------------------------------|---------------------------------------------------------------------------|
23 | 0 | [Base](#base) | The beginning of every message containing data about the typed message |
24 | 1 | [Codec Header](#codec-header) | The codec-specific data to put at the start of a stream to allow decoding |
25 | 2 | [Wire Chunk](#wire-chunk) | A part of an audio stream |
26 | 3 | [Server Settings](#server-settings) | Settings set from the server like volume, latency, etc |
27 | 4 | [Time](#time) | Used for synchronizing time with the server |
28 | 5 | [Hello](#hello) | Sent by the client when connecting with the server |
29 | 6 | [Stream Tags](#stream-tags) | Metadata about the stream for use by the client |
30
31 ### Base
32
33 | Field | Type | Description |
34 |-----------------------|--------|---------------------------------------------------------------------------------------------------|
35 | type | uint16 | Should be one of the typed message IDs |
36 | id | uint16 | Used in requests to identify the message (not always used) |
37 | refersTo | uint16 | Used in responses to identify which request message ID this is responding to |
38 | received.sec | int32 | The second value of the timestamp when this message was received. Filled in by the receiver. |
39 | received.usec | int32 | The microsecond value of the timestamp when this message was received. Filled in by the receiver. |
40 | sent.sec | int32 | The second value of the timestamp when this message was sent. Filled in by the sender. |
41 | sent.usec | int32 | The microsecond value of the timestamp when this message was sent. Filled in by the sender. |
42 | size | uint32 | Total number of bytes of the following typed message |
43
44 ### Codec Header
45
46 | Field | Type | Description |
47 |------------|---------|-------------------------------------------------------------|
48 | codec_size | unint32 | Length of the codec string (not including a null character) |
49 | codec | char[] | String describing the codec (not null terminated) |
50 | size | uint32 | Size of the following payload |
51 | payload | char[] | Buffer of data containing the codec header |
52
53 ### Wire Chunk
54
55 | Field | Type | Description |
56 |----------------|---------|---------------------------------------------------------------------------------------|
57 | timestamp.sec | int32 | The second value of the timestamp when this part of the stream was recorded |
58 | timestamp.usec | int32 | The microsecond value of the timestamp when this part of the stream was recorded |
59 | size | uint32 | Size of the following payload |
60 | payload | char[] | Buffer of data containing the codec header |
61
62 ### Server Settings
63
64 | Field | Type | Description |
65 |---------|--------|----------------------------------------------------------|
66 | size | uint32 | Size of the following JSON string |
67 | payload | char[] | JSON string containing the message (not null terminated) |
68
69 Sample JSON payload (whitespace added for readability):
70
71 ```json
72 {
73 "bufferMs": 1000,
74 "latency": 0,
75 "muted": false,
76 "volume": 100
77 }
78 ```
79
80 - `volume` can have a value between 0-100 inclusive
81
82 ### Time
83
84 | Field | Type | Description |
85 |----------------|---------|------------------------------------------------------------------------|
86 | latency.sec | int32 | The second value of the latency between the server and the client |
87 | latency.usec | int32 | The microsecond value of the latency between the server and the client |
88
89 ### Hello
90
91 | Field | Type | Description |
92 |---------|--------|----------------------------------------------------------|
93 | size | uint32 | Size of the following JSON string |
94 | payload | char[] | JSON string containing the message (not null terminated) |
95
96 Sample JSON payload (whitespace added for readability):
97
98 ```json
99 {
100 "Arch": "x86_64",
101 "ClientName": "Snapclient",
102 "HostName": "my_hostname",
103 "ID": "00:11:22:33:44:55",
104 "Instance": 1,
105 "MAC": "00:11:22:33:44:55",
106 "OS": "Arch Linux",
107 "SnapStreamProtocolVersion": 2,
108 "Version": "0.17.1"
109 }
110 ```
111
112 ### Stream Tags
113
114 | Field | Type | Description |
115 |---------|--------|----------------------------------------------------------------|
116 | size | uint32 | Size of the following JSON string |
117 | payload | char[] | JSON string containing the message (not null terminated) |
118
119 Sample JSON payload (whitespace added for readability):
120
121 ```json
122 {
123 "STREAM": "default"
124 }
125 ```
126
127 [According to the source](https://github.com/badaix/snapcast/blob/master/common/message/stream_tags.hpp#L55-L56), these tags can vary based on the stream.
2121 $ cd <snapcast dir>/externals
2222 $ git submodule update --init --recursive
2323
24 Snapcast depends on boost 1.70 or higher. Since it depends on header only boost libs, boost does not need to be installed, but the boost include path must be set properly: download and extract the latest boost version and add the include path, e.g. calling `make` with prepended `ADD_CFLAGS`: `ADD_CFLAGS="-I/path/to/boost_1_7x_0/" make`.
25 For `cmake` you must add the path to the `-DBOOST_ROOT` flag: `cmake -DBOOST_ROOT=/path/to/boost_1_7x_0`
2426
2527 ## Linux (Native)
2628 Install the build tools and required libs:
2729 For Debian derivates (e.g. Raspbian, Debian, Ubuntu, Mint):
2830
2931 $ sudo apt-get install build-essential
30 $ sudo apt-get install libasound2-dev libvorbisidec-dev libvorbis-dev libflac-dev alsa-utils libavahi-client-dev avahi-daemon
32 $ sudo apt-get install libasound2-dev libvorbisidec-dev libvorbis-dev libopus-dev libflac-dev alsa-utils libavahi-client-dev avahi-daemon
3133
3234 Compilation requires gcc 4.8 or higher, so it's highly recommended to use Debian (Raspbian) Jessie.
3335
3436 For Arch derivates:
3537
3638 $ sudo pacman -S base-devel
37 $ sudo pacman -S alsa-lib avahi libvorbis flac alsa-utils
38
39 For Fedora (and probably RHEL, CentOS & Scientific Linux, but untested):
39 $ sudo pacman -S alsa-lib avahi libvorbis opus-dev flac alsa-utils boost
40
41 For Fedora (and probably RHEL, CentOS, & Scientific Linux, but untested):
4042
4143 $ sudo dnf install @development-tools
42 $ sudo dnf install alsa-lib-devel avahi-devel libvorbis-devel flac-devel libstdc++-static
44 $ sudo dnf install alsa-lib-devel avahi-devel libvorbis-devel opus-devel flac-devel libstdc++-static
4345
4446 ### Build Snapclient and Snapserver
4547 `cd` into the Snapcast src-root directory:
8688 $ cd <snapcast dir>
8789 $ fakeroot make -f debian/rules binary
8890
91 If you don't have boost installed or in your standard include paths, you can call
92
93 $ fakeroot make -f debian/rules CPPFLAGS="-I/path/to/boost_1_7x_0" binary
94
8995 ## FreeBSD (Native)
9096 Install the build tools and required libs:
9197
92 $ sudo pkg install gmake gcc bash avahi libogg libvorbis flac
98 $ sudo pkg install gmake gcc bash avahi libogg libvorbis libopus flac
9399
94100 ### Build Snapserver
95101 `cd` into the Snapserver src-root directory:
138144 fi
139145 echo 'media-sound/snapcast client server flac
140146
141 If for example you only wish to build the server and *not* the client then preceed the server `USE` flag with `-` i.e.
147 If for example you only wish to build the server and *not* the client then precede the server `USE` flag with `-` i.e.
142148
143149 echo 'media-sound/snapcast client -server
144150
167173 3. Install the required libs
168174
169175 ```
170 $ brew install flac libvorbis
176 $ brew install flac libvorbis boost opus
171177 ```
172178
173179 ### Build Snapclient
206212 ```
207213 $ cd /SOME/LOCAL/PATH/android-ndk-r17/build/tools
208214 $ ./make_standalone_toolchain.py --arch arm --api 16 --stl libc++ --install-dir <android-ndk dir>-arm
215 $ ./make_standalone_toolchain.py --arch arm64 --api 21 --stl libc++ --install-dir <android-ndk dir>-arm64
209216 $ ./make_standalone_toolchain.py --arch x86 --api 16 --stl libc++ --install-dir <android-ndk dir>-x86
210217 ```
211218
212219 ### Build Snapclient
213 Cross compile and install FLAC, ogg, and tremor (only needed once):
220 Cross compile and install FLAC, opus, ogg, and tremor (only needed once):
214221
215222 $ cd <snapcast dir>/externals
216223 $ make NDK_DIR=<android-ndk dir>-arm ARCH=arm
224 $ make NDK_DIR=<android-ndk dir>-arm64 ARCH=aarch64
217225 $ make NDK_DIR=<android-ndk dir>-x86 ARCH=x86
218226
219227 Compile the Snapclient:
220228
221229 $ cd <snapcast dir>/client
222 $ ./build_android_all.sh <android-ndk dir> <snapdroid assets dir>
223
224 The binaries for `armeabi` and `x86` will be copied into the Android's assets directory (`<snapdroid assets dir>/bin/`) and so will be bundled with the Snapcast App.
230 $ ./build_android_all.sh <android-ndk dir> <snapdroid jniLibs dir>
231
232 The binaries for `armeabi`, `arm64-v8a` and `x86` will be copied into the Android's jniLibs directory (`<snapdroid jniLibs dir>/`) and so will be bundled with the Snapcast App.
225233
226234
227235 ## OpenWrt/LEDE (Cross compile)
281289 $ BUILDROOT_VERSION=2016.11.2
282290 $ git clone --branch $BUILDROOT_VERSION --depth=1 git://git.buildroot.net/buildroot
283291
284 The `<snapcast dir>/buildroot` is currently setup as an external Buildroot folder following the [recommended structure](https://buildroot.org/downloads/manual/manual.html#customize-dir-structure). As of [Buildroot 2016.11](https://git.buildroot.net/buildroot/tag/?h=2016.11) you may specify multiple BR2_EXTERNAL trees. If you are using a version of Buildroot prior to this, then you will need to manually merge `<snapcast dir>/buildroot` with your existing Buildroot external tree.
292 The `<snapcast dir>/buildroot` is currently set up as an external Buildroot folder following the [recommended structure](https://buildroot.org/downloads/manual/manual.html#customize-dir-structure). As of [Buildroot 2016.11](https://git.buildroot.net/buildroot/tag/?h=2016.11) you may specify multiple BR2_EXTERNAL trees. If you are using a version of Buildroot prior to this, then you will need to manually merge `<snapcast dir>/buildroot` with your existing Buildroot external tree.
285293
286294 Now configure buildroot with the [required packages](/buildroot/configs/snapcast_defconfig) (you can also manually add them to your project's existing defconfig):
287295
4545
4646 #### Stream
4747 ```json
48 {"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"buffer_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}}
48 {"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}}
4949 ```
5050
5151 #### Server
5252 ```json
53 {"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025751,"usec":654777},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream 2"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3 Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"buffer_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"buffer_ms":"20","codec":"flac","name":"stream 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}
53 {"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025751,"usec":654777},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream 2"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3 Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}
5454 ```
5555
5656
205205
206206 #### Response
207207 ```json
208 {"id":3,"jsonrpc":"2.0","result":{"server":{"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025901,"usec":864472},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}},{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":100}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc","lastSeen":{"sec":1488025905,"usec":45238},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream 2"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3 Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"buffer_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"buffer_ms":"20","codec":"flac","name":"stream 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}}}
209 ```
210
211 #### Notification
212 ```json
213 {"jsonrpc":"2.0","method":"Server.OnUpdate","params":{"server":{"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025901,"usec":864472},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}},{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":100}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc","lastSeen":{"sec":1488025905,"usec":45238},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream 2"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3 Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"buffer_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"buffer_ms":"20","codec":"flac","name":"stream 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}}}
208 {"id":3,"jsonrpc":"2.0","result":{"server":{"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025901,"usec":864472},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}},{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":100}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc","lastSeen":{"sec":1488025905,"usec":45238},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream 2"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3 Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}}}
209 ```
210
211 #### Notification
212 ```json
213 {"jsonrpc":"2.0","method":"Server.OnUpdate","params":{"server":{"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025901,"usec":864472},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}},{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":100}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc","lastSeen":{"sec":1488025905,"usec":45238},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream 2"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3 Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}}}
214214 ```
215215 ### Group.SetName
216216 #### Request
250250
251251 #### Response
252252 ```json
253 {"id":1,"jsonrpc":"2.0","result":{"server":{"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025696,"usec":578142},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}},{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":81}},"connected":true,"host":{"arch":"x86_64","ip":"192.168.0.54","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc","lastSeen":{"sec":1488025696,"usec":611255},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream 2"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3 Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"buffer_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"buffer_ms":"20","codec":"flac","name":"stream 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}}}
253 {"id":1,"jsonrpc":"2.0","result":{"server":{"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025696,"usec":578142},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}},{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":81}},"connected":true,"host":{"arch":"x86_64","ip":"192.168.0.54","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc","lastSeen":{"sec":1488025696,"usec":611255},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream 2"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3 Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}}}
254254 ```
255255
256256
262262
263263 #### Response
264264 ```json
265 {"id":2,"jsonrpc":"2.0","result":{"server":{"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025751,"usec":654777},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream 2"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3 Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"buffer_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"buffer_ms":"20","codec":"flac","name":"stream 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}}}
266 ```
267
268 #### Notification
269 ```json
270 {"jsonrpc":"2.0","method":"Server.OnUpdate","params":{"server":{"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025751,"usec":654777},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream 2"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3 Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"buffer_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"buffer_ms":"20","codec":"flac","name":"stream 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}}}
265 {"id":2,"jsonrpc":"2.0","result":{"server":{"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025751,"usec":654777},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream 2"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3 Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}}}
266 ```
267
268 #### Notification
269 ```json
270 {"jsonrpc":"2.0","method":"Server.OnUpdate","params":{"server":{"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025751,"usec":654777},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream 2"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3 Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}}}
271271 ```
272272
273273
337337
338338 ### Stream.OnUpdate
339339 ```json
340 {"jsonrpc":"2.0","method":"Stream.OnUpdate","params":{"id":"stream 1","stream":{"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"buffer_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}}}}
340 {"jsonrpc":"2.0","method":"Stream.OnUpdate","params":{"id":"stream 1","stream":{"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}}}}
341341 ```
342342
343343 ### Server.OnUpdate
344344 ```json
345 {"jsonrpc":"2.0","method":"Server.OnUpdate","params":{"server":{"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025751,"usec":654777},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream 2"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3 Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"buffer_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"buffer_ms":"20","codec":"flac","name":"stream 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}}}
346 ```
347
345 {"jsonrpc":"2.0","method":"Server.OnUpdate","params":{"server":{"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025751,"usec":654777},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream 2"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3 Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}}}
346 ```
347
00 Setup of audio players/server
11 -----------------------------
22 Snapcast can be used with a number of different audio players and servers, and so it can be integrated into your favorite audio-player solution and make it synced-multiroom capable.
3 The only requirement is that the player's audio can be redirected into the Snapserver's fifo `/tmp/snapfifo`. In the following configuration hints for [MPD](http://www.musicpd.org/) and [Mopidy](https://www.mopidy.com/) are given, which are base of other audio player solutions, like [Volumio](https://volumio.org/) or [RuneAudio](http://www.runeaudio.com/) (both MPD) or [Pi MusicBox](http://www.pimusicbox.com/) (Mopidy).
3 The only requirement is that the player's audio can be redirected into the Snapserver's fifo `/tmp/snapfifo`. In the following configuration hints for [MPD](http://www.musicpd.org/) and [Mopidy](https://www.mopidy.com/) are given, which are the base of other audio player solutions, like [Volumio](https://volumio.org/) or [RuneAudio](http://www.runeaudio.com/) (both MPD) or [Pi MusicBox](http://www.pimusicbox.com/) (Mopidy).
44
55 The goal is to build the following chain:
66
77 audio player software -> snapfifo -> snapserver -> network -> snapclient -> alsa
88
9 #### Streams
10 Snapserver can read audio from several input streams, which are configured in the `snapserver.conf` file (default location is `/etc/snapserver.conf`); the config file can be changed with the `-c` parameter.
11 Within the config file a list of input streams can be configured in the `[stream]` section:
12
13 ```
14 [stream]
15 ...
16 # stream URI of the PCM input stream, can be configured multiple times
17 # Format: TYPE://host/path?name=NAME[&codec=CODEC][&sampleformat=SAMPLEFORMAT]
18 stream = pipe:///tmp/snapfifo?name=default
19 ...
20 ```
21
922 #### About the notation
1023 In this document some expressions are in brackets:
11 * `<angle brackets>`: the whole expression must be replaced with you specific setting
24 * `<angle brackets>`: the whole expression must be replaced with your specific setting
1225 * `[square brackets]`: the whole expression is optional and can be left out
1326 * `[key=value]`: if you leave this option out, `value` will be the default for `key`
1427
1528 For example:
1629 ```
17 -s "spotify:///librespot?name=Spotify[&username=<my username>&password=<my password>][&devicename=Snapcast][&bitrate=320]"
30 stream = spotify:///librespot?name=Spotify[&username=<my username>&password=<my password>][&devicename=Snapcast][&bitrate=320]
1831 ```
19 * `username` and `password` are both optional in this case. You need to specify none of both of them
20 * `bitrate` is optional. If not configured, 320 will be used. Same for `devicename`
32 * `username` and `password` are both optional in this case. You need to specify neither or both of them.
33 * `bitrate` is optional. If not configured, `320` will be used.
34 * `devicename` is optional. If not configured, `Snapcast` will be used.
2135
22 A valid usage would be for instance:
36 For instance, a valid usage would be:
2337 ```
24 -s "spotify:///librespot?name=Spotify&bitrate=160"
38 stream = spotify:///librespot?name=Spotify&bitrate=160
2539 ```
2640
2741 ### MPD
28 To connect [MPD](http://www.musicpd.org/) to the Snapserver, edit `/etc/mpd.conf`, so that mpd will feed the audio into the snapserver's named pipe
42 To connect [MPD](http://www.musicpd.org/) to the Snapserver, edit `/etc/mpd.conf`, so that mpd will feed the audio into the snapserver's named pipe.
2943
3044 Disable alsa audio output by commenting out this section:
3145
4054 #}
4155
4256 Add a new audio output of the type "fifo", which will let mpd play audio into the named pipe `/tmp/snapfifo`.
43 Make sure that the "format" setting is the same as the format setting of the Snapserver (default is "48000:16:2", which should make resampling unnecessary in most cases)
57 Make sure that the "format" setting is the same as the format setting of the Snapserver (default is "48000:16:2", which should make resampling unnecessary in most cases).
4458
4559 audio_output {
4660 type "fifo"
6276 #output = autoaudiosink
6377 output = audioresample ! audioconvert ! audio/x-raw,rate=48000,channels=2,format=S16LE ! wavenc ! filesink location=/tmp/snapfifo
6478
65 With newer kernels one might also have to change this sysctl-setting as default settings has changed recently: `sudo sysctl fs.protected_fifos=0`
79 With newer kernels one might also have to change this sysctl-setting, as default settings have changed recently: `sudo sysctl fs.protected_fifos=0`
6680
6781 See https://unix.stackexchange.com/questions/503111/group-permissions-for-root-not-working-in-tmp for more details. You need to run this after each reboot or add it to /etc/sysctl.conf or /etc/sysctl.d/50-default.conf depending on distribution.
6882
114128 ### PulseAudio
115129 Redirect the PulseAudio stream into the snapfifo:
116130
117 audio player software -> PulseAudio -> PulsaAudio pipe sink -> snapfifo -> snapserver -> network -> snapclient -> Alsa
131 audio player software -> PulseAudio -> PulseAudio pipe sink -> snapfifo -> snapserver -> network -> snapclient -> Alsa
118132
119 PulseAudio will create the pipe file for itself and will fail if it already exsits, see the [Configuration section](https://github.com/badaix/snapcast#configuration) in the main readme file on how to change the pipe creation mode to read-only.
133 PulseAudio will create the pipe file for itself and will fail if it already exists; see the [Configuration section](https://github.com/badaix/snapcast#configuration) in the main README file on how to change the pipe creation mode to read-only.
120134
121135 Load the module `pipe-sink` like this:
122136
123137 pacmd load-module module-pipe-sink file=/tmp/snapfifo sink_name=Snapcast rate=48000
124138 pacmd update-sink-proplist Snapcast device.description=Snapcast
125139
126 It might be neccessary to set the pulse audio latency environment variable to 60 msec: `PULSE_LATENCY_MSEC=60`
140 It might be neccessary to set the PulseAudio latency environment variable to 60 msec: `PULSE_LATENCY_MSEC=60`
127141
128142
129143 ### AirPlay
130 Snapserver supports [shairport-sync](https://github.com/mikebrady/shairport-sync) with `stdout` backend.
144 Snapserver supports [shairport-sync](https://github.com/mikebrady/shairport-sync) with the `stdout` backend.
131145 1. Build shairport-sync with `stdout` backend: `./configure --with-stdout --with-avahi --with-ssl=openssl --with-metadata`
132146 2. Copy the `shairport-sync` binary somewhere to your `PATH`, e.g. `/usr/local/bin/`
133 3. Configure snapserver with `-s "airplay:///shairport-sync?name=Airplay[&devicename=Snapcast][&port=5000]"`
147 3. Configure snapserver with `stream = airplay:///shairport-sync?name=Airplay[&devicename=Snapcast][&port=5000]`
134148
135149
136150 ### Spotify
137 Snapserver supports [librespot](https://github.com/librespot-org/librespot) with `pipe` backend.
151 Snapserver supports [librespot](https://github.com/librespot-org/librespot) with the `pipe` backend.
138152 1. Build and copy the `librespot` binary somewhere to your `PATH`, e.g. `/usr/local/bin/`
139 2. Configure snapserver with `-s "spotify:///librespot?name=Spotify[&username=<my username>&password=<my password>][&devicename=Snapcast][&bitrate=320][&onstart=<start command>][&onstop=<stop command>][&volume=<volume in percent>][&cache=<cache dir>]"`
153 2. Configure snapserver with `stream = spotify:///librespot?name=Spotify[&username=<my username>&password=<my password>][&devicename=Snapcast][&bitrate=320][&onstart=<start command>][&onstop=<stop command>][&volume=<volume in percent>][&cache=<cache dir>]`
140154 * Valid bitrates are 96, 160, 320
141155 * `start command` and `stop command` are executed by Librespot at start/stop
142156 * For example: `onstart=/usr/bin/logger -t Snapcast Starting spotify...`
145159 ### Process
146160 Snapserver can start any process and read PCM data from the stdout of the process:
147161
148 Configure snapserver with `-s "process:///path/to/process?name=Process[&params=<--my list --of params>][&logStderr=false]"`
162 Configure snapserver with `stream = process:///path/to/process?name=Process[&params=<--my list --of params>][&log_stderr=false]`
149163
150164
151165 ### Line-in
155169 [cpipe](https://github.com/b-fitzpatrick/cpiped)
156170
157171 #### PulseAudio
158 `parec >/tmp/snapfifo` (defaults to 44.1kHz,16bit,stereo)
172 `parec >/tmp/snapfifo` (defaults to 44.1kHz, 16bit, stereo)
00 # This file is part of snapcast
1 # Copyright (C) 2014-2019 Johannes Pohl
1 # Copyright (C) 2014-2020 Johannes Pohl
22 #
33 # This program is free software: you can redistribute it and/or modify
44 # it under the terms of the GNU General Public License as published by
1313 # You should have received a copy of the GNU General Public License
1414 # along with this program. If not, see <http://www.gnu.org/licenses/>.
1515
16 .PHONY: all check-env flac ogg tremor
16 .PHONY: all check-env flac ogg opus tremor
1717
18 all: flac ogg tremor
18 all: flac ogg opus tremor
1919
2020 check-env:
2121 # if [ ! -d "flac" ]; then \
3131 $(error android NDK_DIR is not set)
3232 endif
3333 ifndef ARCH
34 $(error ARCH is not set ("arm" or "x86"))
34 $(error ARCH is not set ("arm" or "aarch64" or "x86"))
3535 endif
3636 ifeq ($(ARCH), x86)
3737 $(eval CPPFLAGS:=-DLITTLE_ENDIAN=1234 -DBIG_ENDIAN=4321 -DBYTE_ORDER=LITTLE_ENDIAN)
3939 else ifeq ($(ARCH), arm)
4040 $(eval CPPFLAGS:=-U_ARM_ASSEM_)
4141 $(eval PROGRAM_PREFIX:=$(NDK_DIR)/bin/arm-linux-androideabi-)
42 else ifeq ($(ARCH), aarch64)
43 $(eval CPPFLAGS:=-U_ARM_ASSEM_ -DLITTLE_ENDIAN=1234 -DBIG_ENDIAN=4321 -DBYTE_ORDER=LITTLE_ENDIAN)
44 $(eval PROGRAM_PREFIX:=$(NDK_DIR)/bin/aarch64-linux-android-)
4245 else
43 $(error ARCH must be "arm" or "x86")
46 $(error ARCH must be "arm" or "aarch64" or "x86")
4447 endif
4548 $(eval CC:=$(PROGRAM_PREFIX)clang)
4649 $(eval CXX:=$(PROGRAM_PREFIX)clang++)
5962
6063 ogg: check-env
6164 @cd ogg; \
65 export CC="$(CC)"; \
66 export CXX="$(CXX)"; \
67 export CPPFLAGS="$(CPPFLAGS)"; \
68 ./autogen.sh; \
69 ./configure --host=$(ARCH) --prefix=$(NDK_DIR); \
70 make; \
71 make install; \
72 make clean;
73
74 opus: check-env
75 @cd opus; \
6276 export CC="$(CC)"; \
6377 export CXX="$(CXX)"; \
6478 export CPPFLAGS="$(CPPFLAGS)"; \
+0
-105
notes.txt less more
0 *TODO:
1 -Server ping client?
2 -LastSeen: relative time [s] or [ms]?
3 -Android crash: Empty latency => app restart => empty client list
4 -Android clean data structures after changing the Server
5
6 *JSON RPC:
7 curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "Application.SetVolume", "params": {"volume":100}, "id": 1}' http://i3c.pla.lcl:8080/jsonrpc
8 https://en.wikipedia.org/wiki/JSON-RPC
9 https://github.com/pla1/utils/blob/master/kodi_remote.desktop
10 http://forum.fhem.de/index.php?topic=10075.130;wap2
11 http://kodi.wiki/view/JSON-RPC_API/v6#Application.SetVolume
12
13
14 *Logging:
15 -Too many of these:
16 Sep 20 06:50:58 wohnzimmer snapclient[2358]: Exception in Controller::worker(): connect: Network is unreachable
17 Sep 20 06:50:59 wohnzimmer snapclient[2358]: Exception in Controller::worker(): connect: Network is unreachable
18 Sep 20 06:51:00 wohnzimmer snapclient[2358]: Exception in Controller::worker(): connect: Network is unreachable
19 -Limit repeated syslog logging
20
21
22 *GIT Submodules:
23 http://stackoverflow.com/questions/2140985/how-to-set-up-a-git-project-to-use-an-external-repo-submodule
24 cd MyWebApp
25 git submodule add git://github.com/jquery/jquery.git externals/jquery
26
27 http://stackoverflow.com/questions/1777854/git-submodules-specify-a-branch-tag
28 cd submodule_directory
29 git checkout v1.0
30 cd ..
31 git add submodule_directory
32 git commit -m "moved submodule to v1.0"
33 git push
34
35 http://stackoverflow.com/questions/3796927/how-to-git-clone-including-submodules
36 git clone git://github.com/foo/bar.git
37 cd bar
38 git submodule update --init --recursive
39
40
41 *Icons:
42 http://s.evemarket.info/images/gnome/256x256/devices/speaker.png
43 https://upload.wikimedia.org/wikipedia/commons/3/3f/Mute_Icon.svg
44 https://upload.wikimedia.org/wikipedia/commons/2/21/Speaker_Icon.svg
45
46
47 *dpkg:
48 https://www.debian.org/doc/manuals/maint-guide/dother.de.html
49 http://hd-idle.sourceforge.net/
50 http://0pointer.de/public/systemd-man/systemd.exec.html#Environment=
51
52
53 *file locations:
54 http://stackoverflow.com/questions/1024114/location-of-ini-config-files-in-linux-unix
55
56
57 *OpenWrt:
58 https://wiki.openwrt.org/doc/howto/buildroot.exigence
59 https://wiki.openwrt.org/doc/devel/packages
60
61 johannes@T400 ~/Develop/openwrt.15.05/package/sxx/snapcast $ ln -s ~/Develop/snapcast/openWrt/Makefile.openwrt Makefile
62 johannes@T400 ~/Develop/openwrt.15.05/package/sxx/snapcast $ ln -s ~/Develop/snapcast/ src
63 johannes@T400 ~/Develop/openwrt.15.05/package/sxx/snapcast $ ls -l
64 lrwxrwxrwx 1 johannes johannes 48 Apr 3 14:50 Makefile -> /home/johannes/Develop/snapcast/openWrt/Makefile.openwrt
65 lrwxrwxrwx 1 johannes johannes 32 Mär 29 21:22 src -> /home/johannes/Develop/snapcast/
66
67 johannes@T400 ~/Develop/openwrt.15.05 $ make package/sxx/snapcast/clean V=s
68 johannes@T400 ~/Develop/openwrt.15.05 $ make package/sxx/snapcast/compile -j1 V=s
69
70
71 *Sample format
72 http://0pointer.de/lennart/projects/pulseaudio/doxygen/sample.html
73 https://www.musicpd.org/doc/user/config_audio_outputs.html#ao_format
74
75
76 *URI:
77 https://en.wikipedia.org/wiki/Uniform_Resource_Identifier
78
79
80 *Dependencies
81 libavahi-client3
82 libflac8
83 libogg0
84 libvorbis0a
85 libvorbisenc2
86
87
88 *Mac
89 brew install autoconf automake pkg-config libtool
90
91
92 *Listen
93 https://www.postgresql.org/docs/9.1/static/runtime-config-connection.html
94 http://www.cyberciti.biz/faq/unix-linux-mysqld-server-bind-to-more-than-one-ip-address/
95
96
97 *Sonos
98 https://www.youtube.com/watch?v=LGv6vL-YDIk
99 https://www.youtube.com/watch?v=W8A7rtVkdrE&t=2s
100
101
102 *MPD
103 https://git.devuan.org/dev1fanboy/mpd
104 https://git.devuan.org/dev1fanboy/mpd/blob/master/src/Main.cxx
1111 streamreader/stream_uri.cpp
1212 streamreader/stream_manager.cpp
1313 streamreader/pcm_stream.cpp
14 streamreader/tcp_stream.cpp
1415 streamreader/pipe_stream.cpp
16 streamreader/posix_stream.cpp
1517 streamreader/file_stream.cpp
1618 streamreader/airplay_stream.cpp
1719 streamreader/librespot_stream.cpp
5860 list(APPEND SERVER_INCLUDE ${FLAC_INCLUDE_DIRS})
5961 endif (FLAC_FOUND)
6062
63 if (OPUS_FOUND)
64 list(APPEND SERVER_SOURCES encoder/opus_encoder.cpp)
65 list(APPEND SERVER_LIBRARIES ${OPUS_LIBRARIES})
66 list(APPEND SERVER_INCLUDE ${OPUS_INCLUDE_DIRS})
67 endif (OPUS_FOUND)
68
6169 #list(APPEND SERVER_LIBRARIES Boost::boost)
6270
6371 include_directories(${SERVER_INCLUDE})
00 # This file is part of snapcast
1 # Copyright (C) 2014-2019 Johannes Pohl
1 # Copyright (C) 2014-2020 Johannes Pohl
22 #
33 # This program is free software: you can redistribute it and/or modify
44 # it under the terms of the GNU General Public License as published by
1313 # You should have received a copy of the GNU General Public License
1414 # along with this program. If not, see <http://www.gnu.org/licenses/>.
1515
16 VERSION = 0.16.0
16 VERSION = 0.18.0
1717 BIN = snapserver
1818
1919 ifeq ($(TARGET), FREEBSD)
2929 TARGET_DIR ?= /usr
3030 endif
3131
32 # Simplify building debuggable executables 'make DEBUG=-g STRIP=echo'
33 DEBUG=-g
34 SANITIZE=
35 #-fsanitize=thread
36
37 CXXFLAGS += $(ADD_CFLAGS) $(SANITIZE) -std=c++14 -Wall -Wextra -Wpedantic -Wno-unused-function $(DEBUG) -DHAS_FLAC -DHAS_OGG -DHAS_VORBIS -DHAS_VORBIS_ENC -DVERSION=\"$(VERSION)\" -I. -I.. -I../common
38 LDFLAGS = $(ADD_LDFLAGS) $(SANITIZE) -lvorbis -lvorbisenc -logg -lFLAC
39 OBJ = snapserver.o config.o control_server.o control_session_tcp.o control_session_http.o stream_server.o stream_session.o streamreader/stream_uri.o streamreader/base64.o streamreader/stream_manager.o streamreader/pcm_stream.o streamreader/pipe_stream.o streamreader/file_stream.o streamreader/process_stream.o streamreader/airplay_stream.o streamreader/librespot_stream.o streamreader/watchdog.o encoder/encoder_factory.o encoder/flac_encoder.o encoder/pcm_encoder.o encoder/ogg_encoder.o ../common/sample_format.o
32 DEBUG ?= 0
33 ifeq ($(DEBUG), 1)
34 CXXFLAGS += -g
35 else
36 CXXFLAGS += -O2
37 endif
38
39 ifneq ($(SANITIZE), )
40 CXXFLAGS += -fsanitize=$(SANITIZE) -g
41 LDFLAGS += -fsanitize=$(SANITIZE)
42 endif
43
44 CXXFLAGS += $(ADD_CFLAGS) -std=c++14 -Wall -Wextra -Wpedantic -Wno-unused-function -DBOOST_ERROR_CODE_HEADER_ONLY -DHAS_FLAC -DHAS_OGG -DHAS_VORBIS -DHAS_VORBIS_ENC -DHAS_OPUS -DVERSION=\"$(VERSION)\" -I. -I.. -I../common
45 LDFLAGS += $(ADD_LDFLAGS) -lvorbis -lvorbisenc -logg -lFLAC -lopus
46 OBJ = snapserver.o config.o control_server.o control_session_tcp.o control_session_http.o stream_server.o stream_session.o streamreader/stream_uri.o streamreader/base64.o streamreader/stream_manager.o streamreader/pcm_stream.o streamreader/posix_stream.o streamreader/pipe_stream.o streamreader/file_stream.o streamreader/tcp_stream.o streamreader/process_stream.o streamreader/airplay_stream.o streamreader/librespot_stream.o streamreader/watchdog.o encoder/encoder_factory.o encoder/flac_encoder.o encoder/opus_encoder.o encoder/pcm_encoder.o encoder/ogg_encoder.o ../common/sample_format.o
4047
4148 ifneq (,$(TARGET))
4249 CXXFLAGS += -D$(TARGET)
4956 ifeq ($(TARGET), ANDROID)
5057
5158 CXX = $(NDK_DIR)/bin/arm-linux-androideabi-g++
52 STRIP = $(NDK_DIR)/bin/arm-linux-androideabi-strip
5359 CXXFLAGS += -pthread -DNO_CPP11_STRING -fPIC -I$(NDK_DIR)/include
5460 LDFLAGS += -L$(NDK_DIR)/lib -pie -llog -latomic
5561
5662 else ifeq ($(TARGET), OPENWRT)
5763
58 STRIP = echo
5964 CXXFLAGS += -DNO_CPP11_STRING -DHAS_AVAHI -DHAS_DAEMON -pthread
6065 LDFLAGS += -lavahi-client -lavahi-common -latomic
6166 OBJ += ../common/daemon.o publishZeroConf/publish_avahi.o
6974 else ifeq ($(TARGET), FREEBSD)
7075
7176 CXX = g++
72 STRIP = echo
7377 CXXFLAGS += -DFREEBSD -DNO_CPP11_STRING -DHAS_AVAHI -DHAS_DAEMON -pthread
7478 LDFLAGS += -lrt -lavahi-client -lavahi-common -latomic
7579 OBJ += ../common/daemon.o publishZeroConf/publish_avahi.o
7781 else ifeq ($(TARGET), MACOS)
7882
7983 CXX = g++
80 STRIP = strip
8184 CXXFLAGS += -DFREEBSD -DHAS_BONJOUR -DHAS_DAEMON -Wno-deprecated -I/usr/local/include
8285 LDFLAGS += -L/usr/local/lib -framework CoreFoundation -framework IOKit
8386 OBJ += ../common/daemon.o publishZeroConf/publish_bonjour.o
8588 else
8689
8790 CXX = g++
88 STRIP = echo
8991 CXXFLAGS += -DHAS_AVAHI -DHAS_DAEMON -pthread
90 LDFLAGS += -lrt -lavahi-client -lavahi-common
92 LDFLAGS += -lrt -lavahi-client -lavahi-common -latomic
9193 OBJ += ../common/daemon.o publishZeroConf/publish_avahi.o
9294
9395 endif
102104
103105 $(BIN): $(OBJ)
104106 $(CXX) $(CXXFLAGS) -o $(BIN) $(OBJ) $(LDFLAGS)
105 $(STRIP) $(BIN)
106107
107108 %.o: %.cpp
108109 $(CXX) $(CXXFLAGS) -c $< -o $@
119120
120121 install:
121122 echo BSD
122 install -g wheel -o root -m 555 $(BIN) $(TARGET_DIR)/local/bin/$(BIN)
123 install -s -g wheel -o root -m 555 $(BIN) $(TARGET_DIR)/local/bin/$(BIN)
123124 install -g wheel -o root -m 555 $(BIN).1 $(TARGET_DIR)/local/man/man1/$(BIN).1
124 install -g wheel -o root -m 555 debian/$(BIN).bsd $(TARGET_DIR)/local/etc/rc.d/$(BIN)
125 install -g wheel -o root -m 555 ../debian/$(BIN).bsd $(TARGET_DIR)/local/etc/rc.d/$(BIN)
125126
126127 else ifeq ($(TARGET), MACOS)
127128
128129 install:
129130 echo macOS
130 install -g wheel -o root $(BIN) $(TARGET_DIR)/local/bin/$(BIN)
131 install -s -g wheel -o root $(BIN) $(TARGET_DIR)/local/bin/$(BIN)
131132 install -g wheel -o root $(BIN).1 $(TARGET_DIR)/local/share/man/man1/$(BIN).1
132 install -g wheel -o root debian/$(BIN).plist /Library/LaunchAgents/de.badaix.snapcast.$(BIN).plist
133 install -g wheel -o root etc/$(BIN).plist /Library/LaunchAgents/de.badaix.snapcast.$(BIN).plist
134 install -g wheel -o root etc/$(BIN).conf /etc/$(BIN).conf
133135 launchctl load /Library/LaunchAgents/de.badaix.snapcast.$(BIN).plist
134136
135137 else
152154 endif
153155
154156 installfiles:
155 install -D -g root -o root $(BIN) $(TARGET_DIR)/bin/$(BIN)
157 install -s -D -g root -o root $(BIN) $(TARGET_DIR)/bin/$(BIN)
156158 install -D -g root -o root $(BIN).1 $(TARGET_DIR)/share/man/man1/$(BIN).1
157159
158160 installsystemd:
159161 @echo using systemd; \
160 cp debian/$(BIN).service /lib/systemd/system/$(BIN).service; \
161 cp -n debian/$(BIN).default /etc/default/$(BIN); \
162 cp -n server/etc/$(BIN).conf /etc/default/$(BIN).conf; \
162 cp ../debian/$(BIN).service /lib/systemd/system/$(BIN).service; \
163 cp -n ../debian/$(BIN).default /etc/default/$(BIN); \
164 cp -n etc/$(BIN).conf /etc/$(BIN).conf; \
163165 systemctl daemon-reload; \
164166 systemctl enable $(BIN); \
165167 systemctl start $(BIN); \
166168
167169 installsysv:
168170 @echo using sysv; \
169 cp debian/$(BIN).init /etc/init.d/$(BIN); \
170 cp -n debian/$(BIN).default /etc/default/$(BIN); \
171 cp -n server/etc/$(BIN).conf /etc/default/$(BIN).conf; \
171 cp ../debian/$(BIN).init /etc/init.d/$(BIN); \
172 cp -n ../debian/$(BIN).default /etc/default/$(BIN); \
173 cp -n etc/$(BIN).conf /etc/$(BIN).conf; \
172174 update-rc.d $(BIN) defaults; \
173175 /etc/init.d/$(BIN) start; \
174176
175177 installbsd:
176178 @echo using bsd; \
177 cp debian/$(BIN).bsd /usr/local/etc/rc.d/$(BIN); \
179 cp ../debian/$(BIN).bsd /usr/local/etc/rc.d/$(BIN); \
178180
179181 adduser:
180182 @if ! getent passwd snapserver >/dev/null; then \
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
1515 along with this program. If not, see <http://www.gnu.org/licenses/>.
1616 ***/
1717
18 #include "config.h"
18 #include "config.hpp"
1919 #include "common/aixlog.hpp"
2020 #include "common/snap_exception.hpp"
2121 #include "common/str_compat.hpp"
120120 init();
121121 std::ofstream ofs(filename_.c_str(), std::ofstream::out | std::ofstream::trunc);
122122 json clients = {{"ConfigVersion", 2}, {"Groups", getGroups()}};
123 ofs << std::setw(4) << clients;
123 // ofs << std::setw(4) << clients;
124 ofs << clients;
124125 ofs.close();
125126 }
126127
+0
-404
server/config.h less more
0 /***
1 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
3
4 This program is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, either version 3 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see <http://www.gnu.org/licenses/>.
16 ***/
17
18 #ifndef CONFIG_H
19 #define CONFIG_H
20
21 #include <memory>
22 #include <string>
23 #include <sys/time.h>
24 #include <vector>
25
26 #include "common/json.hpp"
27 #include "common/utils.hpp"
28 #include "common/utils/string_utils.hpp"
29
30
31 namespace strutils = utils::string;
32 using json = nlohmann::json;
33
34 struct ClientInfo;
35 struct Group;
36
37 typedef std::shared_ptr<ClientInfo> ClientInfoPtr;
38 typedef std::shared_ptr<Group> GroupPtr;
39
40
41 template <typename T>
42 T jGet(const json& j, const std::string& what, const T& def)
43 {
44 try
45 {
46 if (!j.count(what))
47 return def;
48 return j[what].get<T>();
49 }
50 catch (...)
51 {
52 return def;
53 }
54 }
55
56
57 struct Volume
58 {
59 Volume(uint16_t _percent = 100, bool _muted = false) : percent(_percent), muted(_muted)
60 {
61 }
62
63 void fromJson(const json& j)
64 {
65 percent = jGet<uint16_t>(j, "percent", percent);
66 muted = jGet<bool>(j, "muted", muted);
67 }
68
69 json toJson()
70 {
71 json j;
72 j["percent"] = percent;
73 j["muted"] = muted;
74 return j;
75 }
76
77 uint16_t percent;
78 bool muted;
79 };
80
81
82
83 struct Host
84 {
85 Host() : name(""), mac(""), os(""), arch(""), ip("")
86 {
87 }
88
89 void update()
90 {
91 name = getHostName();
92 os = getOS();
93 arch = getArch();
94 }
95
96 void fromJson(const json& j)
97 {
98 name = strutils::trim_copy(jGet<std::string>(j, "name", ""));
99 mac = strutils::trim_copy(jGet<std::string>(j, "mac", ""));
100 os = strutils::trim_copy(jGet<std::string>(j, "os", ""));
101 arch = strutils::trim_copy(jGet<std::string>(j, "arch", ""));
102 ip = strutils::trim_copy(jGet<std::string>(j, "ip", ""));
103 }
104
105 json toJson()
106 {
107 json j;
108 j["name"] = name;
109 j["mac"] = mac;
110 j["os"] = os;
111 j["arch"] = arch;
112 j["ip"] = ip;
113 return j;
114 }
115
116 std::string name;
117 std::string mac;
118 std::string os;
119 std::string arch;
120 std::string ip;
121 };
122
123
124 struct ClientConfig
125 {
126 ClientConfig() : name(""), volume(100), latency(0), instance(1)
127 {
128 }
129
130 void fromJson(const json& j)
131 {
132 name = strutils::trim_copy(jGet<std::string>(j, "name", ""));
133 volume.fromJson(j["volume"]);
134 latency = jGet<int32_t>(j, "latency", 0);
135 instance = jGet<size_t>(j, "instance", 1);
136 }
137
138 json toJson()
139 {
140 json j;
141 j["name"] = strutils::trim_copy(name);
142 j["volume"] = volume.toJson();
143 j["latency"] = latency;
144 j["instance"] = instance;
145 return j;
146 }
147
148 std::string name;
149 Volume volume;
150 int32_t latency;
151 size_t instance;
152 };
153
154
155
156 struct Snapcast
157 {
158 Snapcast(const std::string& _name = "", const std::string& _version = "") : name(_name), version(_version), protocolVersion(1)
159 {
160 }
161
162 virtual ~Snapcast() = default;
163
164 virtual void fromJson(const json& j)
165 {
166 name = strutils::trim_copy(jGet<std::string>(j, "name", ""));
167 version = strutils::trim_copy(jGet<std::string>(j, "version", ""));
168 protocolVersion = jGet<int>(j, "protocolVersion", 1);
169 }
170
171 virtual json toJson()
172 {
173 json j;
174 j["name"] = strutils::trim_copy(name);
175 j["version"] = strutils::trim_copy(version);
176 j["protocolVersion"] = protocolVersion;
177 return j;
178 }
179
180 std::string name;
181 std::string version;
182 int protocolVersion;
183 };
184
185
186 struct Snapclient : public Snapcast
187 {
188 Snapclient(const std::string& _name = "", const std::string& _version = "") : Snapcast(_name, _version)
189 {
190 }
191 };
192
193
194 struct Snapserver : public Snapcast
195 {
196 Snapserver(const std::string& _name = "", const std::string& _version = "") : Snapcast(_name, _version), controlProtocolVersion(1)
197 {
198 }
199
200 void fromJson(const json& j) override
201 {
202 Snapcast::fromJson(j);
203 controlProtocolVersion = jGet<int>(j, "controlProtocolVersion", 1);
204 }
205
206 json toJson() override
207 {
208 json j = Snapcast::toJson();
209 j["controlProtocolVersion"] = controlProtocolVersion;
210 return j;
211 }
212
213 int controlProtocolVersion;
214 };
215
216
217 struct ClientInfo
218 {
219 ClientInfo(const std::string& _clientId = "") : id(_clientId), connected(false)
220 {
221 lastSeen.tv_sec = 0;
222 lastSeen.tv_usec = 0;
223 }
224
225 void fromJson(const json& j)
226 {
227 host.fromJson(j["host"]);
228 id = jGet<std::string>(j, "id", host.mac);
229 snapclient.fromJson(j["snapclient"]);
230 config.fromJson(j["config"]);
231 lastSeen.tv_sec = jGet<int32_t>(j["lastSeen"], "sec", 0);
232 lastSeen.tv_usec = jGet<int32_t>(j["lastSeen"], "usec", 0);
233 connected = jGet<bool>(j, "connected", true);
234 }
235
236 json toJson()
237 {
238 json j;
239 j["id"] = id;
240 j["host"] = host.toJson();
241 j["snapclient"] = snapclient.toJson();
242 j["config"] = config.toJson();
243 j["lastSeen"]["sec"] = lastSeen.tv_sec;
244 j["lastSeen"]["usec"] = lastSeen.tv_usec;
245 j["connected"] = connected;
246 return j;
247 }
248
249 std::string id;
250 Host host;
251 Snapclient snapclient;
252 ClientConfig config;
253 timeval lastSeen;
254 bool connected;
255 };
256
257
258 struct Group
259 {
260 Group(const ClientInfoPtr client = nullptr) : muted(false)
261 {
262 if (client)
263 id = client->id;
264 id = generateUUID();
265 }
266
267 void fromJson(const json& j)
268 {
269 name = strutils::trim_copy(jGet<std::string>(j, "name", ""));
270 id = strutils::trim_copy(jGet<std::string>(j, "id", ""));
271 streamId = strutils::trim_copy(jGet<std::string>(j, "stream_id", ""));
272 muted = jGet<bool>(j, "muted", false);
273 clients.clear();
274 if (j.count("clients"))
275 {
276 for (auto& jClient : j["clients"])
277 {
278 ClientInfoPtr client = std::make_shared<ClientInfo>();
279 client->fromJson(jClient);
280 client->connected = false;
281 addClient(client);
282 }
283 }
284 }
285
286 json toJson()
287 {
288 json j;
289 j["name"] = strutils::trim_copy(name);
290 j["id"] = strutils::trim_copy(id);
291 j["stream_id"] = strutils::trim_copy(streamId);
292 j["muted"] = muted;
293
294 json jClients = json::array();
295 for (auto client : clients)
296 jClients.push_back(client->toJson());
297 j["clients"] = jClients;
298 return j;
299 }
300
301 ClientInfoPtr removeClient(const std::string& clientId)
302 {
303 for (auto iter = clients.begin(); iter != clients.end(); ++iter)
304 {
305 if ((*iter)->id == clientId)
306 {
307 clients.erase(iter);
308 return (*iter);
309 }
310 }
311 return nullptr;
312 }
313
314 ClientInfoPtr removeClient(ClientInfoPtr client)
315 {
316 if (!client)
317 return nullptr;
318
319 return removeClient(client->id);
320 }
321
322 ClientInfoPtr getClient(const std::string& clientId)
323 {
324 for (auto client : clients)
325 {
326 if (client->id == clientId)
327 return client;
328 }
329 return nullptr;
330 }
331
332 void addClient(ClientInfoPtr client)
333 {
334 if (!client)
335 return;
336
337 for (auto c : clients)
338 {
339 if (c->id == client->id)
340 return;
341 }
342
343 clients.push_back(client);
344 /* sort(clients.begin(), clients.end(),
345 [](const ClientInfoPtr a, const ClientInfoPtr b) -> bool
346 {
347 return a.name > b.name;
348 });
349 */
350 }
351
352 bool empty() const
353 {
354 return clients.empty();
355 }
356
357 std::string name;
358 std::string id;
359 std::string streamId;
360 bool muted;
361 std::vector<ClientInfoPtr> clients;
362 };
363
364
365 class Config
366 {
367 public:
368 static Config& instance()
369 {
370 static Config instance_;
371 return instance_;
372 }
373
374 ClientInfoPtr getClientInfo(const std::string& clientId) const;
375 GroupPtr addClientInfo(const std::string& clientId);
376 GroupPtr addClientInfo(ClientInfoPtr client);
377 void remove(ClientInfoPtr client);
378 void remove(GroupPtr group, bool force = false);
379
380 // GroupPtr removeFromGroup(const std::string& groupId, const std::string& clientId);
381 // GroupPtr setGroupForClient(const std::string& groupId, const std::string& clientId);
382
383 GroupPtr getGroupFromClient(const std::string& clientId);
384 GroupPtr getGroupFromClient(ClientInfoPtr client);
385 GroupPtr getGroup(const std::string& groupId) const;
386
387 json getGroups() const;
388 json getServerStatus(const json& streams) const;
389
390 void save();
391
392 void init(const std::string& root_directory = "", const std::string& user = "", const std::string& group = "");
393
394 std::vector<GroupPtr> groups;
395
396 private:
397 Config();
398 ~Config();
399 std::string filename_;
400 };
401
402
403 #endif
0 /***
1 This file is part of snapcast
2 Copyright (C) 2014-2020 Johannes Pohl
3
4 This program is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, either version 3 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see <http://www.gnu.org/licenses/>.
16 ***/
17
18 #ifndef CONFIG_H
19 #define CONFIG_H
20
21 #include <memory>
22 #include <string>
23 #include <sys/time.h>
24 #include <vector>
25
26 #include "common/json.hpp"
27 #include "common/utils.hpp"
28 #include "common/utils/string_utils.hpp"
29
30
31 namespace strutils = utils::string;
32 using json = nlohmann::json;
33
34 struct ClientInfo;
35 struct Group;
36
37 typedef std::shared_ptr<ClientInfo> ClientInfoPtr;
38 typedef std::shared_ptr<Group> GroupPtr;
39
40
41 template <typename T>
42 T jGet(const json& j, const std::string& what, const T& def)
43 {
44 try
45 {
46 if (!j.count(what))
47 return def;
48 return j[what].get<T>();
49 }
50 catch (...)
51 {
52 return def;
53 }
54 }
55
56
57 struct Volume
58 {
59 Volume(uint16_t _percent = 100, bool _muted = false) : percent(_percent), muted(_muted)
60 {
61 }
62
63 void fromJson(const json& j)
64 {
65 percent = jGet<uint16_t>(j, "percent", percent);
66 muted = jGet<bool>(j, "muted", muted);
67 }
68
69 json toJson()
70 {
71 json j;
72 j["percent"] = percent;
73 j["muted"] = muted;
74 return j;
75 }
76
77 uint16_t percent;
78 bool muted;
79 };
80
81
82
83 struct Host
84 {
85 Host() : name(""), mac(""), os(""), arch(""), ip("")
86 {
87 }
88
89 void update()
90 {
91 name = getHostName();
92 os = getOS();
93 arch = getArch();
94 }
95
96 void fromJson(const json& j)
97 {
98 name = strutils::trim_copy(jGet<std::string>(j, "name", ""));
99 mac = strutils::trim_copy(jGet<std::string>(j, "mac", ""));
100 os = strutils::trim_copy(jGet<std::string>(j, "os", ""));
101 arch = strutils::trim_copy(jGet<std::string>(j, "arch", ""));
102 ip = strutils::trim_copy(jGet<std::string>(j, "ip", ""));
103 }
104
105 json toJson()
106 {
107 json j;
108 j["name"] = name;
109 j["mac"] = mac;
110 j["os"] = os;
111 j["arch"] = arch;
112 j["ip"] = ip;
113 return j;
114 }
115
116 std::string name;
117 std::string mac;
118 std::string os;
119 std::string arch;
120 std::string ip;
121 };
122
123
124 struct ClientConfig
125 {
126 ClientConfig() : name(""), volume(100), latency(0), instance(1)
127 {
128 }
129
130 void fromJson(const json& j)
131 {
132 name = strutils::trim_copy(jGet<std::string>(j, "name", ""));
133 volume.fromJson(j["volume"]);
134 latency = jGet<int32_t>(j, "latency", 0);
135 instance = jGet<size_t>(j, "instance", 1);
136 }
137
138 json toJson()
139 {
140 json j;
141 j["name"] = strutils::trim_copy(name);
142 j["volume"] = volume.toJson();
143 j["latency"] = latency;
144 j["instance"] = instance;
145 return j;
146 }
147
148 std::string name;
149 Volume volume;
150 int32_t latency;
151 size_t instance;
152 };
153
154
155
156 struct Snapcast
157 {
158 Snapcast(const std::string& _name = "", const std::string& _version = "") : name(_name), version(_version), protocolVersion(1)
159 {
160 }
161
162 virtual ~Snapcast() = default;
163
164 virtual void fromJson(const json& j)
165 {
166 name = strutils::trim_copy(jGet<std::string>(j, "name", ""));
167 version = strutils::trim_copy(jGet<std::string>(j, "version", ""));
168 protocolVersion = jGet<int>(j, "protocolVersion", 1);
169 }
170
171 virtual json toJson()
172 {
173 json j;
174 j["name"] = strutils::trim_copy(name);
175 j["version"] = strutils::trim_copy(version);
176 j["protocolVersion"] = protocolVersion;
177 return j;
178 }
179
180 std::string name;
181 std::string version;
182 int protocolVersion;
183 };
184
185
186 struct Snapclient : public Snapcast
187 {
188 Snapclient(const std::string& _name = "", const std::string& _version = "") : Snapcast(_name, _version)
189 {
190 }
191 };
192
193
194 struct Snapserver : public Snapcast
195 {
196 Snapserver(const std::string& _name = "", const std::string& _version = "") : Snapcast(_name, _version), controlProtocolVersion(1)
197 {
198 }
199
200 void fromJson(const json& j) override
201 {
202 Snapcast::fromJson(j);
203 controlProtocolVersion = jGet<int>(j, "controlProtocolVersion", 1);
204 }
205
206 json toJson() override
207 {
208 json j = Snapcast::toJson();
209 j["controlProtocolVersion"] = controlProtocolVersion;
210 return j;
211 }
212
213 int controlProtocolVersion;
214 };
215
216
217 struct ClientInfo
218 {
219 ClientInfo(const std::string& _clientId = "") : id(_clientId), connected(false)
220 {
221 lastSeen.tv_sec = 0;
222 lastSeen.tv_usec = 0;
223 }
224
225 void fromJson(const json& j)
226 {
227 host.fromJson(j["host"]);
228 id = jGet<std::string>(j, "id", host.mac);
229 snapclient.fromJson(j["snapclient"]);
230 config.fromJson(j["config"]);
231 lastSeen.tv_sec = jGet<int32_t>(j["lastSeen"], "sec", 0);
232 lastSeen.tv_usec = jGet<int32_t>(j["lastSeen"], "usec", 0);
233 connected = jGet<bool>(j, "connected", true);
234 }
235
236 json toJson()
237 {
238 json j;
239 j["id"] = id;
240 j["host"] = host.toJson();
241 j["snapclient"] = snapclient.toJson();
242 j["config"] = config.toJson();
243 j["lastSeen"]["sec"] = lastSeen.tv_sec;
244 j["lastSeen"]["usec"] = lastSeen.tv_usec;
245 j["connected"] = connected;
246 return j;
247 }
248
249 std::string id;
250 Host host;
251 Snapclient snapclient;
252 ClientConfig config;
253 timeval lastSeen;
254 bool connected;
255 };
256
257
258 struct Group
259 {
260 Group(const ClientInfoPtr client = nullptr) : muted(false)
261 {
262 if (client)
263 id = client->id;
264 id = generateUUID();
265 }
266
267 void fromJson(const json& j)
268 {
269 name = strutils::trim_copy(jGet<std::string>(j, "name", ""));
270 id = strutils::trim_copy(jGet<std::string>(j, "id", ""));
271 streamId = strutils::trim_copy(jGet<std::string>(j, "stream_id", ""));
272 muted = jGet<bool>(j, "muted", false);
273 clients.clear();
274 if (j.count("clients"))
275 {
276 for (auto& jClient : j["clients"])
277 {
278 ClientInfoPtr client = std::make_shared<ClientInfo>();
279 client->fromJson(jClient);
280 client->connected = false;
281 addClient(client);
282 }
283 }
284 }
285
286 json toJson()
287 {
288 json j;
289 j["name"] = strutils::trim_copy(name);
290 j["id"] = strutils::trim_copy(id);
291 j["stream_id"] = strutils::trim_copy(streamId);
292 j["muted"] = muted;
293
294 json jClients = json::array();
295 for (auto client : clients)
296 jClients.push_back(client->toJson());
297 j["clients"] = jClients;
298 return j;
299 }
300
301 ClientInfoPtr removeClient(const std::string& clientId)
302 {
303 for (auto iter = clients.begin(); iter != clients.end(); ++iter)
304 {
305 if ((*iter)->id == clientId)
306 {
307 clients.erase(iter);
308 return (*iter);
309 }
310 }
311 return nullptr;
312 }
313
314 ClientInfoPtr removeClient(ClientInfoPtr client)
315 {
316 if (!client)
317 return nullptr;
318
319 return removeClient(client->id);
320 }
321
322 ClientInfoPtr getClient(const std::string& clientId)
323 {
324 for (auto client : clients)
325 {
326 if (client->id == clientId)
327 return client;
328 }
329 return nullptr;
330 }
331
332 void addClient(ClientInfoPtr client)
333 {
334 if (!client)
335 return;
336
337 for (auto c : clients)
338 {
339 if (c->id == client->id)
340 return;
341 }
342
343 clients.push_back(client);
344 /* sort(clients.begin(), clients.end(),
345 [](const ClientInfoPtr a, const ClientInfoPtr b) -> bool
346 {
347 return a.name > b.name;
348 });
349 */
350 }
351
352 bool empty() const
353 {
354 return clients.empty();
355 }
356
357 std::string name;
358 std::string id;
359 std::string streamId;
360 bool muted;
361 std::vector<ClientInfoPtr> clients;
362 };
363
364
365 class Config
366 {
367 public:
368 static Config& instance()
369 {
370 static Config instance_;
371 return instance_;
372 }
373
374 ClientInfoPtr getClientInfo(const std::string& clientId) const;
375 GroupPtr addClientInfo(const std::string& clientId);
376 GroupPtr addClientInfo(ClientInfoPtr client);
377 void remove(ClientInfoPtr client);
378 void remove(GroupPtr group, bool force = false);
379
380 // GroupPtr removeFromGroup(const std::string& groupId, const std::string& clientId);
381 // GroupPtr setGroupForClient(const std::string& groupId, const std::string& clientId);
382
383 GroupPtr getGroupFromClient(const std::string& clientId);
384 GroupPtr getGroupFromClient(ClientInfoPtr client);
385 GroupPtr getGroup(const std::string& groupId) const;
386
387 json getGroups() const;
388 json getServerStatus(const json& streams) const;
389
390 void save();
391
392 void init(const std::string& root_directory = "", const std::string& user = "", const std::string& group = "");
393
394 std::vector<GroupPtr> groups;
395
396 private:
397 Config();
398 ~Config();
399 std::string filename_;
400 };
401
402
403 #endif
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
1919 #include "common/aixlog.hpp"
2020 #include "common/snap_exception.hpp"
2121 #include "common/utils.hpp"
22 #include "config.h"
22 #include "config.hpp"
2323 #include "control_session_http.hpp"
2424 #include "control_session_tcp.hpp"
2525 #include "jsonrpcpp.hpp"
3131 using json = nlohmann::json;
3232
3333
34 ControlServer::ControlServer(boost::asio::io_context* io_context, const ServerSettings::TcpSettings& tcp_settings,
34 ControlServer::ControlServer(boost::asio::io_context& io_context, const ServerSettings::TcpSettings& tcp_settings,
3535 const ServerSettings::HttpSettings& http_settings, ControlMessageReceiver* controlMessageReceiver)
3636 : io_context_(io_context), tcp_settings_(tcp_settings), http_settings_(http_settings), controlMessageReceiver_(controlMessageReceiver)
3737 {
7373
7474 std::string ControlServer::onMessageReceived(ControlSession* connection, const std::string& message)
7575 {
76 std::lock_guard<std::recursive_mutex> mlock(session_mutex_);
77 LOG(DEBUG) << "received: \"" << message << "\"\n";
76 // LOG(DEBUG) << "received: \"" << message << "\"\n";
7877 if (controlMessageReceiver_ != nullptr)
7978 return controlMessageReceiver_->onMessageReceived(connection, message);
8079 return "";
117116 setsockopt(socket.native_handle(), SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
118117 // socket->set_option(boost::asio::ip::tcp::no_delay(false));
119118 SLOG(NOTICE) << "ControlServer::NewConnection: " << socket.remote_endpoint().address().to_string() << endl;
120 shared_ptr<SessionType> session = make_shared<SessionType>(this, std::move(socket), std::forward<Args>(args)...);
119 shared_ptr<SessionType> session = make_shared<SessionType>(this, io_context_, std::move(socket), std::forward<Args>(args)...);
121120 {
122121 std::lock_guard<std::recursive_mutex> mlock(session_mutex_);
123122 session->start();
144143 {
145144 LOG(INFO) << "Creating TCP acceptor for address: " << address << ", port: " << tcp_settings_.port << "\n";
146145 acceptor_tcp_.emplace_back(
147 make_unique<tcp::acceptor>(*io_context_, tcp::endpoint(boost::asio::ip::address::from_string(address), tcp_settings_.port)));
146 make_unique<tcp::acceptor>(io_context_, tcp::endpoint(boost::asio::ip::address::from_string(address), tcp_settings_.port)));
148147 }
149148 catch (const boost::system::system_error& e)
150149 {
160159 {
161160 LOG(INFO) << "Creating HTTP acceptor for address: " << address << ", port: " << http_settings_.port << "\n";
162161 acceptor_http_.emplace_back(
163 make_unique<tcp::acceptor>(*io_context_, tcp::endpoint(boost::asio::ip::address::from_string(address), http_settings_.port)));
162 make_unique<tcp::acceptor>(io_context_, tcp::endpoint(boost::asio::ip::address::from_string(address), http_settings_.port)));
164163 }
165164 catch (const boost::system::system_error& e)
166165 {
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
2222 #include <memory>
2323 #include <mutex>
2424 #include <set>
25 #include <sstream>
26 #include <thread>
2725 #include <vector>
2826
2927 #include "common/queue.h"
4442 class ControlServer : public ControlMessageReceiver
4543 {
4644 public:
47 ControlServer(boost::asio::io_context* io_context, const ServerSettings::TcpSettings& tcp_settings, const ServerSettings::HttpSettings& http_settings,
45 ControlServer(boost::asio::io_context& io_context, const ServerSettings::TcpSettings& tcp_settings, const ServerSettings::HttpSettings& http_settings,
4846 ControlMessageReceiver* controlMessageReceiver = nullptr);
4947 virtual ~ControlServer();
5048
7068 std::vector<acceptor_ptr> acceptor_tcp_;
7169 std::vector<acceptor_ptr> acceptor_http_;
7270
73 boost::asio::io_context* io_context_;
71 boost::asio::io_context& io_context_;
7472 ServerSettings::TcpSettings tcp_settings_;
7573 ServerSettings::HttpSettings http_settings_;
7674 ControlMessageReceiver* controlMessageReceiver_;
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
2828 #include <mutex>
2929 #include <set>
3030 #include <string>
31 #include <thread>
3231
3332 using boost::asio::ip::tcp;
3433
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
9797 }
9898 } // namespace
9999
100 ControlSessionHttp::ControlSessionHttp(ControlMessageReceiver* receiver, tcp::socket&& socket, const ServerSettings::HttpSettings& settings)
101 : ControlSession(receiver), socket_(std::move(socket)), settings_(settings)
100 ControlSessionHttp::ControlSessionHttp(ControlMessageReceiver* receiver, boost::asio::io_context& ioc, tcp::socket&& socket,
101 const ServerSettings::HttpSettings& settings)
102 : ControlSession(receiver), socket_(std::move(socket)), settings_(settings), strand_(ioc)
102103 {
103104 LOG(DEBUG) << "ControlSessionHttp\n";
104105 }
114115 void ControlSessionHttp::start()
115116 {
116117 auto self = shared_from_this();
117 http::async_read(socket_, buffer_, req_, [this, self](boost::system::error_code ec, std::size_t bytes) { on_read(ec, bytes); });
118 http::async_read(socket_, buffer_, req_,
119 boost::asio::bind_executor(strand_, [this, self](boost::system::error_code ec, std::size_t bytes) { on_read(ec, bytes); }));
118120 }
119121
120122
269271
270272 // Write the response
271273 auto self = this->shared_from_this();
272 http::async_write(this->socket_, *sp, [this, self, sp](beast::error_code ec, std::size_t bytes) { this->on_write(ec, bytes, sp->need_eof()); });
274 http::async_write(this->socket_, *sp, boost::asio::bind_executor(strand_, [this, self, sp](beast::error_code ec, std::size_t bytes) {
275 this->on_write(ec, bytes, sp->need_eof());
276 }));
273277 });
274278 }
275279
296300 req_ = {};
297301
298302 // Read another request
299 http::async_read(socket_, buffer_, req_, [ this, self = shared_from_this() ](beast::error_code ec, std::size_t bytes) { on_read(ec, bytes); });
303 http::async_read(socket_, buffer_, req_,
304 boost::asio::bind_executor(strand_, [ this, self = shared_from_this() ](beast::error_code ec, std::size_t bytes) { on_read(ec, bytes); }));
300305 }
301306
302307
304309 {
305310 }
306311
307
308312 void ControlSessionHttp::sendAsync(const std::string& message)
309313 {
310314 if (!ws_)
311315 return;
312316
317 strand_.post([this, message]() {
318 messages_.emplace_back(message);
319 if (messages_.size() > 1)
320 {
321 LOG(DEBUG) << "HTTP session outstanding async_writes: " << messages_.size() << "\n";
322 return;
323 }
324 send_next();
325 });
326 }
327
328 void ControlSessionHttp::send_next()
329 {
330 if (!ws_)
331 return;
332
313333 auto self(shared_from_this());
314 ws_->async_write(boost::asio::buffer(message), [this, self](std::error_code ec, std::size_t length) {
315 if (ec)
316 {
317 LOG(ERROR) << "Error while writing to control socket: " << ec.message() << "\n";
318 }
319 else
320 {
321 LOG(DEBUG) << "Wrote " << length << " bytes to control socket\n";
322 }
323 });
334 auto message = messages_.front();
335 ws_->async_write(boost::asio::buffer(message), boost::asio::bind_executor(strand_, [this, self](std::error_code ec, std::size_t length) {
336 messages_.pop_front();
337 if (ec)
338 {
339 LOG(ERROR) << "Error while writing to web socket: " << ec.message() << "\n";
340 }
341 else
342 {
343 LOG(DEBUG) << "Wrote " << length << " bytes to web socket\n";
344 }
345 if (!messages_.empty())
346 send_next();
347 }));
324348 }
325349
326350
350374 {
351375 // Read a message into our buffer
352376 auto self(shared_from_this());
353 ws_->async_read(buffer_, [this, self](beast::error_code ec, std::size_t bytes_transferred) { on_read_ws(ec, bytes_transferred); });
377 ws_->async_read(
378 buffer_, boost::asio::bind_executor(strand_, [this, self](beast::error_code ec, std::size_t bytes_transferred) { on_read_ws(ec, bytes_transferred); }));
354379 }
355380
356381
371396 std::string line{boost::beast::buffers_to_string(buffer_.data())};
372397 if (!line.empty())
373398 {
374 LOG(DEBUG) << "received: " << line << "\n";
399 // LOG(DEBUG) << "received: " << line << "\n";
375400 if ((message_receiver_ != nullptr) && !line.empty())
376401 {
377402 string response = message_receiver_->onMessageReceived(this, line);
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
2121 #include "control_session.hpp"
2222 #include <boost/beast/core.hpp>
2323 #include <boost/beast/websocket.hpp>
24 #include <deque>
2425
2526 namespace beast = boost::beast; // from <boost/beast.hpp>
2627 namespace http = beast::http; // from <boost/beast/http.hpp>
3839 {
3940 public:
4041 /// ctor. Received message from the client are passed to MessageReceiver
41 ControlSessionHttp(ControlMessageReceiver* receiver, tcp::socket&& socket, const ServerSettings::HttpSettings& settings);
42 ControlSessionHttp(ControlMessageReceiver* receiver, boost::asio::io_context& ioc, tcp::socket&& socket, const ServerSettings::HttpSettings& settings);
4243 ~ControlSessionHttp() override;
4344 void start() override;
4445 void stop() override;
5758 template <class Body, class Allocator, class Send>
5859 void handle_request(http::request<Body, http::basic_fields<Allocator>>&& req, Send&& send);
5960
61 void send_next();
62
6063 http::request<http::string_body> req_;
6164
6265 protected:
7174 tcp::socket socket_;
7275 beast::flat_buffer buffer_;
7376 ServerSettings::HttpSettings settings_;
77 boost::asio::io_context::strand strand_;
78 std::deque<std::string> messages_;
7479 };
7580
7681
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
2121
2222 using namespace std;
2323
24 // https://stackoverflow.com/questions/7754695/boost-asio-async-write-how-to-not-interleaving-async-write-calls/7756894
2425
2526
26 ControlSessionTcp::ControlSessionTcp(ControlMessageReceiver* receiver, tcp::socket&& socket) : ControlSession(receiver), socket_(std::move(socket))
27 ControlSessionTcp::ControlSessionTcp(ControlMessageReceiver* receiver, boost::asio::io_context& ioc, tcp::socket&& socket)
28 : ControlSession(receiver), socket_(std::move(socket)), strand_(ioc)
2729 {
2830 }
2931
3941 {
4042 const std::string delimiter = "\n";
4143 auto self(shared_from_this());
42 boost::asio::async_read_until(socket_, streambuf_, delimiter, [this, self, delimiter](const std::error_code& ec, std::size_t bytes_transferred) {
43 if (ec)
44 {
45 LOG(ERROR) << "Error while reading from control socket: " << ec.message() << "\n";
46 return;
47 }
44 boost::asio::async_read_until(
45 socket_, streambuf_, delimiter, boost::asio::bind_executor(strand_, [this, self, delimiter](const std::error_code& ec, std::size_t bytes_transferred) {
46 if (ec)
47 {
48 LOG(ERROR) << "Error while reading from control socket: " << ec.message() << "\n";
49 return;
50 }
4851
49 // Extract up to the first delimiter.
50 std::string line{buffers_begin(streambuf_.data()), buffers_begin(streambuf_.data()) + bytes_transferred - delimiter.length()};
51 if (!line.empty())
52 {
53 if (line.back() == '\r')
54 line.resize(line.size() - 1);
55 LOG(DEBUG) << "received: " << line << "\n";
56 if ((message_receiver_ != nullptr) && !line.empty())
52 // Extract up to the first delimiter.
53 std::string line{buffers_begin(streambuf_.data()), buffers_begin(streambuf_.data()) + bytes_transferred - delimiter.length()};
54 if (!line.empty())
5755 {
58 string response = message_receiver_->onMessageReceived(this, line);
59 if (!response.empty())
60 sendAsync(response);
56 if (line.back() == '\r')
57 line.resize(line.size() - 1);
58 // LOG(DEBUG) << "received: " << line << "\n";
59 if ((message_receiver_ != nullptr) && !line.empty())
60 {
61 string response = message_receiver_->onMessageReceived(this, line);
62 if (!response.empty())
63 sendAsync(response);
64 }
6165 }
62 }
63 streambuf_.consume(bytes_transferred);
64 do_read();
65 });
66 streambuf_.consume(bytes_transferred);
67 do_read();
68 }));
6669 }
6770
6871
8891
8992 void ControlSessionTcp::sendAsync(const std::string& message)
9093 {
91 auto self(shared_from_this());
92 boost::asio::async_write(socket_, boost::asio::buffer(message + "\r\n"), [this, self](std::error_code ec, std::size_t length) {
93 if (ec)
94 strand_.post([this, message]() {
95 messages_.emplace_back(message);
96 if (messages_.size() > 1)
9497 {
95 LOG(ERROR) << "Error while writing to control socket: " << ec.message() << "\n";
98 LOG(DEBUG) << "TCP session outstanding async_writes: " << messages_.size() << "\n";
99 return;
96100 }
97 else
98 {
99 LOG(DEBUG) << "Wrote " << length << " bytes to control socket\n";
100 }
101 send_next();
101102 });
102103 }
103104
105 void ControlSessionTcp::send_next()
106 {
107 auto self(shared_from_this());
108 auto message = messages_.front();
109 boost::asio::async_write(socket_, boost::asio::buffer(message + "\r\n"),
110 boost::asio::bind_executor(strand_, [this, self](std::error_code ec, std::size_t length) {
111 messages_.pop_front();
112 if (ec)
113 {
114 LOG(ERROR) << "Error while writing to control socket: " << ec.message() << "\n";
115 }
116 else
117 {
118 LOG(DEBUG) << "Wrote " << length << " bytes to control socket\n";
119 }
120 if (!messages_.empty())
121 send_next();
122 }));
123 }
104124
105125 bool ControlSessionTcp::send(const std::string& message)
106126 {
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
1919 #define CONTROL_SESSION_TCP_HPP
2020
2121 #include "control_session.hpp"
22 #include <deque>
2223
2324 /// Endpoint for a connected control client.
2425 /**
3031 {
3132 public:
3233 /// ctor. Received message from the client are passed to MessageReceiver
33 ControlSessionTcp(ControlMessageReceiver* receiver, tcp::socket&& socket);
34 ControlSessionTcp(ControlMessageReceiver* receiver, boost::asio::io_context& ioc, tcp::socket&& socket);
3435 ~ControlSessionTcp() override;
3536 void start() override;
3637 void stop() override;
4344
4445 protected:
4546 void do_read();
47 void send_next();
48
4649 tcp::socket socket_;
4750 boost::asio::streambuf streambuf_;
51 boost::asio::io_context::strand strand_;
52 std::deque<std::string> messages_;
4853 };
4954
5055
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
2525 #include "message/codec_header.hpp"
2626 #include "message/pcm_chunk.hpp"
2727
28 namespace encoder
29 {
2830
2931 class Encoder;
3032
9597 std::string codecOptions_;
9698 };
9799
100 } // namespace encoder
98101
99102 #endif
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
1717
1818 #include "encoder_factory.hpp"
1919 #include "pcm_encoder.hpp"
20 #if defined(HAS_OGG) && defined(HAS_VORBIS) && defined(HAS_VORBISENC)
20 #if defined(HAS_OGG) && defined(HAS_VORBIS) && defined(HAS_VORBIS_ENC)
2121 #include "ogg_encoder.hpp"
2222 #endif
2323 #if defined(HAS_FLAC)
2424 #include "flac_encoder.hpp"
25 #endif
26 #if defined(HAS_OPUS)
27 #include "opus_encoder.hpp"
2528 #endif
2629 #include "common/aixlog.hpp"
2730 #include "common/snap_exception.hpp"
3033
3134 using namespace std;
3235
36 namespace encoder
37 {
3338
34 Encoder* EncoderFactory::createEncoder(const std::string& codecSettings) const
39 std::unique_ptr<Encoder> EncoderFactory::createEncoder(const std::string& codecSettings) const
3540 {
36 Encoder* encoder;
3741 std::string codec(codecSettings);
3842 std::string codecOptions;
3943 if (codec.find(":") != std::string::npos)
4246 codec = utils::string::trim_copy(codec.substr(0, codec.find(":")));
4347 }
4448 if (codec == "pcm")
45 encoder = new PcmEncoder(codecOptions);
46 #if defined(HAS_OGG) && defined(HAS_VORBIS) && defined(HAS_VORBISENC)
49 return std::make_unique<PcmEncoder>(codecOptions);
50 #if defined(HAS_OGG) && defined(HAS_VORBIS) && defined(HAS_VORBIS_ENC)
4751 else if (codec == "ogg")
48 encoder = new OggEncoder(codecOptions);
52 return std::make_unique<OggEncoder>(codecOptions);
4953 #endif
5054 #if defined(HAS_FLAC)
5155 else if (codec == "flac")
52 encoder = new FlacEncoder(codecOptions);
56 return std::make_unique<FlacEncoder>(codecOptions);
5357 #endif
54 else
55 {
56 throw SnapException("unknown codec: " + codec);
57 }
58 #if defined(HAS_OPUS)
59 else if (codec == "opus")
60 return std::make_unique<OpusEncoder>(codecOptions);
61 #endif
5862
59 return encoder;
60 /* try
61 {
62 encoder->init(NULL, format, codecOptions);
63 }
64 catch (const std::exception& e)
65 {
66 cout << "Error: " << e.what() << "\n";
67 return 1;
68 }
69 */
63 throw SnapException("unknown codec: " + codec);
7064 }
65
66 } // namespace encoder
11 #define ENCODER_FACTORY_H
22
33 #include "encoder.hpp"
4 #include <memory>
45 #include <string>
6
7 namespace encoder
8 {
59
610 class EncoderFactory
711 {
812 public:
913 // EncoderFactory(const std::string& codecSettings);
10 Encoder* createEncoder(const std::string& codecSettings) const;
14 std::unique_ptr<Encoder> createEncoder(const std::string& codecSettings) const;
1115 };
1216
17 } // namespace encoder
1318
1419 #endif
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
2424
2525 using namespace std;
2626
27 namespace encoder
28 {
2729
2830 FlacEncoder::FlacEncoder(const std::string& codecOptions) : Encoder(codecOptions), encoder_(nullptr), pcmBufferSize_(0), encodedSamples_(0)
2931 {
132134 return FLAC__STREAM_ENCODER_WRITE_STATUS_OK;
133135 }
134136
135
137 namespace callback
138 {
136139 FLAC__StreamEncoderWriteStatus write_callback(const FLAC__StreamEncoder* encoder, const FLAC__byte buffer[], size_t bytes, unsigned samples,
137140 unsigned current_frame, void* client_data)
138141 {
139142 FlacEncoder* flacEncoder = (FlacEncoder*)client_data;
140143 return flacEncoder->write_callback(encoder, buffer, bytes, samples, current_frame);
141144 }
142
145 } // namespace callback
143146
144147 void FlacEncoder::initEncoder()
145148 {
195198 throw SnapException("error setting meta data");
196199
197200 // initialize encoder
198 init_status = FLAC__stream_encoder_init_stream(encoder_, ::write_callback, nullptr, nullptr, nullptr, this);
201 init_status = FLAC__stream_encoder_init_stream(encoder_, callback::write_callback, nullptr, nullptr, nullptr, this);
199202 if (init_status != FLAC__STREAM_ENCODER_INIT_STATUS_OK)
200203 throw SnapException("ERROR: initializing encoder: " + string(FLAC__StreamEncoderInitStatusString[init_status]));
201204 }
205
206 } // namespace encoder
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
2525 #include "FLAC/metadata.h"
2626 #include "FLAC/stream_encoder.h"
2727
28 namespace encoder
29 {
2830
2931 class FlacEncoder : public Encoder
3032 {
5254 size_t encodedSamples_;
5355 };
5456
57 } // namespace encoder
5558
5659 #endif
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
2727
2828 using namespace std;
2929
30 namespace encoder
31 {
3032
3133 OggEncoder::OggEncoder(const std::string& codecOptions) : Encoder(codecOptions), lastGranulepos_(0)
3234 {
35 }
36
37
38 OggEncoder::~OggEncoder()
39 {
40 ogg_stream_clear(&os_);
41 vorbis_block_clear(&vb_);
42 vorbis_dsp_clear(&vd_);
43 vorbis_comment_clear(&vc_);
44 vorbis_info_clear(&vi_);
3345 }
3446
3547
256268 pos += og_.body_len;
257269 }
258270 }
271
272 } // namespace encoder
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
2121 #include <ogg/ogg.h>
2222 #include <vorbis/vorbisenc.h>
2323
24 namespace encoder
25 {
26
2427 class OggEncoder : public Encoder
2528 {
2629 public:
2730 OggEncoder(const std::string& codecOptions = "");
31 ~OggEncoder() override;
32
2833 void encode(const msg::PcmChunk* chunk) override;
2934 std::string getAvailableOptions() const override;
3035 std::string getDefaultOptions() const override;
4752 ogg_int64_t lastGranulepos_;
4853 };
4954
55 } // namespace encoder
5056
5157 #endif
0 /***
1 This file is part of snapcast
2 Copyright (C) 2015 Hannes Ellinger
3
4 This program is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, either version 3 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see <http://www.gnu.org/licenses/>.
16 ***/
17
18 #include "opus_encoder.hpp"
19 #include "common/aixlog.hpp"
20 #include "common/snap_exception.hpp"
21 #include "common/str_compat.hpp"
22 #include "common/utils/string_utils.hpp"
23
24 using namespace std;
25
26 namespace encoder
27 {
28
29 #define ID_OPUS 0x4F505553
30 static constexpr opus_int32 const_min_bitrate = 6000;
31 static constexpr opus_int32 const_max_bitrate = 512000;
32
33 namespace
34 {
35 template <typename T>
36 void assign(void* pointer, T val)
37 {
38 T* p = (T*)pointer;
39 *p = val;
40 }
41 } // namespace
42
43
44 OpusEncoder::OpusEncoder(const std::string& codecOptions) : Encoder(codecOptions), enc_(nullptr)
45 {
46 headerChunk_ = make_unique<msg::CodecHeader>("opus");
47 }
48
49
50 OpusEncoder::~OpusEncoder()
51 {
52 if (enc_ != nullptr)
53 opus_encoder_destroy(enc_);
54 }
55
56
57 std::string OpusEncoder::getAvailableOptions() const
58 {
59 return "BITRATE:[" + cpt::to_string(const_min_bitrate) + " - " + cpt::to_string(const_max_bitrate) + "|MAX|AUTO],COMPLEXITY:[1-10]";
60 }
61
62
63 std::string OpusEncoder::getDefaultOptions() const
64 {
65 return "BITRATE:192000,COMPLEXITY:10";
66 }
67
68
69 std::string OpusEncoder::name() const
70 {
71 return "opus";
72 }
73
74
75 void OpusEncoder::initEncoder()
76 {
77 // Opus is quite restrictive in sample rate and bit depth
78 // It can handle mono signals, but we will check for stereo
79 if ((sampleFormat_.rate != 48000) || (sampleFormat_.bits != 16) || (sampleFormat_.channels != 2))
80 throw SnapException("Opus sampleformat must be 48000:16:2");
81
82 opus_int32 bitrate = 192000;
83 opus_int32 complexity = 10;
84
85 // parse options: bitrate and complexity
86 auto options = utils::string::split(codecOptions_, ',');
87 for (const auto& option : options)
88 {
89 auto kv = utils::string::split(option, ':');
90 if (kv.size() == 2)
91 {
92 if (kv.front() == "BITRATE")
93 {
94 if (kv.back() == "MAX")
95 bitrate = OPUS_BITRATE_MAX;
96 else if (kv.back() == "AUTO")
97 bitrate = OPUS_AUTO;
98 else
99 {
100 try
101 {
102 bitrate = cpt::stoi(kv.back());
103 if ((bitrate < const_min_bitrate) || (bitrate > const_max_bitrate))
104 throw SnapException("Opus bitrate must be between " + cpt::to_string(const_min_bitrate) + " and " +
105 cpt::to_string(const_max_bitrate));
106 }
107 catch (const std::invalid_argument&)
108 {
109 throw SnapException("Opus error parsing bitrate (must be between " + cpt::to_string(const_min_bitrate) + " and " +
110 cpt::to_string(const_max_bitrate) + "): " + kv.back());
111 }
112 }
113 }
114 else if (kv.front() == "COMPLEXITY")
115 {
116 try
117 {
118 complexity = cpt::stoi(kv.back());
119 if ((complexity < 1) || (complexity > 10))
120 throw SnapException("Opus complexity must be between 1 and 10");
121 }
122 catch (const std::invalid_argument&)
123 {
124 throw SnapException("Opus error parsing complexity (must be between 1 and 10): " + kv.back());
125 }
126 }
127 else
128 throw SnapException("Opus unknown option: " + kv.front());
129 }
130 else
131 throw SnapException("Opus error parsing options: " + codecOptions_);
132 }
133
134 LOG(INFO) << "Opus bitrate: " << bitrate << " bps, complexity: " << complexity << "\n";
135
136 int error;
137 enc_ = opus_encoder_create(sampleFormat_.rate, sampleFormat_.channels, OPUS_APPLICATION_RESTRICTED_LOWDELAY, &error);
138 if (error != 0)
139 {
140 throw SnapException("Failed to initialize Opus encoder: " + std::string(opus_strerror(error)));
141 }
142
143 opus_encoder_ctl(enc_, OPUS_SET_BITRATE(bitrate));
144 opus_encoder_ctl(enc_, OPUS_SET_COMPLEXITY(complexity));
145
146 // create some opus pseudo header to let the decoder know about the sample format
147 headerChunk_->payloadSize = 12;
148 headerChunk_->payload = (char*)malloc(headerChunk_->payloadSize);
149 char* payload = headerChunk_->payload;
150 assign(payload, SWAP_32(ID_OPUS));
151 assign(payload + 4, SWAP_32(sampleFormat_.rate));
152 assign(payload + 8, SWAP_16(sampleFormat_.bits));
153 assign(payload + 10, SWAP_16(sampleFormat_.channels));
154
155 remainder_ = std::make_unique<msg::PcmChunk>(sampleFormat_, 10);
156 remainder_max_size_ = remainder_->payloadSize;
157 remainder_->payloadSize = 0;
158 }
159
160
161 // Opus encoder can only handle chunk sizes of:
162 // 5, 10, 20, 40, 60 ms
163 // 240, 480, 960, 1920, 2880 frames
164 // We will split the chunk into encodable sizes and store any remaining data in the remainder_ buffer
165 // and encode the buffer content in the next iteration
166 void OpusEncoder::encode(const msg::PcmChunk* chunk)
167 {
168 LOG(DEBUG) << "encode " << chunk->duration<std::chrono::milliseconds>().count() << "ms\n";
169 uint32_t offset = 0;
170
171 // check if there is something left from the last call to encode and fill the remainder buffer to
172 // an encodable size of 10ms
173 if (remainder_->payloadSize > 0)
174 {
175 offset = std::min(static_cast<uint32_t>(remainder_max_size_ - remainder_->payloadSize), chunk->payloadSize);
176 memcpy(remainder_->payload + remainder_->payloadSize, chunk->payload, offset);
177 LOG(DEBUG) << "remainder buffer size: " << remainder_->payloadSize << "/" << remainder_max_size_ << ", appending " << offset << " bytes\n";
178 remainder_->payloadSize += offset;
179
180 if (remainder_->payloadSize < remainder_max_size_)
181 {
182 LOG(DEBUG) << "not enough data to encode (" << remainder_->payloadSize << " of " << remainder_max_size_ << " bytes)"
183 << "\n";
184 return;
185 }
186 encode(chunk->format, remainder_->payload, remainder_->payloadSize);
187 remainder_->payloadSize = 0;
188 }
189
190 // encode greedy 60ms, 40ms, 20ms, 10ms chunks
191 std::vector<size_t> chunk_durations{60, 40, 20, 10};
192 for (const auto duration : chunk_durations)
193 {
194 auto ms2bytes = [this](size_t ms) { return (ms * sampleFormat_.msRate() * sampleFormat_.frameSize); };
195 uint32_t bytes = ms2bytes(duration);
196 while (chunk->payloadSize - offset >= bytes)
197 {
198 LOG(DEBUG) << "encoding " << duration << "ms (" << bytes << "), offset: " << offset << ", chunk size: " << chunk->payloadSize - offset << "\n";
199 encode(chunk->format, chunk->payload + offset, bytes);
200 offset += bytes;
201 }
202 if (chunk->payloadSize == offset)
203 break;
204 }
205
206 // something is left (must be less than 10ms)
207 if (chunk->payloadSize > offset)
208 {
209 memcpy(remainder_->payload + remainder_->payloadSize, chunk->payload + offset, chunk->payloadSize - offset);
210 remainder_->payloadSize = chunk->payloadSize - offset;
211 }
212 }
213
214
215 void OpusEncoder::encode(const SampleFormat& format, const char* data, size_t size)
216 {
217 // void* buffer;
218 // LOG(INFO) << "frames: " << chunk->readFrames(buffer, std::chrono::milliseconds(10)) << "\n";
219 int samples_per_channel = size / format.frameSize;
220 if (encoded_.size() < size)
221 encoded_.resize(size);
222
223 opus_int32 len = opus_encode(enc_, (opus_int16*)data, samples_per_channel, encoded_.data(), size);
224 LOG(DEBUG) << "Encode " << samples_per_channel << " frames, size " << size << " bytes, encoded: " << len << " bytes" << '\n';
225
226 if (len > 0)
227 {
228 // copy encoded data to chunk
229 auto* opusChunk = new msg::PcmChunk(format, 0);
230 opusChunk->payloadSize = len;
231 opusChunk->payload = (char*)realloc(opusChunk->payload, opusChunk->payloadSize);
232 memcpy(opusChunk->payload, encoded_.data(), len);
233 listener_->onChunkEncoded(this, opusChunk, (double)samples_per_channel / ((double)sampleFormat_.rate / 1000.));
234 }
235 else
236 {
237 LOG(ERROR) << "Failed to encode chunk: " << opus_strerror(len) << ", samples / channel: " << samples_per_channel << ", bytes: " << size << '\n';
238 }
239 }
240
241 } // namespace encoder
0 /***
1 This file is part of snapcast
2 Copyright (C) 2015 Hannes Ellinger
3
4 This program is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, either version 3 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see <http://www.gnu.org/licenses/>.
16 ***/
17
18 #pragma once
19
20 #include "encoder.hpp"
21 #include <opus/opus.h>
22
23
24 namespace encoder
25 {
26
27 class OpusEncoder : public Encoder
28 {
29 public:
30 OpusEncoder(const std::string& codecOptions = "");
31 ~OpusEncoder() override;
32
33 void encode(const msg::PcmChunk* chunk) override;
34 std::string getAvailableOptions() const override;
35 std::string getDefaultOptions() const override;
36 std::string name() const override;
37
38 protected:
39 void encode(const SampleFormat& format, const char* data, size_t size);
40 void initEncoder() override;
41 ::OpusEncoder* enc_;
42 std::vector<unsigned char> encoded_;
43 std::unique_ptr<msg::PcmChunk> remainder_;
44 size_t remainder_max_size_;
45 };
46
47 } // namespace encoder
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
2020 #include <memory>
2121
2222
23 namespace encoder
24 {
25
2326 #define ID_RIFF 0x46464952
2427 #define ID_WAVE 0x45564157
2528 #define ID_FMT 0x20746d66
2629 #define ID_DATA 0x61746164
30
31
32 namespace
33 {
34 template <typename T>
35 void assign(void* pointer, T val)
36 {
37 T* p = (T*)pointer;
38 *p = val;
39 }
40 } // namespace
2741
2842
2943 PcmEncoder::PcmEncoder(const std::string& codecOptions) : Encoder(codecOptions)
6478 {
6579 return "pcm";
6680 }
81
82 } // namespace encoder
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
1919 #define PCM_ENCODER_H
2020 #include "encoder.hpp"
2121
22 namespace encoder
23 {
2224
2325 class PcmEncoder : public Encoder
2426 {
2931
3032 protected:
3133 void initEncoder() override;
32
33 template <typename T>
34 void assign(void* pointer, T val)
35 {
36 T* p = (T*)pointer;
37 *p = val;
38 }
3934 };
4035
36 } // namespace encoder
4137
4238 #endif
11
22 <head>
33 <title>Snapcast Interface</title>
4 Taken from <a href="https://github.com/derglaus/snapcast-websockets-ui">derglaus/snapcast-websockets-ui</a> for testing purposes
4 Taken from <a href="https://github.com/derglaus/snapcast-websockets-ui">derglaus/snapcast-websockets-ui</a> for
5 testing purposes
56 <script>
6 var connection = new WebSocket('ws://127.0.0.1:1780/jsonrpc');
7
8 var connection = new WebSocket('ws://' + window.location.hostname + ':1780/jsonrpc');
9
710 var server;
811
912 connection.onmessage = function (e) {
10 var recv = e.data;
11 //String.fromCharCode.apply(null, new Uint8Array(e.data));
12 console.log(recv);
13 var recv = e.data
14 // console.log(recv);
1315 var answer = JSON.parse(recv);
14 if (answer.id == 1) { server = answer.result; }
15 // console.log(answer.method);
16 if (answer.method == "Client.OnVolumeChanged" || answer.method == "Client.OnLatencyChanged" || answer.method == "Client.OnNameChanged") { clientChange(answer.params); }
17 if (answer.method == "Client.OnConnect" || answer.method == "Client.OnDisconnect") { clientConnect(answer.params); }
18 if (answer.method == "Group.OnMute") { groupMute(answer.params); }
19 if (answer.method == "Group.OnStreamChanged") { groupStream(answer.params); }
20 if (answer.method == "Stream.OnUpdate") { streamUpdate(answer.params); }
21 if (answer.method == "Server.OnUpdate") { server = answer.params }
16 console.log(answer)
17 if (answer.id == 1) {
18 server = answer.result;
19 } else if (Array.isArray(answer)) {
20 for (let i = 0; i < answer.length; i++) {
21 action(answer[i]);
22 }
23 } else {
24 action(answer);
25
26 }
27
2228 show()
2329 }
2430
3036 alert("error");
3137 }
3238
39 function action(answer) {
40 switch (answer.method) {
41 case 'Client.OnVolumeChanged':
42 case 'Client.OnLatencyChanged':
43 case 'Client.OnNameChanged':
44 clientChange(answer.params);
45 break;
46 case 'Client.OnConnect':
47 case 'Client.OnDisconnect':
48 clientConnect(answer.params);
49 break;
50 case 'Group.OnMute':
51 groupMute(answer.params);
52 break;
53 case 'Group.OnStremChanged':
54 groupStream(answer.params);
55 break;
56 case 'Stream.OnUpdate':
57 streamUpdate(answer.params);
58 break;
59 case 'Server.OnUpdate':
60 server = answer.params;
61 break;
62 default:
63 break;
64 }
65 }
66
3367 function send(str) {
34 connection.send(str)
35 }
36
37 function clientChange(params) {//console.log(params);
38 var i_group = 0
39 while (i_group < server.server.groups.length) {
40 var i_client = 0
41 while (i_client < server.server.groups[i_group].clients.length) {
42 if (server.server.groups[i_group].clients[i_client].id == params.id) {//console.log(server.server.groups[i_group].clients[i_client]);
68 var buf = new ArrayBuffer(str.length);
69 var bufView = new Uint8Array(buf);
70 for (var i = 0, strLen = str.length; i < strLen; i++) {
71 bufView[i] = str.charCodeAt(i);
72 }
73
74 // console.log(buf);
75 var recv = String.fromCharCode.apply(null, new Uint8Array(buf));
76 // console.log(recv);
77
78 connection.send(buf)
79
80 }
81
82 function clientChange(params) {
83 // Update the client configuration with one from params
84 for (let i_group = 0; i_group < server.server.groups.length; i_group++) {
85 for (let i_client = 0; i_client < server.server.groups[i_group].clients.length; i_client++) {
86 if (server.server.groups[i_group].clients[i_client].id == params.id) {
4387 server.server.groups[i_group].clients[i_client].config = Object.assign(server.server.groups[i_group].clients[i_client].config, params);
44 //console.log(server.server.groups[i_group].clients[i_client]);
45 }
46 i_client++
47 }
48 i_group++
49 }
50 }
51
52 function clientConnect(params) {//console.log(params);
53 var i_group = 0
54
55 while (i_group < server.server.groups.length) {
56 var i_client = 0
57 while (i_client < server.server.groups[i_group].clients.length) {
88 }
89 }
90 }
91 }
92
93 function clientConnect(params) {
94 // Update all client info
95 for (let i_group = 0; i_group < server.server.groups.length; i_group++) {
96 for (let i_client = 0; i_client < server.server.groups[i_group].clients.length; i_client++) {
5897 if (server.server.groups[i_group].clients[i_client].id == params.client.id) {
5998 server.server.groups[i_group].clients[i_client] = params.client;
6099 // console.log(server.server.groups[i_group].clients[i_client]);
61100 }
62 i_client++
63 }
64 i_group++
65 }
66 }
67
68 function groupMute(params) {//console.log(params);
69 var i_group = 0
70
71 while (i_group < server.server.groups.length) {
101 }
102 }
103 }
104
105 function groupMute(params) {
106 // Set group mute boolean
107 for (let i_group = 0; i_group < server.server.groups.length; i_group++) {
72108 if (server.server.groups[i_group].id == params.id) {
73109 server.server.groups[i_group].muted = params.mute;
74 // console.log(server.server.groups[i_group]);
75 }
76 i_group++
77 }
78 }
79
80 function groupStream(params) {//console.log(params);
81 var i_group = 0
82
83 while (i_group < server.server.groups.length) {
110 }
111 }
112 }
113
114 function groupStream(params) {
115 // Set group stream id
116 for (let i_group = 0; i_group < server.server.groups.length; i_group++) {
84117 if (server.server.groups[i_group].id == params.id) {
85118 server.server.groups[i_group].stream_id = params.stream_id;
86 // console.log(server.server.groups[i_group]);
87 }
88 i_group++
89 }
90 }
91
92 function streamUpdate(params) {//console.log(params);
93 var i_stream = 0
94
95 while (i_stream < server.server.streams.length) {
119 }
120 }
121 }
122
123 function streamUpdate(params) {
124 // Update all stream inforamtion
125 for (let i_stream = 0; i_stream < server.server.streams.length;) {
96126 if (server.server.streams[i_stream].id == params.id) {
97127 server.server.streams[i_stream] = params.stream;
98128 // console.log(server.server.streams[i_stream]);
99129 }
100 i_stream++
130
101131 }
102132 }
103133
104134 function show() {
105 var i_group = 0;
135 // Render the page
106136 var content = "";
107137
108 while (i_group < server.server.groups.length) {
109 var i_client = 0;
138 for (let i_group = 0; i_group < server.server.groups.length; i_group++) {
139 // Set mute variables
140 var classgroup;
110141 var unmuted;
111 var streamselect = "<select id='stream_" + server.server.groups[i_group].id + "' onchange='setStream(\"" + server.server.groups[i_group].id + "\")' class='stream'>"
112
113 var i_stream = 0;
114 while (i_stream < server.server.streams.length) {
115 var streamselected = "";
116 if (server.server.groups[i_group].stream_id == server.server.streams[i_stream].id) { streamselected = 'selected' }
117 streamselect = streamselect + "<option value='" + server.server.streams[i_stream].id + "' " + streamselected + ">" + server.server.streams[i_stream].id + ": " + server.server.streams[i_stream].status + "</option>";
118 i_stream++
119 }
120 streamselect = streamselect + "</select>";
121 var classgroup = 'group';
122 if (server.server.groups[i_group].muted == true) { classgroup = 'groupmuted' }
123 content = content + "<div id='g_" + server.server.groups[i_group].id + "' class='" + classgroup + "'>";
124 content = content + streamselect;
125
126142 var mutetext;
127
128143 if (server.server.groups[i_group].muted == true) {
144 classgroup = 'groupmuted';
129145 unmuted = 'false';
130146 mutetext = '&#x1F507';
131 }
132 if (server.server.groups[i_group].muted == false) {
147 } else {
148 classgroup = 'group';
133149 unmuted = 'true';
134150 mutetext = '&#128266';
135151 }
136152
137 content = content + " <a href=\"javascript:setMuteGroup('" + server.server.groups[i_group].id + "','" + unmuted + "');\" class='mutebuttongroup'>" + mutetext + "</a>";
138 //content=content+": "+server.server.groups[i_group].muted;
139
140 content = content + "<input type='button' value='Refresh' class='refreshbutton' onclick='javascript: location.reload()'>";
141 content = content + "<br>";
142 while (i_client < server.server.groups[i_group].clients.length) {
153 // Start group div
154 content += "<div id='g_" + server.server.groups[i_group].id + "' class='" + classgroup + "'>";
155
156 // Create stream selection dropdown
157 var streamselect = "<select id='stream_" + server.server.groups[i_group].id + "' onchange='setStream(\"" + server.server.groups[i_group].id + "\")' class='stream'>"
158 for (let i_stream = 0; i_stream < server.server.streams.length; i_stream++) {
159 var streamselected = "";
160 if (server.server.groups[i_group].stream_id == server.server.streams[i_stream].id) {
161 streamselected = 'selected'
162 }
163 streamselect += "<option value='" + server.server.streams[i_stream].id + "' " + streamselected + ">" + server.server.streams[i_stream].id + ": " + server.server.streams[i_stream].status + "</option>";
164 }
165
166 streamselect += "</select>";
167 content += streamselect;
168
169 // Group mute and refresh button
170 content += " <a href=\"javascript:setMuteGroup('" + server.server.groups[i_group].id + "','" + unmuted + "');\" class='mutebuttongroup'>" + mutetext + "</a>";
171 content += "<input type='button' value='Refresh' class='refreshbutton' onclick='javascript: location.reload()'>";
172 content += "<br/>";
173
174 // Create clients in group
175 for (let i_client = 0; i_client < server.server.groups[i_group].clients.length; i_client++) {
143176 var sv = server.server.groups[i_group].clients[i_client];
144177
178 // Set name and connection state vars, start client div
179 var name;
180 var clas = 'client'
181 if (sv.config.name != "") {
182 name = sv.config.name;
183 } else {
184 name = sv.host.name;
185 }
186 if (sv.connected == false) {
187 clas = 'disconnected';
188 }
189 content = content + "<div id='c_" + sv.id + "' class='" + clas + "'>";
190
191 // Client mute status vars
192 var unmuted;
193 var mutetextclient;
194 var sliderclass;
195 if (sv.config.volume.muted == true) {
196 unmuted = 'false';
197 sliderclass = 'slidermute';
198 mutetext = '&#128263';
199 } else {
200 sliderclass = 'slider'
201 unmuted = 'true';
202 mutetext = '&#128266';
203 }
204
205 // Client group selection vars
145206 var groupselect = "<select id='group_" + sv.id + "' onchange='setGroup(\"" + sv.id + "\")'>";
146
147 var o_group = 0
148 while (o_group < server.server.groups.length) {
207 for (let o_group = 0; o_group < server.server.groups.length; o_group++) {
149208 var groupselected = "";
150 if (o_group == i_group) { groupselected = 'selected' }
151
209 if (o_group == i_group) {
210 groupselected = 'selected'
211 }
152212 groupselect = groupselect + "<option value='" + server.server.groups[o_group].id + "' " + groupselected + ">Group " + o_group + " (" + server.server.groups[o_group].clients.length + " Clients)</option>";
153 o_group++
154 }
213 }
214
155215 groupselect = groupselect + "<option value='new'>new</option>";
156216 groupselect = groupselect + "</select>"
157217
158 var name;
159 var unmuted;
160 if (sv.config.name != "") { name = sv.config.name; }
161 else { name = sv.host.name; }
162
163 var clas = 'client'
164 if (sv.connected == false) { clas = 'disconnected'; }
165
166 content = content + "<div id='c_" + sv.id + "' class='" + clas + "'>";
167
168 var mutetextclient;
169 if (sv.config.volume.muted == true) {
170 unmuted = 'false';
171 mutetext = '&#128263';
172 }
173 if (sv.config.volume.muted == false) {
174 unmuted = 'true';
175 mutetext = '&#128266';
176 }
218 // Populate client div
177219 content = content + " <a href=\"javascript:setVolume('" + sv.id + "','" + unmuted + "');\" class='mutebutton'>" + mutetext + "</a>";
178 // content=content+": "+sv.config.volume.muted;
179
180 var sliderclass = 'slider';
181 if (sv.config.volume.muted == true) { sliderclass = 'slidermute'; }
182
183220 content = content + "<div class='sliders'><div class='sliderdiv'><input type='range' min=0 max=100 step=1 id='vol_" + sv.id + "' onchange='javascript:setVolume(\"" + sv.id + "\",\"" + sv.config.volume.muted + "\")' value=" + sv.config.volume.percent + " class='" + sliderclass + "' orient='vertical'></div>";
184221 content = content + "<div class='finebg'>++<br>+<br>0<br>-<br>--</div><div class='sliderdiv_fine'><input type='range' min=0 max=10 step=1 id='vol_fine_" + sv.id + "' onchange='javascript:setVolume(\"" + sv.id + "\",\"" + sv.config.volume.muted + "\")' value=5 class='" + sliderclass + "_fine' orient='vertical'></div></div>";
185222 content = content + " <a href=\"javascript:setName('" + sv.id + "');\" class='edit'>&#9998</a>";
186223 content = content + name;
187 // content=content+" Connected:"+sv.connected;
188224 content = content + groupselect;
189225 content = content + "</div>";
190
191 i_client++
192 }
226 }
227
193228 content = content + "</div>"
194 i_group++
195 }
196
229 }
230
231 // Pad then update page
197232 content = content + "<br><br>";
198233 document.getElementById('show').innerHTML = content;
199234 }
202237 percent = document.getElementById('vol_' + id).value;
203238 percent_fine = document.getElementById('vol_fine_' + id).value;
204239
205 //alert(percent +" "+percent_fine+" "+Number(percent));
240 // Take away 5 as it's the default of the fine slider. Only relevant if it
241 // has changed
206242 percent = Number(percent) + Number(percent_fine) - 5;
207 if (percent < 0) { percent = 0 }
208 if (percent > 100) { percent = 100 }
209
243
244 if (percent < 0) {
245 percent = 0
246 }
247 else if (percent > 100) {
248 percent = 100
249 }
250
251 // Request changes
210252 send('{"id":8,"jsonrpc":"2.0","method":"Client.SetVolume","params":{"id":"' + id + '","volume":{"muted":' + mute + ',"percent":' + percent + '}}}')
211253
212 var i_group = 0
213
214 while (i_group < server.server.groups.length) {
215 var i_client = 0
216 while (i_client < server.server.groups[i_group].clients.length) {
254 // Make updates to server info and refresh content
255 for (let i_group = 0; i_group < server.server.groups.length; i_group++) {
256 for (let i_client = 0; i_client < server.server.groups[i_group].clients.length; i_client++) {
217257 var sv = server.server.groups[i_group].clients[i_client];
218
219258 if (sv.id == id) {
220 if (mute == 'true') { sv.config.volume.muted = true; }
221 if (mute == 'false') { sv.config.volume.muted = false; }
259 if (mute == 'true') {
260 sv.config.volume.muted = true;
261 }
262 if (mute == 'false') {
263 sv.config.volume.muted = false;
264 }
222265 sv.config.volume.percent = percent;
223 // console.log(server.server.groups[i_group]);
224 }
225
226 i_client++
227 }
228
229 i_group++
230 }
231
266 }
267 }
268 }
232269 show()
233270 }
234
235
236
237271
238272 function setMuteGroup(id, what) {
239273 send('{"id":"MuteGroup_' + id + '","jsonrpc":"2.0","method":"Group.SetMute","params":{"id":"' + id + '","mute":' + what + '}}')
240 var i_group = 0
241 while (i_group < server.server.groups.length) {
274
275 for (let i_group = 0; i_group < server.server.groups.length; i_group++) {
242276 if (server.server.groups[i_group].id == id) {
243 if (what == 'true') { server.server.groups[i_group].muted = true; }
244 if (what == 'false') { server.server.groups[i_group].muted = false; }
277 if (what == 'true') {
278 server.server.groups[i_group].muted = true;
279 }
280 if (what == 'false') {
281 server.server.groups[i_group].muted = false;
282 }
245283 // console.log(server.server.groups[i_group]);
246284 }
247 i_group++
248285 }
249286 show()
250287 }
251288
252289 function setStream(id) {
253
254290 send('{"id":4,"jsonrpc":"2.0","method":"Group.SetStream","params":{"id":"' + id + '","stream_id":"' + document.getElementById('stream_' + id).value + '"}}')
255291
256 var i_group = 0
257
258 while (i_group < server.server.groups.length) {
292 for (let i_group = 0; i_group < server.server.groups.length; i_group++) {
259293 if (server.server.groups[i_group].id == id) {
260294 server.server.groups[i_group].stream_id = document.getElementById('stream_' + id).value;
261295 // console.log(server.server.groups[i_group]);
262296 }
263 i_group++
264297 }
265298 show()
266299 }
267300
268301 function setGroup(id) {
269302 group = document.getElementById('group_' + id).value;
303
304 // Get client group id
270305 var current_group;
271 var i_group = 0
272 while (i_group < server.server.groups.length) {
273 var i_client = 0
274 while (i_client < server.server.groups[i_group].clients.length) {
275 if (id == server.server.groups[i_group].clients[i_client].id) { current_group = server.server.groups[i_group].id }
276 i_client++
277 }
278 i_group++
279 }
280
306 groups:
307 for (let i_group = 0; i_group < server.server.groups.length; i_group++) {
308 for (let i_client = 0; i_client < server.server.groups[i_group].clients.length; i_client++) {
309 if (id == server.server.groups[i_group].clients[i_client].id) {
310 current_group = server.server.groups[i_group].id;
311 break groups;
312 }
313 }
314 }
315
316 // Get
317 // List of target group's clients
318 // OR
319 // List of current group's other clients
281320 var send_clients = [];
282 var i_group = 0
283 while (i_group < server.server.groups.length) {
321 for (let i_group = 0; i_group < server.server.groups.length; i_group++) {
284322 if (server.server.groups[i_group].id == group || (group == "new" && server.server.groups[i_group].id == current_group)) {
285 var i_client = 0
286 while (i_client < server.server.groups[i_group].clients.length) {
323 for (let i_client = 0; i_client < server.server.groups[i_group].clients.length; i_client++) {
287324 if (group == "new" && server.server.groups[i_group].clients[i_client].id == id) { }
288 else {//console.log(group);
289 //console.log(server.server.groups[i_group].clients[i_client].id);
290 //console.log(id);
325 else {
291326 send_clients[send_clients.length] = server.server.groups[i_group].clients[i_client].id;
292327 }
293 i_client++
294 }
295 }
296 i_group++
297 }
298 if (group != "new") { send_clients[send_clients.length] = id; }
328 }
329 }
330 }
331
332 if (group == "new") {
333 group = current_group
334 }
335 else {
336 send_clients[send_clients.length] = id;
337 }
299338
300339 var send_clients_string = JSON.stringify(send_clients);
301 // console.log(send_clients_string);
302
303 var sendgroup = group
304 if (group == "new") { group = current_group }
305
306340 send('{"id":1,"jsonrpc":"2.0","method":"Group.SetClients","params":{"clients":' + send_clients_string + ',"id":"' + group + '"}}')
307 //send('{"id":1,"jsonrpc":"2.0","method":"Server.GetStatus"}}')
308341 }
309342
310343 function setName(id) {
344 // Get current name and lacency
311345 var current_name;
312 var current_latemcy;
313 var i_group = 0;
314
315 while (i_group < server.server.groups.length) {
316 var i_client = 0
317 while (i_client < server.server.groups[i_group].clients.length) {
346 var current_latency;
347 groups:
348 for (let i_group = 0; i_group < server.server.groups.length; i_group++) {
349 for (let i_client = 0; i_client < server.server.groups[i_group].clients.length; i_client++) {
318350 var sv = server.server.groups[i_group].clients[i_client];
319351 if (sv.id == id) {
320 if (sv.config.name != "") { current_name = sv.config.name; }
321 else { current_name = sv.host.name; }
352 if (sv.config.name != "") {
353 current_name = sv.config.name;
354 } else {
355 current_name = sv.host.name;
356 }
322357 current_latency = sv.config.latency;
323 }
324 i_client++
325 }
326 i_group++
358 break groups;
359 }
360 }
327361 }
328362
329363 var newName = window.prompt("New Name", current_name);
330364 var newLatency = window.prompt("New Latency", current_latency);
331365
332 send('{"id":6,"jsonrpc":"2.0","method":"Client.SetName","params":{"id":"' + id + '","name":"' + newName + '"}}')
333 send('{"id":7,"jsonrpc":"2.0","method":"Client.SetLatency","params":{"id":"' + id + '","latency":' + newLatency + '}}')
334
335 var i_group = 0;
336 while (i_group < server.server.groups.length) {
337 var i_client = 0
338 while (i_client < server.server.groups[i_group].clients.length) {
366 // Don't change anything if user cancel's
367 if (newName != null) {
368 send('{"id":6,"jsonrpc":"2.0","method":"Client.SetName","params":{"id":"' + id + '","name":"' + newName + '"}}')
369 } else {
370 newName = current_name
371 }
372 if (newLatency != null) {
373 send('{"id":7,"jsonrpc":"2.0","method":"Client.SetLatency","params":{"id":"' + id + '","latency":' + newLatency + '}}')
374 } else {
375 newLatency = current_latency
376 }
377
378 for (let i_group = 0; i_group < server.server.groups.length; i_group++) {
379 for (let i_client = 0; i_client < server.server.groups[i_group].clients.length; i_client++) {
339380 var sv = server.server.groups[i_group].clients[i_client];
340
341381 if (sv.id == id) {
342382 sv.config.name = newName;
343383 sv.config.latency = newLatency;
344384 }
345 i_client++
346 }
347 i_group++
385 }
348386 }
349387 show()
350388 }
389
351390 </script>
352
353391 <style>
354392 body {
355393 background: #1f1f1f;
454492 opacity: 0.27;
455493 }
456494
457
458495 .sliders {
459496 text-align: left;
460497 vertical-align: middle;
462499 padding-top: 10px;
463500 clear: both;
464501 float: none;
465
466502 }
467503
468504 .sliderdiv {
469505 display: inline-block;
470
471506 padding-left: 40px;
472507 text-align: left;
473508 width: 20px;
475510
476511 .sliderdiv_fine {
477512 display: inline-block;
478
479513 text-align: left;
480514 width: 20px;
481
482515 position: relative;
483516 top: 0px;
484517 left: 13px;
504537 border: 1px solid #555555;
505538 -moz-appearance: none;
506539 -webkit-appearance: none;
507 appearancce: none;
508
540 appearance: none;
509541 }
510542
511543 .stream {
516548
517549 .refreshbutton {
518550 background-color: rgb(61, 61, 61);
519
520551 font-size: 20px;
521552 color: #e3e3e3;
522553 border: 1px solid #555555;
1212
1313 # default values are commented
1414 # uncomment and edit to change them
15
16
17 # General server settings #####################################################
18 #
19 [server]
20 # Number of additional worker threads to use
21 # - For values < 0 the number of threads will be 2 (on single and dual cores)
22 # or 4 (for quad and more cores)
23 # - 0 will utilize just the processes main thread and might cause audio drops
24 # in case there are a couple of longer running tasks, such as encoding
25 # multiple audio streams
26 #threads = -1
27 #
28 ###############################################################################
1529
1630
1731 # HTTP RPC ####################################################################
3145 #port = 1780
3246
3347 # serve a website from the doc_root location
34 # doc_root =
48 #doc_root =
3549 #
3650 ###############################################################################
3751
6983 #port = 1704
7084
7185 # stream URI of the PCM input stream, can be configured multiple times
72 # Format: TYPE://host/path?name=NAME[&codec=CODEC][&sampleformat=SAMPLEFORMAT]
86 # The following notation is used in this paragraph:
87 # <angle brackets>: the whole expression must be replaced with your specific setting
88 # [square brackets]: the whole expression is optional and can be left out
89 # [key=value]: if you leave this option out, "value" will be the default for "key"
90 #
91 # Format: TYPE://host/path?name=<name>[&codec=<codec>][&sampleformat=<sampleformat>][&chunk_ms=<chunk ms>]
92 # parameters have the form "key=value", they are concatenated with an "&" character
93 # parameter "name" is mandatory for all streams, while codec, sampleformat and chunk_ms are optional
94 # and will override the default codec, sampleformat or chunk_ms settings
95 # Available types are:
96 # pipe: pipe:///<path/to/pipe>?name=<name>
97 # librespot: librespot:///<path/to/librespot>?name=<name>[&username=<my username>&password=<my password>][&devicename=Snapcast][&bitrate=320][&wd_timeout=7800][&volume=100][&onevent=""][&nomalize=false]
98 # note that you need to have the librespot binary on your machine
99 # sampleformat will be set to "44100:16:2"
100 # file: file:///<path/to/PCM/file>?name=<name>
101 # process: process:///<path/to/process>?name=<name>[&wd_timeout=0][&log_stderr=false]
102 # airplay: airplay:///<path/to/airplay>?name=<name>[&port=5000]
103 # note that you need to have the airplay binary on your machine
104 # sampleformat will be set to "44100:16:2"
105 # tcp server: tcp://<listen IP, e.g. 127.0.0.1>:<port>?name=<name>[&mode=server]
106 # tcp client: tcp://<server IP, e.g. 127.0.0.1>:<port>?name=<name>&mode=client
73107 stream = pipe:///tmp/snapfifo?name=default
108 #stream = tcp://127.0.0.1?name=mopidy_tcp
74109
75110 # Default sample format
76111 #sampleformat = 48000:16:2
77112
78113 # Default transport codec
79 # (flac|ogg|pcm)[:options]
114 # (flac|ogg|opus|pcm)[:options]
80115 # Type codec:? to get codec specific options
81116 #codec = flac
82117
83 # Default stream read buffer [ms]
84 #stream_buffer = 20
118 # Default stream read chunk size [ms]
119 #chunk_ms = 20
85120
86121 # Buffer [ms]
87122 #buffer = 1000
0 <?xml version="1.0" encoding="UTF-8"?>
1 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2 <plist version="1.0">
3 <dict>
4 <key>Label</key>
5 <string>de.badaix.snapcast.snapserver</string>
6 <key>ProgramArguments</key>
7 <array>
8 <string>/usr/local/bin/snapserver</string>
9 <!-- <string>-d</string> -->
10 </array>
11 <key>RunAtLoad</key>
12 <true/>
13 <key>KeepAlive</key>
14 <true/>
15 <key>ProcessType</key>
16 <string>Interactive</string>
17 </dict>
18 </plist>
22 _( )/ ___) / \ ( ( \( _ \( _ \ / __)( ) ( )
33 / \) \\___ \( O )/ / ) / ) __/( (__(_ _)(_ _)
44 \____/(____/ \__/ \_)__)(__\_)(__) \___)(_) (_)
5 version 1.2.2
5 version 1.3.1
66 https://github.com/badaix/jsonrpcpp
77
88 This file is part of jsonrpc++
1818 /// run-clang-tidy-3.8.py -header-filter='jsonrpcpp.hpp'
1919 /// -checks='*,-misc-definitions-in-headers,-google-readability-braces-around-statements,-readability-braces-around-statements,-cppcoreguidelines-pro-bounds-pointer-arithmetic,-google-build-using-namespace,-google-build-using-namespace,-modernize-pass-by-value,-google-explicit-constructor'
2020
21 #ifndef JSON_RPC_H
22 #define JSON_RPC_H
21 #ifndef JSON_RPC_HPP
22 #define JSON_RPC_HPP
2323
2424 #include "json.hpp"
2525 #include <cstring>
3232
3333 namespace jsonrpcpp
3434 {
35
3635
3736 class Entity;
3837 class Request;
4241 class Error;
4342 class Batch;
4443
45
46 typedef std::shared_ptr<Entity> entity_ptr;
47 typedef std::shared_ptr<Request> request_ptr;
48 typedef std::shared_ptr<Notification> notification_ptr;
49 typedef std::shared_ptr<Parameter> parameter_ptr;
50 typedef std::shared_ptr<Response> response_ptr;
51 typedef std::shared_ptr<Error> error_ptr;
52 typedef std::shared_ptr<Batch> batch_ptr;
53
44 using entity_ptr = std::shared_ptr<Entity>;
45 using request_ptr = std::shared_ptr<Request>;
46 using notification_ptr = std::shared_ptr<Notification>;
47 using parameter_ptr = std::shared_ptr<Parameter>;
48 using response_ptr = std::shared_ptr<Response>;
49 using error_ptr = std::shared_ptr<Error>;
50 using batch_ptr = std::shared_ptr<Batch>;
5451
5552
5653 class Entity
7370 Entity(const Entity&) = default;
7471 Entity& operator=(const Entity&) = default;
7572
76 bool is_exception();
77 bool is_id();
78 bool is_error();
79 bool is_response();
80 bool is_request();
81 bool is_notification();
82 bool is_batch();
73 bool is_exception() const;
74 bool is_id() const;
75 bool is_error() const;
76 bool is_response() const;
77 bool is_request() const;
78 bool is_notification() const;
79 bool is_batch() const;
8380
8481 virtual std::string type_str() const;
8582
9289 protected:
9390 entity_t entity;
9491 };
95
9692
9793
9894 class NullableEntity : public Entity
114110 };
115111
116112
117
118113 class Id : public Entity
119114 {
120115 public:
140135 return out;
141136 }
142137
143 value_t type() const
138 const value_t& type() const
144139 {
145140 return type_;
146141 }
150145 return int_id_;
151146 }
152147
153 std::string string_id() const
148 const std::string& string_id() const
154149 {
155150 return string_id_;
156151 }
162157 };
163158
164159
165
166160 class Parameter : public NullableEntity
167161 {
168162 public:
224218 };
225219
226220
227
228221 class Error : public NullableEntity
229222 {
230223 public:
240233 return code_;
241234 }
242235
243 std::string message() const
236 const std::string& message() const
244237 {
245238 return message_;
246239 }
247240
248 Json data() const
241 const Json& data() const
249242 {
250243 return data_;
251244 }
255248 std::string message_;
256249 Json data_;
257250 };
258
259251
260252
261253 /// JSON-RPC 2.0 request
272264 Json to_json() const override;
273265 void parse_json(const Json& json) override;
274266
275 std::string method() const
267 const std::string& method() const
276268 {
277269 return method_;
278270 }
279271
280 Parameter params() const
272 const Parameter& params() const
281273 {
282274 return params_;
283275 }
284276
285 Id id() const
277 const Id& id() const
286278 {
287279 return id_;
288280 }
294286 };
295287
296288
297
298289 class RpcException : public std::exception
299290 {
300291 public:
306297 protected:
307298 std::runtime_error m_;
308299 };
309
310300
311301
312302 class RpcEntityException : public RpcException, public Entity
316306 RpcEntityException(const std::string& text);
317307 Json to_json() const override = 0;
318308
319 Error error() const
309 const Error& error() const
320310 {
321311 return error_;
322312 }
327317 };
328318
329319
330
331320 class ParseErrorException : public RpcEntityException
332321 {
333322 public:
335324 ParseErrorException(const std::string& data);
336325 Json to_json() const override;
337326 };
338
339327
340328
341329 // -32600 Invalid Request The JSON sent is not a valid Request object.
347335 {
348336 public:
349337 RequestException(const Error& error, const Id& requestId = Id());
350 RequestException(const RequestException& e) = default;
351338 Json to_json() const override;
352339
353 Id id() const
340 const Id& id() const
354341 {
355342 return id_;
356343 }
358345 protected:
359346 Id id_;
360347 };
361
362348
363349
364350 class InvalidRequestException : public RequestException
371357 };
372358
373359
374
375360 class MethodNotFoundException : public RequestException
376361 {
377362 public:
382367 };
383368
384369
385
386370 class InvalidParamsException : public RequestException
387371 {
388372 public:
393377 };
394378
395379
396
397380 class InternalErrorException : public RequestException
398381 {
399382 public:
402385 InternalErrorException(const char* data, const Id& requestId = Id());
403386 InternalErrorException(const std::string& data, const Id& requestId = Id());
404387 };
405
406388
407389
408390 class Response : public Entity
418400 Json to_json() const override;
419401 void parse_json(const Json& json) override;
420402
421 Id id() const
403 const Id& id() const
422404 {
423405 return id_;
424406 }
425407
426 Json result() const
408 const Json& result() const
427409 {
428410 return result_;
429411 }
430412
431 Error error() const
413 const Error& error() const
432414 {
433415 return error_;
434416 }
440422 };
441423
442424
443
444425 class Notification : public Entity
445426 {
446427 public:
451432 Json to_json() const override;
452433 void parse_json(const Json& json) override;
453434
454 std::string method() const
435 const std::string& method() const
455436 {
456437 return method_;
457438 }
458439
459 Parameter params() const
440 const Parameter& params() const
460441 {
461442 return params_;
462443 }
467448 };
468449
469450
470
471451 typedef std::function<void(const Parameter& params)> notification_callback;
472452 typedef std::function<jsonrpcpp::response_ptr(const Id& id, const Parameter& params)> request_callback;
473
474453
475454 class Parser
476455 {
501480 };
502481
503482
504
505483 class Batch : public Entity
506484 {
507485 public:
532510 {
533511 }
534512
535
536 inline bool Entity::is_exception()
513 inline bool Entity::is_exception() const
537514 {
538515 return (entity == entity_t::exception);
539516 }
540517
541
542 inline bool Entity::is_id()
518 inline bool Entity::is_id() const
543519 {
544520 return (entity == entity_t::id);
545521 }
546522
547
548 inline bool Entity::is_error()
523 inline bool Entity::is_error() const
549524 {
550525 return (entity == entity_t::error);
551526 }
552527
553
554 inline bool Entity::is_response()
528 inline bool Entity::is_response() const
555529 {
556530 return (entity == entity_t::response);
557531 }
558532
559
560 inline bool Entity::is_request()
533 inline bool Entity::is_request() const
561534 {
562535 return (entity == entity_t::request);
563536 }
564537
565
566 inline bool Entity::is_notification()
538 inline bool Entity::is_notification() const
567539 {
568540 return (entity == entity_t::notification);
569541 }
570542
571
572 inline bool Entity::is_batch()
543 inline bool Entity::is_batch() const
573544 {
574545 return (entity == entity_t::batch);
575546 }
576
577547
578548 inline void Entity::parse(const char* json_str)
579549 {
599569 }
600570 }
601571
602
603572 inline void Entity::parse(const std::string& json_str)
604573 {
605574 parse(json_str.c_str());
606575 }
607
608576
609577 inline std::string Entity::type_str() const
610578 {
632600 }
633601
634602
635
636603 /////////////////////////// NullableEntity implementation /////////////////////
637604
638605 inline NullableEntity::NullableEntity(entity_t type) : Entity(type), isNull(false)
639606 {
640607 }
641608
642
643609 inline NullableEntity::NullableEntity(entity_t type, std::nullptr_t) : Entity(type), isNull(true)
644610 {
645611 }
646612
647613
648
649614 /////////////////////////// Id implementation /////////////////////////////////
650615
651616 inline Id::Id() : Entity(entity_t::id), type_(value_t::null), int_id_(0), string_id_("")
652617 {
653618 }
654619
655
656620 inline Id::Id(int id) : Entity(entity_t::id), type_(value_t::integer), int_id_(id), string_id_("")
657621 {
658622 }
659623
660
661624 inline Id::Id(const char* id) : Entity(entity_t::id), type_(value_t::string), int_id_(0), string_id_(id)
662625 {
663626 }
664627
665
666628 inline Id::Id(const std::string& id) : Id(id.c_str())
667629 {
668630 }
669631
670
671632 inline Id::Id(const Json& json_id) : Entity(entity_t::id), type_(value_t::null)
672633 {
673634 Id::parse_json(json_id);
674635 }
675
676636
677637 inline void Id::parse_json(const Json& json)
678638 {
693653 else
694654 throw std::invalid_argument("id must be integer, string or null");
695655 }
696
697656
698657 inline Json Id::to_json() const
699658 {
708667 }
709668
710669
711
712670 //////////////////////// Error implementation /////////////////////////////////
713671
714672 inline Parameter::Parameter(std::nullptr_t) : NullableEntity(entity_t::id, nullptr), type(value_t::null)
715673 {
716674 }
717
718675
719676 inline Parameter::Parameter(const Json& json) : NullableEntity(entity_t::id), type(value_t::null)
720677 {
721678 if (json != nullptr)
722679 Parameter::parse_json(json);
723680 }
724
725681
726682 inline Parameter::Parameter(const std::string& key1, const Json& value1, const std::string& key2, const Json& value2, const std::string& key3,
727683 const Json& value3, const std::string& key4, const Json& value4)
736692 param_map[key4] = value4;
737693 }
738694
739
740695 inline void Parameter::parse_json(const Json& json)
741696 {
742697 if (json.is_array())
752707 type = value_t::map;
753708 }
754709 }
755
756710
757711 inline Json Parameter::to_json() const
758712 {
764718 return nullptr;
765719 }
766720
767
768721 inline bool Parameter::is_array() const
769722 {
770723 return type == value_t::array;
771724 }
772725
773
774726 inline bool Parameter::is_map() const
775727 {
776728 return type == value_t::map;
777729 }
778730
779
780731 inline bool Parameter::is_null() const
781732 {
782733 return isNull;
783734 }
784
785735
786736 inline bool Parameter::has(const std::string& key) const
787737 {
790740 return (param_map.find(key) != param_map.end());
791741 }
792742
793
794743 inline Json Parameter::get(const std::string& key) const
795744 {
796745 return param_map.at(key);
797746 }
798
799747
800748 inline bool Parameter::has(size_t idx) const
801749 {
804752 return (param_array.size() > idx);
805753 }
806754
807
808755 inline Json Parameter::get(size_t idx) const
809756 {
810757 return param_array.at(idx);
811758 }
812
813759
814760
815761 //////////////////////// Error implementation /////////////////////////////////
820766 Error::parse_json(json);
821767 }
822768
823
824769 inline Error::Error(std::nullptr_t) : NullableEntity(entity_t::error, nullptr), code_(0), message_(""), data_(nullptr)
825770 {
826771 }
827772
828
829773 inline Error::Error(const std::string& message, int code, const Json& data) : NullableEntity(entity_t::error), code_(code), message_(message), data_(data)
830774 {
831775 }
832
833776
834777 inline void Error::parse_json(const Json& json)
835778 {
856799 }
857800 }
858801
859
860802 inline Json Error::to_json() const
861803 {
862804 Json j = {
869811 }
870812
871813
872
873814 ////////////////////// Request implementation /////////////////////////////////
874815
875816 inline Request::Request(const Json& json) : Entity(entity_t::request), method_(""), id_()
878819 Request::parse_json(json);
879820 }
880821
881
882822 inline Request::Request(const Id& id, const std::string& method, const Parameter& params) : Entity(entity_t::request), method_(method), params_(params), id_(id)
883823 {
884824 }
885
886825
887826 inline void Request::parse_json(const Json& json)
888827 {
929868 }
930869 }
931870
932
933871 inline Json Request::to_json() const
934872 {
935873 Json json = {{"jsonrpc", "2.0"}, {"method", method_}, {"id", id_.to_json()}};
941879 }
942880
943881
944
945882 inline RpcException::RpcException(const char* text) : m_(text)
946883 {
947884 }
956893 }
957894
958895
959
960896 inline RpcEntityException::RpcEntityException(const Error& error) : RpcException(error.message()), Entity(entity_t::exception), error_(error)
961897 {
962898 }
966902 }
967903
968904
969
970905 inline ParseErrorException::ParseErrorException(const Error& error) : RpcEntityException(error)
971906 {
972907 }
983918 }
984919
985920
986
987921 inline RequestException::RequestException(const Error& error, const Id& requestId) : RpcEntityException(error), id_(requestId)
988922 {
989923 }
994928
995929 return response;
996930 }
997
998931
999932
1000933 inline InvalidRequestException::InvalidRequestException(const Id& requestId) : RequestException(Error("Invalid request", -32600), requestId)
1015948 }
1016949
1017950
1018
1019951 inline MethodNotFoundException::MethodNotFoundException(const Id& requestId) : RequestException(Error("Method not found", -32601), requestId)
1020952 {
1021953 }
1034966 }
1035967
1036968
1037
1038969 inline InvalidParamsException::InvalidParamsException(const Id& requestId) : RequestException(Error("Invalid params", -32602), requestId)
1039970 {
1040971 }
1053984 }
1054985
1055986
1056
1057987 inline InternalErrorException::InternalErrorException(const Id& requestId) : RequestException(Error("Internal error", -32603), requestId)
1058988 {
1059989 }
10721002 }
10731003
10741004
1075
10761005 ///////////////////// Response implementation /////////////////////////////////
10771006
10781007 inline Response::Response(const Json& json) : Entity(entity_t::response)
10811010 Response::parse_json(json);
10821011 }
10831012
1084
10851013 inline Response::Response(const Id& id, const Json& result) : Entity(entity_t::response), id_(id), result_(result), error_(nullptr)
10861014 {
10871015 }
10881016
1089
10901017 inline Response::Response(const Id& id, const Error& error) : Entity(entity_t::response), id_(id), result_(), error_(error)
10911018 {
10921019 }
10931020
1094
10951021 inline Response::Response(const Request& request, const Json& result) : Response(request.id(), result)
10961022 {
10971023 }
10981024
1099
11001025 inline Response::Response(const Request& request, const Error& error) : Response(request.id(), error)
11011026 {
11021027 }
11031028
1104
11051029 inline Response::Response(const RequestException& exception) : Response(exception.id(), exception.error())
11061030 {
11071031 }
1108
11091032
11101033 inline void Response::parse_json(const Json& json)
11111034 {
11241047 if (json.count("result") != 0u)
11251048 result_ = json["result"];
11261049 else if (json.count("error") != 0u)
1127 error_.parse_json(json["error"]);
1050 error_ = json["error"];
11281051 else
11291052 throw RpcException("response must contain result or error");
11301053 }
11371060 throw RpcException(e.what());
11381061 }
11391062 }
1140
11411063
11421064 inline Json Response::to_json() const
11431065 {
11541076 }
11551077
11561078
1157
11581079 ///////////////// Notification implementation /////////////////////////////////
11591080
11601081 inline Notification::Notification(const Json& json) : Entity(entity_t::notification)
11631084 Notification::parse_json(json);
11641085 }
11651086
1166
11671087 inline Notification::Notification(const char* method, const Parameter& params) : Entity(entity_t::notification), method_(method), params_(params)
11681088 {
11691089 }
11701090
1171
11721091 inline Notification::Notification(const std::string& method, const Parameter& params) : Notification(method.c_str(), params)
11731092 {
11741093 }
1175
11761094
11771095 inline void Notification::parse_json(const Json& json)
11781096 {
12071125 }
12081126 }
12091127
1210
12111128 inline Json Notification::to_json() const
12121129 {
12131130 Json json = {
12211138 }
12221139
12231140
1224
12251141 //////////////////////// Batch implementation /////////////////////////////////
12261142
12271143 inline Batch::Batch(const Json& json) : Entity(entity_t::batch)
12291145 if (json != nullptr)
12301146 Batch::parse_json(json);
12311147 }
1232
12331148
12341149 inline void Batch::parse_json(const Json& json)
12351150 {
12591174 throw InvalidRequestException();
12601175 }
12611176
1262
12631177 inline Json Batch::to_json() const
12641178 {
12651179 Json result;
12691183 }
12701184
12711185
1272 /*void Batch::add(const entity_ptr entity)
1273 {
1274 entities.push_back(entity);
1275 }
1276 */
1277
1278
1279
12801186 //////////////////////// Parser implementation ////////////////////////////////
12811187
12821188 inline void Parser::register_notification_callback(const std::string& notification, notification_callback callback)
12851191 notification_callbacks_[notification] = callback;
12861192 }
12871193
1288
12891194 inline void Parser::register_request_callback(const std::string& request, request_callback callback)
12901195 {
12911196 if (callback)
12921197 request_callbacks_[request] = callback;
12931198 }
1294
12951199
12961200 inline entity_ptr Parser::parse(const std::string& json_str)
12971201 {
13241228 return entity;
13251229 }
13261230
1327
13281231 inline entity_ptr Parser::parse_json(const Json& json)
13291232 {
13301233 return do_parse_json(json);
13311234 }
13321235
1333
13341236 inline entity_ptr Parser::do_parse(const std::string& json_str)
13351237 {
13361238 try
13481250
13491251 return nullptr;
13501252 }
1351
13521253
13531254 inline entity_ptr Parser::do_parse_json(const Json& json)
13541255 {
13751276 return nullptr;
13761277 }
13771278
1378
13791279 inline bool Parser::is_request(const std::string& json_str)
13801280 {
13811281 try
13881288 }
13891289 }
13901290
1391
13921291 inline bool Parser::is_request(const Json& json)
13931292 {
13941293 return ((json.count("method") != 0u) && (json.count("id") != 0u));
13951294 }
13961295
1397
13981296 inline bool Parser::is_notification(const std::string& json_str)
13991297 {
14001298 try
14071305 }
14081306 }
14091307
1410
14111308 inline bool Parser::is_notification(const Json& json)
14121309 {
14131310 return ((json.count("method") != 0u) && (json.count("id") == 0));
14141311 }
14151312
1416
14171313 inline bool Parser::is_response(const std::string& json_str)
14181314 {
14191315 try
14261322 }
14271323 }
14281324
1429
14301325 inline bool Parser::is_response(const Json& json)
14311326 {
1432 return ((json.count("result") != 0u) && (json.count("id") != 0u));
1433 }
1434
1327 return (((json.count("result") != 0u) || (json.count("error") != 0u)) && (json.count("id") != 0u));
1328 }
14351329
14361330 inline bool Parser::is_batch(const std::string& json_str)
14371331 {
14451339 }
14461340 }
14471341
1448
14491342 inline bool Parser::is_batch(const Json& json)
14501343 {
14511344 return (json.is_array());
14521345 }
14531346
1454
14551347 } // namespace jsonrpcpp
14561348
1457
1458
14591349 #endif
2626 static AvahiSimplePoll* simple_poll;
2727 static char* name;
2828
29 PublishAvahi::PublishAvahi(const std::string& serviceName) : PublishmDNS(serviceName), client_(nullptr), active_(false)
29 PublishAvahi::PublishAvahi(const std::string& serviceName, boost::asio::io_context& ioc) : PublishmDNS(serviceName, ioc), client_(nullptr), timer_(ioc)
3030 {
3131 group = nullptr;
3232 simple_poll = nullptr;
5555 LOG(ERROR) << "Failed to create client: " << avahi_strerror(error) << "\n";
5656 }
5757
58 active_ = true;
59 pollThread_ = std::thread(&PublishAvahi::worker, this);
60 }
61
62
63 void PublishAvahi::worker()
64 {
65 while (active_ && (avahi_simple_poll_iterate(simple_poll, 100) == 0))
66 ;
58 poll();
59 }
60
61
62 void PublishAvahi::poll()
63 {
64 timer_.expires_after(std::chrono::milliseconds(50));
65 timer_.async_wait([this](const boost::system::error_code& ec) {
66 if (!ec && (avahi_simple_poll_iterate(simple_poll, 0) == 0))
67 poll();
68 });
6769 }
6870
6971
7072 PublishAvahi::~PublishAvahi()
7173 {
72 active_ = false;
73 pollThread_.join();
74 timer_.cancel();
7475
7576 if (client_)
7677 avahi_client_free(client_);
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
2929 #include <avahi-common/simple-watch.h>
3030 #include <avahi-common/timeval.h>
3131 #include <string>
32 #include <thread>
3332 #include <vector>
3433
3534 class PublishAvahi;
3938 class PublishAvahi : public PublishmDNS
4039 {
4140 public:
42 PublishAvahi(const std::string& serviceName);
41 PublishAvahi(const std::string& serviceName, boost::asio::io_context& ioc);
4342 ~PublishAvahi() override;
4443 void publish(const std::vector<mDNSService>& services) override;
4544
4746 static void entry_group_callback(AvahiEntryGroup* g, AvahiEntryGroupState state, AVAHI_GCC_UNUSED void* userdata);
4847 static void client_callback(AvahiClient* c, AvahiClientState state, AVAHI_GCC_UNUSED void* userdata);
4948 void create_services(AvahiClient* c);
50 void worker();
49 void poll();
5150 AvahiClient* client_;
52 std::thread pollThread_;
53 std::atomic<bool> active_;
5451 std::vector<mDNSService> services_;
52 boost::asio::steady_timer timer_;
5553 };
5654
5755
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
2727 } Opaque16;
2828
2929
30 PublishBonjour::PublishBonjour(const std::string& serviceName) : PublishmDNS(serviceName), active_(false)
30 PublishBonjour::PublishBonjour(const std::string& serviceName, boost::asio::io_context& ioc) : PublishmDNS(serviceName, ioc), active_(false)
3131 {
3232 /// dns-sd -R Snapcast _snapcast._tcp local 1704
3333 /// dns-sd -R Snapcast _snapcast-jsonrpc._tcp local 1705
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
2929 class PublishBonjour : public PublishmDNS
3030 {
3131 public:
32 PublishBonjour(const std::string& serviceName);
32 PublishBonjour(const std::string& serviceName, boost::asio::io_context& ioc);
3333 virtual ~PublishBonjour();
3434 virtual void publish(const std::vector<mDNSService>& services);
3535
00 #ifndef PUBLISH_MDNS_H
11 #define PUBLISH_MDNS_H
22
3 #include <boost/asio.hpp>
34 #include <string>
45 #include <vector>
56
1819 class PublishmDNS
1920 {
2021 public:
21 PublishmDNS(const std::string& serviceName) : serviceName_(serviceName)
22 PublishmDNS(const std::string& serviceName, boost::asio::io_context& ioc) : serviceName_(serviceName), ioc_(ioc)
2223 {
2324 }
2425
2829
2930 protected:
3031 std::string serviceName_;
32 boost::asio::io_context& ioc_;
3133 };
3234
3335 #if defined(HAS_AVAHI)
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
4545 std::string codec{"flac"};
4646 int32_t bufferMs{1000};
4747 std::string sampleFormat{"48000:16:2"};
48 size_t streamReadMs{20};
48 size_t streamChunkMs{20};
4949 bool sendAudioToMutedClients{false};
5050 std::vector<std::string> bind_to_address{{"0.0.0.0"}};
5151 };
0 .TH SNAPSERVER 1 "October 2019"
0 .TH SNAPSERVER 1 "January 2020"
11 .SH NAME
22 snapserver - Snapcast server
33 .SH SYNOPSIS
4343 \fI~/.config/snapcast/server.json\fR or (if $HOME is not set) \fI/var/lib/snapcast/server.json\fR
4444 persistent server data file
4545 .SH "COPYRIGHT"
46 Copyright (C) 2014-2019 Johannes Pohl (snapcast@badaix.de).
46 Copyright (C) 2014-2020 Johannes Pohl (snapcast@badaix.de).
4747 License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
4848 This is free software: you are free to change and redistribute it.
4949 There is NO WARRANTY, to the extent permitted by law.
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
2424 #include "common/daemon.hpp"
2525 #endif
2626 #include "common/sample_format.hpp"
27 #include "common/signal_handler.hpp"
2827 #include "common/snap_exception.hpp"
2928 #include "common/time_defs.hpp"
3029 #include "common/utils/string_utils.hpp"
3635 #include "publishZeroConf/publish_mdns.hpp"
3736 #endif
3837 #include "common/aixlog.hpp"
39 #include "config.h"
40
41
42 volatile sig_atomic_t g_terminated = false;
43 std::condition_variable terminateSignaled;
38 #include "config.hpp"
39
4440
4541 using namespace std;
4642 using namespace popl;
8278 auto streamValue = conf.add<Value<string>>(
8379 "s", "stream.stream", "URI of the PCM input stream.\nFormat: TYPE://host/path?name=NAME\n[&codec=CODEC]\n[&sampleformat=SAMPLEFORMAT]", pcmStream,
8480 &pcmStream);
81 int num_threads = -1;
82 conf.add<Value<int>>("", "server.threads", "number of server threads", num_threads, &num_threads);
8583
8684 conf.add<Value<string>>("", "stream.sampleformat", "Default sample format", settings.stream.sampleFormat, &settings.stream.sampleFormat);
87 conf.add<Value<string>>("c", "stream.codec", "Default transport codec\n(flac|ogg|pcm)[:options]\nType codec:? to get codec specific options",
85 conf.add<Value<string>>("c", "stream.codec", "Default transport codec\n(flac|ogg|opus|pcm)[:options]\nType codec:? to get codec specific options",
8886 settings.stream.codec, &settings.stream.codec);
89 conf.add<Value<size_t>>("", "stream.stream_buffer", "Default stream read buffer [ms]", settings.stream.streamReadMs, &settings.stream.streamReadMs);
87 // deprecated: stream_buffer, use chunk_ms instead
88 conf.add<Value<size_t>>("", "stream.stream_buffer", "Default stream read chunk size [ms]", settings.stream.streamChunkMs,
89 &settings.stream.streamChunkMs);
90 conf.add<Value<size_t>>("", "stream.chunk_ms", "Default stream read chunk size [ms]", settings.stream.streamChunkMs, &settings.stream.streamChunkMs);
9091 conf.add<Value<int>>("b", "stream.buffer", "Buffer [ms]", settings.stream.bufferMs, &settings.stream.bufferMs);
9192 conf.add<Value<bool>>("", "stream.send_to_muted", "Send audio to muted clients", settings.stream.sendAudioToMutedClients,
9293 &settings.stream.sendAudioToMutedClients);
141142 if (versionSwitch->is_set())
142143 {
143144 cout << "snapserver v" << VERSION << "\n"
144 << "Copyright (C) 2014-2019 BadAix (snapcast@badaix.de).\n"
145 << "Copyright (C) 2014-2020 BadAix (snapcast@badaix.de).\n"
145146 << "License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.\n"
146147 << "This is free software: you are free to change and redistribute it.\n"
147148 << "There is NO WARRANTY, to the extent permitted by law.\n\n"
164165
165166 if (settings.stream.codec.find(":?") != string::npos)
166167 {
167 EncoderFactory encoderFactory;
168 std::unique_ptr<Encoder> encoder(encoderFactory.createEncoder(settings.stream.codec));
168 encoder::EncoderFactory encoderFactory;
169 std::unique_ptr<encoder::Encoder> encoder(encoderFactory.createEncoder(settings.stream.codec));
169170 if (encoder)
170171 {
171172 cout << "Options for codec \"" << encoder->name() << "\":\n"
185186 }
186187 else
187188 {
188 AixLog::Log::instance().add_logsink<AixLog::SinkCout>(AixLog::Severity::info, AixLog::Type::all, "%Y-%m-%d %H-%M-%S [#severity]");
189 }
189 AixLog::Log::instance().add_logsink<AixLog::SinkCout>(AixLog::Severity::info, AixLog::Type::all, "%Y-%m-%d %H-%M-%S [#severity] (#tag_func)");
190 }
191
192 for (const auto& opt : conf.unknown_options())
193 LOG(WARNING) << "unknown configuration option: " << opt << "\n";
190194
191195 if (!streamValue->is_set())
192196 settings.stream.pcmStreams.push_back(streamValue->value());
196200 LOG(INFO) << "Adding stream: " << streamValue->value(n) << "\n";
197201 settings.stream.pcmStreams.push_back(streamValue->value(n));
198202 }
199
200 signal(SIGHUP, signal_handler);
201 signal(SIGTERM, signal_handler);
202 signal(SIGINT, signal_handler);
203203
204204 #ifdef HAS_DAEMON
205205 std::unique_ptr<Daemon> daemon;
236236 Config::instance().init();
237237 #endif
238238
239
239 boost::asio::io_context io_context;
240240 #if defined(HAS_AVAHI) || defined(HAS_BONJOUR)
241 PublishZeroConf publishZeroConfg("Snapcast");
241 auto publishZeroConfg = std::make_unique<PublishZeroConf>("Snapcast", io_context);
242242 vector<mDNSService> dns_services;
243243 dns_services.emplace_back("_snapcast._tcp", settings.stream.port);
244244 dns_services.emplace_back("_snapcast-stream._tcp", settings.stream.port);
251251 {
252252 dns_services.emplace_back("_snapcast-http._tcp", settings.http.port);
253253 }
254 publishZeroConfg.publish(dns_services);
255 #endif
254 publishZeroConfg->publish(dns_services);
255 #endif
256 if (settings.stream.streamChunkMs < 10)
257 {
258 LOG(WARNING) << "Stream read chunk size is less than 10ms, changing to 10ms\n";
259 settings.stream.streamChunkMs = 10;
260 }
256261
257262 if (settings.stream.bufferMs < 400)
258263 {
260265 settings.stream.bufferMs = 400;
261266 }
262267
263 boost::asio::io_context io_context;
264 std::unique_ptr<StreamServer> streamServer(new StreamServer(&io_context, settings));
268 auto streamServer = std::make_unique<StreamServer>(io_context, settings);
265269 streamServer->start();
266270
267 auto func = [](boost::asio::io_context* ioservice) -> void { ioservice->run(); };
268 std::thread t(func, &io_context);
269
270 while (!g_terminated)
271 chronos::sleep(100);
272
273 io_context.stop();
274 t.join();
271 if (num_threads < 0)
272 num_threads = std::max(2, std::min(4, static_cast<int>(std::thread::hardware_concurrency())));
273 LOG(INFO) << "number of threads: " << num_threads << ", hw threads: " << std::thread::hardware_concurrency() << "\n";
274
275 // Construct a signal set registered for process termination.
276 boost::asio::signal_set signals(io_context, SIGHUP, SIGINT, SIGTERM);
277 signals.async_wait([&io_context](const boost::system::error_code& ec, int signal) {
278 if (!ec)
279 SLOG(INFO) << "Received signal " << signal << ": " << strsignal(signal) << "\n";
280 else
281 SLOG(INFO) << "Failed to wait for signal: " << ec << "\n";
282 io_context.stop();
283 });
284
285 std::vector<std::thread> threads;
286 for (int n = 0; n < num_threads; ++n)
287 threads.emplace_back([&] { io_context.run(); });
288
289 io_context.run();
290
291 for (auto& t : threads)
292 t.join();
275293
276294 LOG(INFO) << "Stopping streamServer" << endl;
277295 streamServer->stop();
282300 SLOG(ERROR) << "Exception: " << e.what() << std::endl;
283301 exitcode = EXIT_FAILURE;
284302 }
285
303 Config::instance().save();
286304 SLOG(NOTICE) << "daemon terminated." << endl;
287305 exit(exitcode);
288306 }
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
1717
1818 #include "stream_server.hpp"
1919 #include "common/aixlog.hpp"
20 #include "config.h"
20 #include "config.hpp"
2121 #include "message/hello.hpp"
2222 #include "message/stream_tags.hpp"
2323 #include "message/time.hpp"
2424 #include <iostream>
2525
2626 using namespace std;
27 using namespace streamreader;
2728
2829 using json = nlohmann::json;
2930
3031
31 StreamServer::StreamServer(boost::asio::io_context* io_context, const ServerSettings& serverSettings) : io_context_(io_context), settings_(serverSettings)
32 StreamServer::StreamServer(boost::asio::io_context& io_context, const ServerSettings& serverSettings) : io_context_(io_context), settings_(serverSettings)
3233 {
3334 }
3435
3637 StreamServer::~StreamServer() = default;
3738
3839
40 void StreamServer::cleanup()
41 {
42 auto new_end = std::remove_if(sessions_.begin(), sessions_.end(), [](std::weak_ptr<StreamSession> session) { return session.expired(); });
43 auto count = distance(new_end, sessions_.end());
44 if (count > 0)
45 {
46 SLOG(ERROR) << "Removing " << count << " inactive session(s), active sessions: " << sessions_.size() - count << "\n";
47 sessions_.erase(new_end, sessions_.end());
48 }
49 }
50
51
3952 void StreamServer::onMetaChanged(const PcmStream* pcmStream)
4053 {
41 /// Notification: {"jsonrpc":"2.0","method":"Stream.OnMetadata","params":{"id":"stream 1", "meta": {"album": "some album", "artist": "some artist", "track":
42 /// "some track"...}}
54 // clang-format off
55 // Notification: {"jsonrpc":"2.0","method":"Stream.OnMetadata","params":{"id":"stream 1", "meta": {"album": "some album", "artist": "some artist", "track": "some track"...}}
56 // clang-format on
4357
4458 // Send meta to all connected clients
4559 const auto meta = pcmStream->getMeta();
46 // cout << "metadata = " << meta->msg.dump(3) << "\n";
47
60 LOG(DEBUG) << "metadata = " << meta->msg.dump(3) << "\n";
61
62 std::lock_guard<std::recursive_mutex> mlock(sessionsMutex_);
4863 for (auto s : sessions_)
4964 {
50 if (s->pcmStream().get() == pcmStream)
51 s->sendAsync(meta);
65 if (auto session = s.lock())
66 {
67 if (session->pcmStream().get() == pcmStream)
68 session->sendAsync(meta);
69 }
5270 }
5371
5472 LOG(INFO) << "onMetaChanged (" << pcmStream->getName() << ")\n";
5573 json notification = jsonrpcpp::Notification("Stream.OnMetadata", jsonrpcpp::Parameter("id", pcmStream->getId(), "meta", meta->msg)).to_json();
5674 controlServer_->send(notification.dump(), nullptr);
57 ////cout << "Notification: " << notification.dump() << "\n";
75 // cout << "Notification: " << notification.dump() << "\n";
5876 }
5977
6078 void StreamServer::onStateChanged(const PcmStream* pcmStream, const ReaderState& state)
6179 {
62 /// Notification: {"jsonrpc":"2.0","method":"Stream.OnUpdate","params":{"id":"stream 1","stream":{"id":"stream
63 /// 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"buffer_ms":"20","codec":"flac","name":"stream
64 /// 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}}}}
65 LOG(INFO) << "onStateChanged (" << pcmStream->getName() << "): " << state << "\n";
80 // clang-format off
81 // Notification: {"jsonrpc":"2.0","method":"Stream.OnUpdate","params":{"id":"stream 1","stream":{"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}}}}
82 // clang-format on
83 LOG(INFO) << "onStateChanged (" << pcmStream->getName() << "): " << static_cast<int>(state) << "\n";
6684 // LOG(INFO) << pcmStream->toJson().dump(4);
6785 json notification = jsonrpcpp::Notification("Stream.OnUpdate", jsonrpcpp::Parameter("id", pcmStream->getId(), "stream", pcmStream->toJson())).to_json();
6886 controlServer_->send(notification.dump(), nullptr);
69 ////cout << "Notification: " << notification.dump() << "\n";
87 // cout << "Notification: " << notification.dump() << "\n";
7088 }
7189
7290
7492 {
7593 // LOG(INFO) << "onChunkRead (" << pcmStream->getName() << "): " << duration << "ms\n";
7694 bool isDefaultStream(pcmStream == streamManager_->getDefaultStream().get());
77
78 msg::message_ptr shared_message(chunk);
79 std::lock_guard<std::recursive_mutex> mlock(sessionsMutex_);
80 for (auto s : sessions_)
95 unique_ptr<msg::PcmChunk> chunk_ptr(chunk);
96
97 std::ostringstream oss;
98 tv t;
99 chunk_ptr->sent = t;
100 chunk_ptr->serialize(oss);
101 shared_const_buffer buffer(oss.str());
102
103 std::vector<std::shared_ptr<StreamSession>> sessions;
104 {
105 std::lock_guard<std::recursive_mutex> mlock(sessionsMutex_);
106 for (auto session : sessions_)
107 if (auto s = session.lock())
108 sessions.push_back(s);
109 }
110
111 for (auto session : sessions)
81112 {
82113 if (!settings_.stream.sendAudioToMutedClients)
83114 {
84 GroupPtr group = Config::instance().getGroupFromClient(s->clientId);
115 GroupPtr group = Config::instance().getGroupFromClient(session->clientId);
85116 if (group)
86117 {
87118 if (group->muted)
119 {
88120 continue;
89
90 ClientInfoPtr client = group->getClient(s->clientId);
91 if (client && client->config.volume.muted)
92 continue;
93 }
94 }
95
96 if (!s->pcmStream() && isDefaultStream) //->getName() == "default")
97 s->sendAsync(shared_message);
98 else if (s->pcmStream().get() == pcmStream)
99 s->sendAsync(shared_message);
121 }
122 else
123 {
124 std::lock_guard<std::recursive_mutex> lock(clientMutex_);
125 ClientInfoPtr client = group->getClient(session->clientId);
126 if (client && client->config.volume.muted)
127 continue;
128 }
129 }
130 }
131
132 if (!session->pcmStream() && isDefaultStream) //->getName() == "default")
133 session->sendAsync(buffer);
134 else if (session->pcmStream().get() == pcmStream)
135 session->sendAsync(buffer);
100136 }
101137 }
102138
117153
118154 LOG(INFO) << "onDisconnect: " << session->clientId << "\n";
119155 LOG(DEBUG) << "sessions: " << sessions_.size() << "\n";
120 // don't block: remove StreamSession in a thread
121 auto func = [](shared_ptr<StreamSession> s) -> void { s->stop(); };
122 std::thread t(func, session);
123 t.detach();
124 sessions_.erase(session);
125
156 sessions_.erase(std::remove_if(sessions_.begin(), sessions_.end(),
157 [streamSession](std::weak_ptr<StreamSession> session) {
158 auto s = session.lock();
159 return s.get() == streamSession;
160 }),
161 sessions_.end());
126162 LOG(DEBUG) << "sessions: " << sessions_.size() << "\n";
127163
128164 // notify controllers if not yet done
135171 Config::instance().save();
136172 if (controlServer_ != nullptr)
137173 {
138 /// Check if there is no session of this client is left
139 /// Can happen in case of ungraceful disconnect/reconnect or
140 /// in case of a duplicate client id
174 // Check if there is no session of this client is left
175 // Can happen in case of ungraceful disconnect/reconnect or
176 // in case of a duplicate client id
141177 if (getStreamSession(clientInfo->id) == nullptr)
142178 {
143 /// Notification:
144 /// {"jsonrpc":"2.0","method":"Client.OnDisconnect","params":{"client":{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":81}},"connected":false,"host":{"arch":"x86_64","ip":"192.168.0.54","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux
145 /// Mint 17.3
146 /// Rosa"},"id":"00:21:6a:7d:74:fc","lastSeen":{"sec":1488025523,"usec":814067},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}},"id":"00:21:6a:7d:74:fc"}}
179 // clang-format off
180 // Notification:
181 // {"jsonrpc":"2.0","method":"Client.OnDisconnect","params":{"client":{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":81}},"connected":false,"host":{"arch":"x86_64","ip":"192.168.0.54","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc","lastSeen":{"sec":1488025523,"usec":814067},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}},"id":"00:21:6a:7d:74:fc"}}
182 // clang-format on
147183 json notification =
148184 jsonrpcpp::Notification("Client.OnDisconnect", jsonrpcpp::Parameter("id", clientInfo->id, "client", clientInfo->toJson())).to_json();
149185 controlServer_->send(notification.dump());
150 ////cout << "Notification: " << notification.dump() << "\n";
151 }
152 }
186 // cout << "Notification: " << notification.dump() << "\n";
187 }
188 }
189 cleanup();
153190 }
154191
155192
157194 {
158195 try
159196 {
160 ////LOG(INFO) << "StreamServer::ProcessRequest method: " << request->method << ", " << "id: " << request->id() << "\n";
197 // LOG(INFO) << "StreamServer::ProcessRequest method: " << request->method << ", " << "id: " << request->id() << "\n";
161198 Json result;
162199
163200 if (request->method().find("Client.") == 0)
168205
169206 if (request->method() == "Client.GetStatus")
170207 {
171 /// Request: {"id":8,"jsonrpc":"2.0","method":"Client.GetStatus","params":{"id":"00:21:6a:7d:74:fc"}}
172 /// Response:
173 /// {"id":8,"jsonrpc":"2.0","result":{"client":{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":74}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux
174 /// Mint 17.3
175 /// Rosa"},"id":"00:21:6a:7d:74:fc","lastSeen":{"sec":1488026416,"usec":135973},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}}}
208 // clang-format off
209 // Request: {"id":8,"jsonrpc":"2.0","method":"Client.GetStatus","params":{"id":"00:21:6a:7d:74:fc"}}
210 // Response: {"id":8,"jsonrpc":"2.0","result":{"client":{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":74}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc","lastSeen":{"sec":1488026416,"usec":135973},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}}}
211 // clang-format on
176212 result["client"] = clientInfo->toJson();
177213 }
178214 else if (request->method() == "Client.SetVolume")
179215 {
180 /// Request: {"id":8,"jsonrpc":"2.0","method":"Client.SetVolume","params":{"id":"00:21:6a:7d:74:fc","volume":{"muted":false,"percent":74}}}
181 /// Response: {"id":8,"jsonrpc":"2.0","result":{"volume":{"muted":false,"percent":74}}}
182 /// Notification: {"jsonrpc":"2.0","method":"Client.OnVolumeChanged","params":{"id":"00:21:6a:7d:74:fc","volume":{"muted":false,"percent":74}}}
216 // clang-format off
217 // Request: {"id":8,"jsonrpc":"2.0","method":"Client.SetVolume","params":{"id":"00:21:6a:7d:74:fc","volume":{"muted":false,"percent":74}}}
218 // Response: {"id":8,"jsonrpc":"2.0","result":{"volume":{"muted":false,"percent":74}}}
219 // Notification: {"jsonrpc":"2.0","method":"Client.OnVolumeChanged","params":{"id":"00:21:6a:7d:74:fc","volume":{"muted":false,"percent":74}}}
220 // clang-format on
221
222 std::lock_guard<std::recursive_mutex> lock(clientMutex_);
183223 clientInfo->config.volume.fromJson(request->params().get("volume"));
184224 result["volume"] = clientInfo->config.volume.toJson();
185225 notification.reset(new jsonrpcpp::Notification("Client.OnVolumeChanged",
187227 }
188228 else if (request->method() == "Client.SetLatency")
189229 {
190 /// Request: {"id":7,"jsonrpc":"2.0","method":"Client.SetLatency","params":{"id":"00:21:6a:7d:74:fc#2","latency":10}}
191 /// Response: {"id":7,"jsonrpc":"2.0","result":{"latency":10}}
192 /// Notification: {"jsonrpc":"2.0","method":"Client.OnLatencyChanged","params":{"id":"00:21:6a:7d:74:fc#2","latency":10}}
230 // clang-format off
231 // Request: {"id":7,"jsonrpc":"2.0","method":"Client.SetLatency","params":{"id":"00:21:6a:7d:74:fc#2","latency":10}}
232 // Response: {"id":7,"jsonrpc":"2.0","result":{"latency":10}}
233 // Notification: {"jsonrpc":"2.0","method":"Client.OnLatencyChanged","params":{"id":"00:21:6a:7d:74:fc#2","latency":10}}
234 // clang-format on
193235 int latency = request->params().get("latency");
194236 if (latency < -10000)
195237 latency = -10000;
202244 }
203245 else if (request->method() == "Client.SetName")
204246 {
205 /// Request: {"id":6,"jsonrpc":"2.0","method":"Client.SetName","params":{"id":"00:21:6a:7d:74:fc#2","name":"Laptop"}}
206 /// Response: {"id":6,"jsonrpc":"2.0","result":{"name":"Laptop"}}
207 /// Notification: {"jsonrpc":"2.0","method":"Client.OnNameChanged","params":{"id":"00:21:6a:7d:74:fc#2","name":"Laptop"}}
247 // clang-format off
248 // Request: {"id":6,"jsonrpc":"2.0","method":"Client.SetName","params":{"id":"00:21:6a:7d:74:fc#2","name":"Laptop"}}
249 // Response: {"id":6,"jsonrpc":"2.0","result":{"name":"Laptop"}}
250 // Notification: {"jsonrpc":"2.0","method":"Client.OnNameChanged","params":{"id":"00:21:6a:7d:74:fc#2","name":"Laptop"}}
251 // clang-format on
208252 clientInfo->config.name = request->params().get<std::string>("name");
209253 result["name"] = clientInfo->config.name;
210254 notification.reset(
238282
239283 if (request->method() == "Group.GetStatus")
240284 {
241 /// Request: {"id":5,"jsonrpc":"2.0","method":"Group.GetStatus","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1"}}
242 /// Response:
243 /// {"id":5,"jsonrpc":"2.0","result":{"group":{"clients":[{"config":{"instance":2,"latency":10,"name":"Laptop","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux
244 /// Mint 17.3
245 /// Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488026485,"usec":644997},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}},{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":74}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux
246 /// Mint 17.3
247 /// Rosa"},"id":"00:21:6a:7d:74:fc","lastSeen":{"sec":1488026481,"usec":223747},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":true,"name":"","stream_id":"stream
248 /// 1"}}}
285 // clang-format off
286 // Request: {"id":5,"jsonrpc":"2.0","method":"Group.GetStatus","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1"}}
287 // Response: {"id":5,"jsonrpc":"2.0","result":{"group":{"clients":[{"config":{"instance":2,"latency":10,"name":"Laptop","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488026485,"usec":644997},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}},{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":74}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc","lastSeen":{"sec":1488026481,"usec":223747},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":true,"name":"","stream_id":"stream 1"}}}
288 // clang-format on
249289 result["group"] = group->toJson();
250290 }
251291 else if (request->method() == "Group.SetName")
252292 {
253 /// Request: {"id":6,"jsonrpc":"2.0","method":"Group.SetName","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","name":"Laptop"}}
254 /// Response: {"id":6,"jsonrpc":"2.0","result":{"name":"MediaPlayer"}}
255 /// Notification: {"jsonrpc":"2.0","method":"Group.OnNameChanged","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","MediaPlayer":"Laptop"}}
293 // clang-format off
294 // Request: {"id":6,"jsonrpc":"2.0","method":"Group.SetName","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","name":"Laptop"}}
295 // Response: {"id":6,"jsonrpc":"2.0","result":{"name":"MediaPlayer"}}
296 // Notification: {"jsonrpc":"2.0","method":"Group.OnNameChanged","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","MediaPlayer":"Laptop"}}
297 // clang-format on
256298 group->name = request->params().get<std::string>("name");
257299 result["name"] = group->name;
258300 notification.reset(new jsonrpcpp::Notification("Group.OnNameChanged", jsonrpcpp::Parameter("id", group->id, "name", group->name)));
259301 }
260302 else if (request->method() == "Group.SetMute")
261303 {
262 /// Request: {"id":5,"jsonrpc":"2.0","method":"Group.SetMute","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","mute":true}}
263 /// Response: {"id":5,"jsonrpc":"2.0","result":{"mute":true}}
264 /// Notification: {"jsonrpc":"2.0","method":"Group.OnMute","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","mute":true}}
304 // clang-format off
305 // Request: {"id":5,"jsonrpc":"2.0","method":"Group.SetMute","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","mute":true}}
306 // Response: {"id":5,"jsonrpc":"2.0","result":{"mute":true}}
307 // Notification: {"jsonrpc":"2.0","method":"Group.OnMute","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","mute":true}}
308 // clang-format on
265309 bool muted = request->params().get<bool>("mute");
266310 group->muted = muted;
267311
286330 }
287331 else if (request->method() == "Group.SetStream")
288332 {
289 /// Request: {"id":4,"jsonrpc":"2.0","method":"Group.SetStream","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","stream_id":"stream
290 /// 1"}}
291 /// Response: {"id":4,"jsonrpc":"2.0","result":{"stream_id":"stream 1"}}
292 /// Notification: {"jsonrpc":"2.0","method":"Group.OnStreamChanged","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","stream_id":"stream
293 /// 1"}}
333 // clang-format off
334 // Request: {"id":4,"jsonrpc":"2.0","method":"Group.SetStream","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","stream_id":"stream 1"}}
335 // Response: {"id":4,"jsonrpc":"2.0","result":{"stream_id":"stream 1"}}
336 // Notification: {"jsonrpc":"2.0","method":"Group.OnStreamChanged","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","stream_id":"stream 1"}}
337 // clang-format on
294338 string streamId = request->params().get<std::string>("stream_id");
295339 PcmStreamPtr stream = streamManager_->getStream(streamId);
296340 if (stream == nullptr)
298342
299343 group->streamId = streamId;
300344
301 /// Update clients
345 // Update clients
302346 for (auto client : group->clients)
303347 {
304348 session_ptr session = getStreamSession(client->id);
310354 }
311355 }
312356
313 /// Notify others
357 // Notify others
314358 result["stream_id"] = group->streamId;
315359 notification.reset(new jsonrpcpp::Notification("Group.OnStreamChanged", jsonrpcpp::Parameter("id", group->id, "stream_id", group->streamId)));
316360 }
317361 else if (request->method() == "Group.SetClients")
318362 {
319 /// Request:
320 /// {"id":3,"jsonrpc":"2.0","method":"Group.SetClients","params":{"clients":["00:21:6a:7d:74:fc#2","00:21:6a:7d:74:fc"],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1"}}
321 /// Response: {"id":3,"jsonrpc":"2.0","result":{"server":{"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123
322 /// 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux
323 /// Mint 17.3
324 /// Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025901,"usec":864472},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}},{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":100}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux
325 /// Mint 17.3
326 /// Rosa"},"id":"00:21:6a:7d:74:fc","lastSeen":{"sec":1488025905,"usec":45238},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream
327 /// 2"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3
328 /// Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream
329 /// 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"buffer_ms":"20","codec":"flac","name":"stream
330 /// 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream
331 /// 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"buffer_ms":"20","codec":"flac","name":"stream
332 /// 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}}}
333 /// Notification:
334 /// {"jsonrpc":"2.0","method":"Server.OnUpdate","params":{"server":{"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123
335 /// 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux
336 /// Mint 17.3
337 /// Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025901,"usec":864472},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}},{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":100}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux
338 /// Mint 17.3
339 /// Rosa"},"id":"00:21:6a:7d:74:fc","lastSeen":{"sec":1488025905,"usec":45238},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream
340 /// 2"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3
341 /// Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream
342 /// 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"buffer_ms":"20","codec":"flac","name":"stream
343 /// 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream
344 /// 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"buffer_ms":"20","codec":"flac","name":"stream
345 /// 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}}}
363 // clang-format off
364 // Request: {"id":3,"jsonrpc":"2.0","method":"Group.SetClients","params":{"clients":["00:21:6a:7d:74:fc#2","00:21:6a:7d:74:fc"],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1"}}
365 // Response: {"id":3,"jsonrpc":"2.0","result":{"server":{"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025901,"usec":864472},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}},{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":100}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc","lastSeen":{"sec":1488025905,"usec":45238},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream 2"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3 Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}}}
366 // Notification: {"jsonrpc":"2.0","method":"Server.OnUpdate","params":{"server":{"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025901,"usec":864472},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}},{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":100}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc","lastSeen":{"sec":1488025905,"usec":45238},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream 2"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3 Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}}}
367 // clang-format on
346368 vector<string> clients = request->params().get("clients");
347 /// Remove clients from group
369 // Remove clients from group
348370 for (auto iter = group->clients.begin(); iter != group->clients.end();)
349371 {
350372 auto client = *iter;
358380 newGroup->streamId = group->streamId;
359381 }
360382
361 /// Add clients to group
383 // Add clients to group
362384 PcmStreamPtr stream = streamManager_->getStream(group->streamId);
363385 for (const auto& clientId : clients)
364386 {
377399
378400 group->addClient(client);
379401
380 /// assign new stream
402 // assign new stream
381403 session_ptr session = getStreamSession(client->id);
382404 if (session && stream && (session->pcmStream() != stream))
383405 {
393415 json server = Config::instance().getServerStatus(streamManager_->toJson());
394416 result["server"] = server;
395417
396 /// Notify others: since at least two groups are affected, send a complete server update
418 // Notify others: since at least two groups are affected, send a complete server update
397419 notification.reset(new jsonrpcpp::Notification("Server.OnUpdate", jsonrpcpp::Parameter("server", server)));
398420 }
399421 else
403425 {
404426 if (request->method().find("Server.GetRPCVersion") == 0)
405427 {
406 /// Request: {"id":8,"jsonrpc":"2.0","method":"Server.GetRPCVersion"}
407 /// Response: {"id":8,"jsonrpc":"2.0","result":{"major":2,"minor":0,"patch":0}}
428 // Request: {"id":8,"jsonrpc":"2.0","method":"Server.GetRPCVersion"}
429 // Response: {"id":8,"jsonrpc":"2.0","result":{"major":2,"minor":0,"patch":0}}
408430 // <major>: backwards incompatible change
409431 result["major"] = 2;
410432 // <minor>: feature addition to the API
414436 }
415437 else if (request->method() == "Server.GetStatus")
416438 {
417 /// Request: {"id":1,"jsonrpc":"2.0","method":"Server.GetStatus"}
418 /// Response: {"id":1,"jsonrpc":"2.0","result":{"server":{"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123
419 /// 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux
420 /// Mint 17.3
421 /// Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025696,"usec":578142},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}},{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":81}},"connected":true,"host":{"arch":"x86_64","ip":"192.168.0.54","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux
422 /// Mint 17.3
423 /// Rosa"},"id":"00:21:6a:7d:74:fc","lastSeen":{"sec":1488025696,"usec":611255},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream
424 /// 2"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3
425 /// Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream
426 /// 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"buffer_ms":"20","codec":"flac","name":"stream
427 /// 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream
428 /// 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"buffer_ms":"20","codec":"flac","name":"stream
429 /// 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}}}
439 // clang-format off
440 // Request: {"id":1,"jsonrpc":"2.0","method":"Server.GetStatus"}
441 // Response: {"id":1,"jsonrpc":"2.0","result":{"server":{"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025696,"usec":578142},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}},{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":81}},"connected":true,"host":{"arch":"x86_64","ip":"192.168.0.54","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc","lastSeen":{"sec":1488025696,"usec":611255},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream 2"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3 Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}}}
442 // clang-format on
430443 result["server"] = Config::instance().getServerStatus(streamManager_->toJson());
431444 }
432445 else if (request->method() == "Server.DeleteClient")
433446 {
434 /// Request: {"id":2,"jsonrpc":"2.0","method":"Server.DeleteClient","params":{"id":"00:21:6a:7d:74:fc"}}
435 /// Response: {"id":2,"jsonrpc":"2.0","result":{"server":{"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123
436 /// 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux
437 /// Mint 17.3
438 /// Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025751,"usec":654777},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream
439 /// 2"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3
440 /// Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream
441 /// 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"buffer_ms":"20","codec":"flac","name":"stream
442 /// 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream
443 /// 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"buffer_ms":"20","codec":"flac","name":"stream
444 /// 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}}}
445 /// Notification:
446 /// {"jsonrpc":"2.0","method":"Server.OnUpdate","params":{"server":{"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123
447 /// 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux
448 /// Mint 17.3
449 /// Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025751,"usec":654777},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream
450 /// 2"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3
451 /// Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream
452 /// 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"buffer_ms":"20","codec":"flac","name":"stream
453 /// 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream
454 /// 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"buffer_ms":"20","codec":"flac","name":"stream
455 /// 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}}}
447 // clang-format off
448 // Request: {"id":2,"jsonrpc":"2.0","method":"Server.DeleteClient","params":{"id":"00:21:6a:7d:74:fc"}}
449 // Response: {"id":2,"jsonrpc":"2.0","result":{"server":{"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025751,"usec":654777},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream 2"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3 Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}}}
450 // Notification: {"jsonrpc":"2.0","method":"Server.OnUpdate","params":{"server":{"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025751,"usec":654777},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream 2"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3 Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}}}
451 // clang-format on
456452 ClientInfoPtr clientInfo = Config::instance().getClientInfo(request->params().get<std::string>("id"));
457453 if (clientInfo == nullptr)
458454 throw jsonrpcpp::InternalErrorException("Client not found", request->id());
494490 }
495491 else if (request->method() == "Stream.AddStream")
496492 {
497 /// Request: {"id":4,"jsonrpc":"2.0","method":"Stream.AddStream","params":{"streamUri":"uri"}}
498 ///
499 /// Response: {"id":4,"jsonrpc":"2.0","result":{"stream_id":"Spotify"}}
500 /// Call onMetaChanged(const PcmStream* pcmStream) for updates and notifications
493 // clang-format off
494 // Request: {"id":4,"jsonrpc":"2.0","method":"Stream.AddStream","params":{"streamUri":"uri"}}
495 // Response: {"id":4,"jsonrpc":"2.0","result":{"stream_id":"Spotify"}}
496 // Call onMetaChanged(const PcmStream* pcmStream) for updates and notifications
497 // clang-format on
501498
502499 LOG(INFO) << "Stream.AddStream(" << request->params().get("streamUri") << ")"
503500 << "\n";
513510 }
514511 else if (request->method() == "Stream.RemoveStream")
515512 {
516 /// Request: {"id":4,"jsonrpc":"2.0","method":"Stream.RemoveStream","params":{"id":"Spotify"}}
517 ///
518 /// Response: {"id":4,"jsonrpc":"2.0","result":{"stream_id":"Spotify"}}
519 /// Call onMetaChanged(const PcmStream* pcmStream) for updates and notifications
513 // clang-format off
514 // Request: {"id":4,"jsonrpc":"2.0","method":"Stream.RemoveStream","params":{"id":"Spotify"}}
515 // Response: {"id":4,"jsonrpc":"2.0","result":{"stream_id":"Spotify"}}
516 // Call onMetaChanged(const PcmStream* pcmStream) for updates and notifications
517 // clang-format on
520518
521519 LOG(INFO) << "Stream.RemoveStream(" << request->params().get("id") << ")"
522520 << "\n";
533531 else
534532 throw jsonrpcpp::MethodNotFoundException(request->id());
535533
536 Config::instance().save();
537534 response.reset(new jsonrpcpp::Response(*request, result));
538535 }
539536 catch (const jsonrpcpp::RequestException& e)
551548
552549 std::string StreamServer::onMessageReceived(ControlSession* controlSession, const std::string& message)
553550 {
554 LOG(DEBUG) << "onMessageReceived: " << message << "\n";
551 // LOG(DEBUG) << "onMessageReceived: " << message << "\n";
555552 jsonrpcpp::entity_ptr entity(nullptr);
556553 try
557554 {
574571 {
575572 jsonrpcpp::request_ptr request = dynamic_pointer_cast<jsonrpcpp::Request>(entity);
576573 ProcessRequest(request, response, notification);
574 Config::instance().save();
577575 ////cout << "Request: " << request->to_json().dump() << "\n";
578576 if (notification)
579577 {
605603 notificationBatch.add_ptr(notification);
606604 }
607605 }
606 Config::instance().save();
608607 if (!notificationBatch.entities.empty())
609608 controlServer_->send(notificationBatch.to_json().dump(), controlSession);
610609 if (!responseBatch.entities.empty())
699698
700699 if (newGroup)
701700 {
702 /// Notification:
703 /// {"jsonrpc":"2.0","method":"Server.OnUpdate","params":{"server":{"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123
704 /// 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux
705 /// Mint 17.3
706 /// Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025796,"usec":714671},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream
707 /// 2"},{"clients":[{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":100}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux
708 /// Mint 17.3
709 /// Rosa"},"id":"00:21:6a:7d:74:fc","lastSeen":{"sec":1488025798,"usec":728305},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"c5da8f7a-f377-1e51-8266-c5cc61099b71","muted":false,"name":"","stream_id":"stream
710 /// 1"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3
711 /// Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream
712 /// 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"buffer_ms":"20","codec":"flac","name":"stream
713 /// 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream
714 /// 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"buffer_ms":"20","codec":"flac","name":"stream
715 /// 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}}}
701 // clang-format off
702 // Notification: {"jsonrpc":"2.0","method":"Server.OnUpdate","params":{"server":{"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025796,"usec":714671},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream 2"},{"clients":[{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":100}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc","lastSeen":{"sec":1488025798,"usec":728305},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"c5da8f7a-f377-1e51-8266-c5cc61099b71","muted":false,"name":"","stream_id":"stream 1"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3 Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}}}
703 // clang-format on
716704 json server = Config::instance().getServerStatus(streamManager_->toJson());
717705 json notification = jsonrpcpp::Notification("Server.OnUpdate", jsonrpcpp::Parameter("server", server)).to_json();
718706 controlServer_->send(notification.dump());
719 ////cout << "Notification: " << notification.dump() << "\n";
720707 }
721708 else
722709 {
723 /// Notification:
724 /// {"jsonrpc":"2.0","method":"Client.OnConnect","params":{"client":{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":81}},"connected":true,"host":{"arch":"x86_64","ip":"192.168.0.54","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux
725 /// Mint 17.3
726 /// Rosa"},"id":"00:21:6a:7d:74:fc","lastSeen":{"sec":1488025524,"usec":876332},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}},"id":"00:21:6a:7d:74:fc"}}
710 // clang-format off
711 // Notification: {"jsonrpc":"2.0","method":"Client.OnConnect","params":{"client":{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":81}},"connected":true,"host":{"arch":"x86_64","ip":"192.168.0.54","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc","lastSeen":{"sec":1488025524,"usec":876332},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}},"id":"00:21:6a:7d:74:fc"}}
712 // clang-format on
727713 json notification = jsonrpcpp::Notification("Client.OnConnect", jsonrpcpp::Parameter("id", client->id, "client", client->toJson())).to_json();
728714 controlServer_->send(notification.dump());
729 ////cout << "Notification: " << notification.dump() << "\n";
715 // cout << "Notification: " << notification.dump() << "\n";
730716 }
731717 // cout << Config::instance().getServerStatus(streamManager_->toJson()).dump(4) << "\n";
732718 // cout << group->toJson().dump(4) << "\n";
738724 session_ptr StreamServer::getStreamSession(StreamSession* streamSession) const
739725 {
740726 std::lock_guard<std::recursive_mutex> mlock(sessionsMutex_);
727
741728 for (auto session : sessions_)
742729 {
743 if (session.get() == streamSession)
744 return session;
730 if (auto s = session.lock())
731 if (s.get() == streamSession)
732 return s;
745733 }
746734 return nullptr;
747735 }
753741 std::lock_guard<std::recursive_mutex> mlock(sessionsMutex_);
754742 for (auto session : sessions_)
755743 {
756 if (session->clientId == clientId)
757 return session;
744 if (auto s = session.lock())
745 if (s->clientId == clientId)
746 return s;
758747 }
759748 return nullptr;
760749 }
788777 socket.set_option(tcp::no_delay(true));
789778
790779 SLOG(NOTICE) << "StreamServer::NewConnection: " << socket.remote_endpoint().address().to_string() << endl;
791 shared_ptr<StreamSession> session = make_shared<StreamSession>(this, std::move(socket));
780 shared_ptr<StreamSession> session = make_shared<StreamSession>(io_context_, this, std::move(socket));
792781
793782 session->setBufferMs(settings_.stream.bufferMs);
794783 session->start();
795784
796785 std::lock_guard<std::recursive_mutex> mlock(sessionsMutex_);
797 sessions_.insert(session);
786 sessions_.emplace_back(session);
787 cleanup();
798788 }
799789 catch (const std::exception& e)
800790 {
808798 {
809799 try
810800 {
811 controlServer_.reset(new ControlServer(io_context_, settings_.tcp, settings_.http, this));
801 controlServer_ = std::make_unique<ControlServer>(io_context_, settings_.tcp, settings_.http, this);
812802 controlServer_->start();
813803
814 streamManager_.reset(new StreamManager(this, settings_.stream.sampleFormat, settings_.stream.codec, settings_.stream.streamReadMs));
804 streamManager_ =
805 std::make_unique<StreamManager>(this, io_context_, settings_.stream.sampleFormat, settings_.stream.codec, settings_.stream.streamChunkMs);
815806 // throw SnapException("xxx");
816807 for (const auto& streamUri : settings_.stream.pcmStreams)
817808 {
827818 {
828819 LOG(INFO) << "Creating stream acceptor for address: " << address << ", port: " << settings_.stream.port << "\n";
829820 acceptor_.emplace_back(
830 make_unique<tcp::acceptor>(*io_context_, tcp::endpoint(boost::asio::ip::address::from_string(address), settings_.stream.port)));
821 make_unique<tcp::acceptor>(io_context_, tcp::endpoint(boost::asio::ip::address::from_string(address), settings_.stream.port)));
831822 }
832823 catch (const boost::system::system_error& e)
833824 {
848839
849840 void StreamServer::stop()
850841 {
851 if (streamManager_)
852 {
853 streamManager_->stop();
854 streamManager_ = nullptr;
855 }
856
857 {
858 std::lock_guard<std::recursive_mutex> mlock(sessionsMutex_);
859 for (auto session : sessions_)
860 {
861 if (session)
862 session->stop();
863 }
864 sessions_.clear();
865 }
866
867 if (controlServer_)
868 {
869 controlServer_->stop();
870 controlServer_ = nullptr;
871 }
872
873842 for (auto& acceptor : acceptor_)
874843 acceptor->cancel();
875844 acceptor_.clear();
876 }
845
846 if (streamManager_)
847 {
848 streamManager_->stop();
849 streamManager_ = nullptr;
850 }
851
852 if (controlServer_)
853 {
854 controlServer_->stop();
855 controlServer_ = nullptr;
856 }
857
858 std::lock_guard<std::recursive_mutex> mlock(sessionsMutex_);
859 cleanup();
860 for (auto s : sessions_)
861 {
862 if (auto session = s.lock())
863 session->stop();
864 }
865 }
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
1515 along with this program. If not, see <http://www.gnu.org/licenses/>.
1616 ***/
1717
18 #ifndef STREAM_SERVER_H
19 #define STREAM_SERVER_H
18 #ifndef STREAM_SERVER_HPP
19 #define STREAM_SERVER_HPP
2020
2121 #include <boost/asio.hpp>
2222 #include <memory>
2323 #include <mutex>
2424 #include <set>
2525 #include <sstream>
26 #include <thread>
2726 #include <vector>
2827
2928 #include "common/queue.h"
3736 #include "stream_session.hpp"
3837 #include "streamreader/stream_manager.hpp"
3938
39 using namespace streamreader;
4040
4141 using boost::asio::ip::tcp;
4242 using acceptor_ptr = std::unique_ptr<tcp::acceptor>;
43 using socket_ptr = std::shared_ptr<tcp::socket>;
4443 using session_ptr = std::shared_ptr<StreamSession>;
4544
4645
5150 * Receives (via the MessageReceiver interface) and answers messages from the clients
5251 * Forwards PCM data to the clients
5352 */
54 class StreamServer : public MessageReceiver, ControlMessageReceiver, PcmListener
53 class StreamServer : public MessageReceiver, public ControlMessageReceiver, public PcmListener
5554 {
5655 public:
57 StreamServer(boost::asio::io_context* io_context, const ServerSettings& serverSettings);
56 StreamServer(boost::asio::io_context& io_context, const ServerSettings& serverSettings);
5857 virtual ~StreamServer();
5958
6059 void start();
8281 session_ptr getStreamSession(const std::string& mac) const;
8382 session_ptr getStreamSession(StreamSession* session) const;
8483 void ProcessRequest(const jsonrpcpp::request_ptr request, jsonrpcpp::entity_ptr& response, jsonrpcpp::notification_ptr& notification) const;
84 void cleanup();
85
8586 mutable std::recursive_mutex sessionsMutex_;
86 std::set<session_ptr> sessions_;
87 boost::asio::io_context* io_context_;
87 mutable std::recursive_mutex clientMutex_;
88 std::vector<std::weak_ptr<StreamSession>> sessions_;
89 boost::asio::io_context& io_context_;
8890 std::vector<acceptor_ptr> acceptor_;
8991
9092 ServerSettings settings_;
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
2020 #include "common/aixlog.hpp"
2121 #include "message/pcm_chunk.hpp"
2222 #include <iostream>
23 #include <mutex>
2423
2524 using namespace std;
26
27
28
29 StreamSession::StreamSession(MessageReceiver* receiver, tcp::socket&& socket)
30 : active_(false), readerThread_(nullptr), writerThread_(nullptr), socket_(std::move(socket)), messageReceiver_(receiver), pcmStream_(nullptr)
31 {
25 using namespace streamreader;
26
27
28 static constexpr auto LOG_TAG = "StreamSession";
29
30
31 StreamSession::StreamSession(boost::asio::io_context& ioc, MessageReceiver* receiver, tcp::socket&& socket)
32 : socket_(std::move(socket)), messageReceiver_(receiver), pcmStream_(nullptr), strand_(ioc)
33 {
34 base_msg_size_ = baseMessage_.getSize();
35 buffer_.resize(base_msg_size_);
3236 }
3337
3438
3842 }
3943
4044
45 void StreamSession::read_next()
46 {
47 shared_ptr<StreamSession> self;
48 try
49 {
50 self = shared_from_this();
51 }
52 catch (const std::bad_weak_ptr& e)
53 {
54 LOG(ERROR, LOG_TAG) << "read_next: Error getting shared from this\n";
55 return;
56 }
57
58 boost::asio::async_read(socket_, boost::asio::buffer(buffer_, base_msg_size_),
59 boost::asio::bind_executor(strand_, [this, self](boost::system::error_code ec, std::size_t length) mutable {
60 if (ec)
61 {
62 LOG(ERROR, LOG_TAG) << "Error reading message header of length " << length << ": " << ec.message() << "\n";
63 messageReceiver_->onDisconnect(this);
64 return;
65 }
66
67 baseMessage_.deserialize(buffer_.data());
68 LOG(DEBUG, LOG_TAG) << "getNextMessage: " << baseMessage_.type << ", size: " << baseMessage_.size << ", id: " << baseMessage_.id
69 << ", refers: " << baseMessage_.refersTo << "\n";
70 if (baseMessage_.type > message_type::kLast)
71 {
72 LOG(ERROR, LOG_TAG) << "unknown message type received: " << baseMessage_.type << ", size: " << baseMessage_.size << "\n";
73 messageReceiver_->onDisconnect(this);
74 return;
75 }
76 else if (baseMessage_.size > msg::max_size)
77 {
78 LOG(ERROR, LOG_TAG) << "received message of type " << baseMessage_.type << " to large: " << baseMessage_.size << "\n";
79 messageReceiver_->onDisconnect(this);
80 return;
81 }
82
83 if (baseMessage_.size > buffer_.size())
84 buffer_.resize(baseMessage_.size);
85
86 boost::asio::async_read(
87 socket_, boost::asio::buffer(buffer_, baseMessage_.size),
88 boost::asio::bind_executor(strand_, [this, self](boost::system::error_code ec, std::size_t length) mutable {
89 if (ec)
90 {
91 LOG(ERROR, LOG_TAG) << "Error reading message body of length " << length << ": " << ec.message() << "\n";
92 messageReceiver_->onDisconnect(this);
93 return;
94 }
95
96 tv t;
97 baseMessage_.received = t;
98 if (messageReceiver_ != nullptr)
99 messageReceiver_->onMessageReceived(this, baseMessage_, buffer_.data());
100 read_next();
101 }));
102 }));
103 }
104
105
41106 void StreamSession::setPcmStream(PcmStreamPtr pcmStream)
42107 {
43108 pcmStream_ = pcmStream;
52117
53118 void StreamSession::start()
54119 {
55 {
56 std::lock_guard<std::mutex> activeLock(activeMutex_);
57 active_ = true;
58 }
59 readerThread_.reset(new thread(&StreamSession::reader, this));
60 writerThread_.reset(new thread(&StreamSession::writer, this));
120 read_next();
121 // strand_.post([this]() { read_next(); });
61122 }
62123
63124
64125 void StreamSession::stop()
65126 {
66 {
67 std::lock_guard<std::mutex> activeLock(activeMutex_);
68 if (!active_)
127 LOG(DEBUG, LOG_TAG) << "StreamSession::stop\n";
128 boost::system::error_code ec;
129 socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
130 if (ec)
131 LOG(ERROR, LOG_TAG) << "Error in socket shutdown: " << ec.message() << "\n";
132 socket_.close(ec);
133 if (ec)
134 LOG(ERROR, LOG_TAG) << "Error in socket close: " << ec.message() << "\n";
135 LOG(DEBUG, LOG_TAG) << "StreamSession stopped\n";
136 }
137
138
139 void StreamSession::send_next()
140 {
141 shared_ptr<StreamSession> self;
142 try
143 {
144 self = shared_from_this();
145 }
146 catch (const std::bad_weak_ptr& e)
147 {
148 LOG(ERROR, LOG_TAG) << "send_next: Error getting shared from this\n";
149 return;
150 }
151
152 auto buffer = messages_.front();
153
154 boost::asio::async_write(socket_, buffer, boost::asio::bind_executor(strand_, [this, self, buffer](boost::system::error_code ec, std::size_t length) {
155 messages_.pop_front();
156 if (ec)
157 {
158 LOG(ERROR, LOG_TAG) << "StreamSession write error (msg lenght: " << length << "): " << ec.message() << "\n";
159 messageReceiver_->onDisconnect(this);
160 return;
161 }
162 if (!messages_.empty())
163 send_next();
164 }));
165 }
166
167
168 void StreamSession::sendAsync(shared_const_buffer const_buf, bool send_now)
169 {
170 strand_.post([this, const_buf, send_now]() {
171 if (send_now)
172 messages_.push_front(const_buf);
173 else
174 messages_.push_back(const_buf);
175 if (messages_.size() > 1)
176 {
177 LOG(DEBUG, LOG_TAG) << "outstanding async_write\n";
69178 return;
70
71 active_ = false;
72 }
73
74 try
75 {
76 boost::system::error_code ec;
77 {
78 std::lock_guard<std::mutex> socketLock(socketMutex_);
79 socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
80 if (ec)
81 LOG(ERROR) << "Error in socket shutdown: " << ec.message() << "\n";
82 socket_.close(ec);
83 if (ec)
84 LOG(ERROR) << "Error in socket close: " << ec.message() << "\n";
85179 }
86 if (readerThread_ && readerThread_->joinable())
87 {
88 LOG(DEBUG) << "StreamSession joining readerThread\n";
89 readerThread_->join();
90 }
91 if (writerThread_ && writerThread_->joinable())
92 {
93 LOG(DEBUG) << "StreamSession joining writerThread\n";
94 messages_.abort_wait();
95 writerThread_->join();
96 }
97 }
98 catch (...)
99 {
100 }
101
102 readerThread_ = nullptr;
103 writerThread_ = nullptr;
104 LOG(DEBUG) << "StreamSession stopped\n";
105 }
106
107
108 void StreamSession::socketRead(void* _to, size_t _bytes)
109 {
110 size_t read = 0;
111 do
112 {
113 read += socket_.read_some(boost::asio::buffer((char*)_to + read, _bytes - read));
114 } while (active_ && (read < _bytes));
115 }
116
117
118 void StreamSession::sendAsync(const msg::message_ptr& message, bool sendNow)
180 send_next();
181 });
182 }
183
184
185 void StreamSession::sendAsync(msg::message_ptr message, bool send_now)
119186 {
120187 if (!message)
121188 return;
122189
123 // the writer will take care about old messages
124 while (messages_.size() > 2000) // chunk->getDuration() > 10000)
125 messages_.pop();
126
127 if (sendNow)
128 messages_.push_front(message);
129 else
130 messages_.push(message);
131 }
132
133
134 bool StreamSession::active() const
135 {
136 return active_;
137 }
138
139
140 void StreamSession::setBufferMs(size_t bufferMs)
141 {
142 bufferMs_ = bufferMs;
143 }
144
145
146 bool StreamSession::send(const msg::message_ptr& message)
147 {
148 // TODO on exception: set active = false
149 // LOG(INFO) << "send: " << message->type << ", size: " << message->getSize() << ", id: " << message->id << ", refers: " << message->refersTo << "\n";
150 std::lock_guard<std::mutex> socketLock(socketMutex_);
151 {
152 std::lock_guard<std::mutex> activeLock(activeMutex_);
153 if (!active_)
154 return false;
155 }
156 boost::asio::streambuf streambuf;
157 std::ostream stream(&streambuf);
190 // sendAsync(shared_const_buffer(*message), send_now);
158191 tv t;
159192 message->sent = t;
160 message->serialize(stream);
161 boost::asio::write(socket_, streambuf);
162 // LOG(INFO) << "done: " << message->type << ", size: " << message->size << ", id: " << message->id << ", refers: " << message->refersTo << "\n";
163 return true;
164 }
165
166
167 void StreamSession::getNextMessage()
168 {
169 msg::BaseMessage baseMessage;
170 size_t baseMsgSize = baseMessage.getSize();
171 vector<char> buffer(baseMsgSize);
172 socketRead(&buffer[0], baseMsgSize);
173 baseMessage.deserialize(&buffer[0]);
174
175 if (baseMessage.type > message_type::kLast)
176 {
177 stringstream ss;
178 ss << "unknown message type received: " << baseMessage.type << ", size: " << baseMessage.size;
179 throw std::runtime_error(ss.str().c_str());
180 }
181 else if (baseMessage.size > msg::max_size)
182 {
183 stringstream ss;
184 ss << "received message of type " << baseMessage.type << " to large: " << baseMessage.size;
185 throw std::runtime_error(ss.str().c_str());
186 }
187
188 // LOG(INFO) << "getNextMessage: " << baseMessage.type << ", size: " << baseMessage.size << ", id: " << baseMessage.id << ", refers: " <<
189 // baseMessage.refersTo << "\n";
190 if (baseMessage.size > buffer.size())
191 buffer.resize(baseMessage.size);
192
193 socketRead(&buffer[0], baseMessage.size);
194 tv t;
195 baseMessage.received = t;
196
197 if (active_ && (messageReceiver_ != nullptr))
198 messageReceiver_->onMessageReceived(this, baseMessage, &buffer[0]);
199 }
200
201
202 void StreamSession::reader()
203 {
204 try
205 {
206 while (active_)
207 {
208 getNextMessage();
209 }
210 }
211 catch (const std::exception& e)
212 {
213 SLOG(ERROR) << "Exception in StreamSession::reader(): " << e.what() << endl;
214 }
215
216 if (active_ && (messageReceiver_ != nullptr))
217 messageReceiver_->onDisconnect(this);
218 }
219
220
221 void StreamSession::writer()
222 {
223 try
224 {
225 boost::asio::streambuf streambuf;
226 std::ostream stream(&streambuf);
227 shared_ptr<msg::BaseMessage> message;
228 while (active_)
229 {
230 if (messages_.try_pop(message, std::chrono::milliseconds(500)))
231 {
232 if (bufferMs_ > 0)
233 {
234 const msg::WireChunk* wireChunk = dynamic_cast<const msg::WireChunk*>(message.get());
235 if (wireChunk != nullptr)
236 {
237 chronos::time_point_clk now = chronos::clk::now();
238 size_t age = 0;
239 if (now > wireChunk->start())
240 age = std::chrono::duration_cast<chronos::msec>(now - wireChunk->start()).count();
241 // LOG(DEBUG) << "PCM chunk. Age: " << age << ", buffer: " << bufferMs_ << ", age > buffer: " << (age > bufferMs_) << "\n";
242 if (age > bufferMs_)
243 continue;
244 }
245 }
246 send(message);
247 }
248 }
249 }
250 catch (const std::exception& e)
251 {
252 SLOG(ERROR) << "Exception in StreamSession::writer(): " << e.what() << endl;
253 }
254
255 if (active_ && (messageReceiver_ != nullptr))
256 messageReceiver_->onDisconnect(this);
257 }
193 std::ostringstream oss;
194 message->serialize(oss);
195 sendAsync(shared_const_buffer(oss.str()), send_now);
196 }
197
198
199 void StreamSession::setBufferMs(size_t bufferMs)
200 {
201 bufferMs_ = bufferMs;
202 }
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
1515 along with this program. If not, see <http://www.gnu.org/licenses/>.
1616 ***/
1717
18 #ifndef STREAM_SESSION_H
19 #define STREAM_SESSION_H
18 #ifndef STREAM_SESSION_HPP
19 #define STREAM_SESSION_HPP
2020
2121 #include "common/queue.h"
2222 #include "message/message.hpp"
2424 #include <atomic>
2525 #include <boost/asio.hpp>
2626 #include <condition_variable>
27 #include <deque>
2728 #include <memory>
2829 #include <mutex>
2930 #include <set>
31 #include <sstream>
3032 #include <string>
31 #include <thread>
33 #include <vector>
3234
3335
3436 using boost::asio::ip::tcp;
4648 };
4749
4850
51 // A reference-counted non-modifiable buffer class.
52 // TODO: add overload for messages
53 class shared_const_buffer
54 {
55 public:
56 // Construct from a std::string.
57 explicit shared_const_buffer(const std::string& data) : data_(new std::vector<char>(data.begin(), data.end())), buffer_(boost::asio::buffer(*data_))
58 {
59 }
60
61 // // Construct from a message.
62 // explicit shared_const_buffer(const msg::BaseMessage& message)
63 // {
64 // std::ostringstream oss;
65 // message.serialize(oss);
66
67 // data_ = std::shared_ptr<std::vector<char>>(new std::vector<char>(oss.str().begin(), oss.str().end()));
68 // //std::make_shared<std::vector<char>>(oss.str().begin(), oss.str().end());
69 // buffer_ = boost::asio::buffer(*data_);
70 // }
71
72
73 // Implement the ConstBufferSequence requirements.
74 typedef boost::asio::const_buffer value_type;
75 typedef const boost::asio::const_buffer* const_iterator;
76 const boost::asio::const_buffer* begin() const
77 {
78 return &buffer_;
79 }
80 const boost::asio::const_buffer* end() const
81 {
82 return &buffer_ + 1;
83 }
84
85 private:
86 std::shared_ptr<std::vector<char>> data_;
87 boost::asio::const_buffer buffer_;
88 };
89
90
4991 /// Endpoint for a connected client.
5092 /**
5193 * Endpoint for a connected client.
5294 * Messages are sent to the client with the "send" method.
5395 * Received messages from the client are passed to the MessageReceiver callback
5496 */
55 class StreamSession
97 class StreamSession : public std::enable_shared_from_this<StreamSession>
5698 {
5799 public:
58100 /// ctor. Received message from the client are passed to MessageReceiver
59 StreamSession(MessageReceiver* receiver, tcp::socket&& socket);
101 StreamSession(boost::asio::io_context& ioc, MessageReceiver* receiver, tcp::socket&& socket);
60102 ~StreamSession();
61103 void start();
62104 void stop();
63105
64 /// Sends a message to the client (synchronous)
65 bool send(const msg::message_ptr& message);
106 /// Sends a message to the client (asynchronous)
107 void sendAsync(msg::message_ptr message, bool send_now = false);
66108
67109 /// Sends a message to the client (asynchronous)
68 void sendAsync(const msg::message_ptr& message, bool sendNow = false);
69
70 bool active() const;
110 void sendAsync(shared_const_buffer const_buf, bool send_now = false);
71111
72112 /// Max playout latency. No need to send PCM data that is older than bufferMs
73113 void setBufferMs(size_t bufferMs);
79119 return socket_.remote_endpoint().address().to_string();
80120 }
81121
82 void setPcmStream(PcmStreamPtr pcmStream);
83 const PcmStreamPtr pcmStream() const;
122 void setPcmStream(streamreader::PcmStreamPtr pcmStream);
123 const streamreader::PcmStreamPtr pcmStream() const;
84124
85125 protected:
86 void socketRead(void* _to, size_t _bytes);
87 void getNextMessage();
88 void reader();
89 void writer();
126 void read_next();
127 void send_next();
90128
91 mutable std::mutex activeMutex_;
92 std::atomic<bool> active_;
93
94 std::unique_ptr<std::thread> readerThread_;
95 std::unique_ptr<std::thread> writerThread_;
96 mutable std::mutex socketMutex_;
129 msg::BaseMessage baseMessage_;
130 std::vector<char> buffer_;
131 size_t base_msg_size_;
97132 tcp::socket socket_;
98133 MessageReceiver* messageReceiver_;
99 Queue<std::shared_ptr<msg::BaseMessage>> messages_;
100134 size_t bufferMs_;
101 PcmStreamPtr pcmStream_;
135 streamreader::PcmStreamPtr pcmStream_;
136 boost::asio::io_context::strand strand_;
137 std::deque<shared_const_buffer> messages_;
102138 };
103139
104140
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
2424
2525 using namespace std;
2626
27 static string hex2str(string input)
27 namespace streamreader
28 {
29
30 static constexpr auto LOG_TAG = "AirplayStream";
31
32 namespace
33 {
34 string hex2str(string input)
2835 {
2936 typedef unsigned char byte;
3037 unsigned long x = strtoul(input.c_str(), nullptr, 16);
3138 byte a[] = {byte(x >> 24), byte(x >> 16), byte(x >> 8), byte(x), 0};
3239 return string((char*)a);
3340 }
41 } // namespace
3442
3543 /*
3644 * Expat is used in metadata parsing from Shairport-sync.
4048 * move to Makefile?
4149 */
4250
43 AirplayStream::AirplayStream(PcmListener* pcmListener, const StreamUri& uri) : ProcessStream(pcmListener, uri), port_(5000)
51 AirplayStream::AirplayStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri)
52 : ProcessStream(pcmListener, ioc, uri), port_(5000), pipe_open_timer_(ioc)
4453 {
4554 logStderr_ = true;
4655
5968 params_wo_port_ += " --metadata-pipename " + pipePath_;
6069 params_ = params_wo_port_ + " --port=" + cpt::to_string(port_);
6170
71 #ifdef HAS_EXPAT
72 createParser();
73 #endif
74
75 #if 0
76 // This thread is replaced with asio based "pipeReadLine".
77 // Also seems that this thread was leaking.
78 // The new implementation is _not tested_
6279 pipeReaderThread_ = thread(&AirplayStream::pipeReader, this);
6380 pipeReaderThread_.detach();
81 #endif
6482 }
6583
6684
114132
115133 if (entry_->type == "ssnc" && entry_->code == "mden")
116134 {
117 // LOG(INFO) << "metadata=" << jtag_.dump(4) << "\n";
135 // LOG(INFO, LOG_TAG) << "metadata=" << jtag_.dump(4) << "\n";
118136 setMeta(jtag_);
119137 }
120138 }
121139 #endif
122140
141
142 void AirplayStream::do_connect()
143 {
144 ProcessStream::do_connect();
145 pipeReadLine();
146 }
147
148
149 void AirplayStream::pipeReadLine()
150 {
151 if (!pipe_fd_ || !pipe_fd_->is_open())
152 {
153 try
154 {
155 int fd = open(pipePath_.c_str(), O_RDONLY | O_NONBLOCK);
156 pipe_fd_ = std::make_unique<boost::asio::posix::stream_descriptor>(ioc_, fd);
157 }
158 catch (const std::exception& e)
159 {
160 LOG(ERROR, LOG_TAG) << "Error opening pipe: " << e.what() << "\n";
161 pipe_fd_ = nullptr;
162 wait(pipe_open_timer_, 500ms, [this] { pipeReadLine(); });
163 return;
164 }
165 }
166
167 const std::string delimiter = "\n";
168 boost::asio::async_read_until(*pipe_fd_, streambuf_pipe_, delimiter, [this, delimiter](const std::error_code& ec, std::size_t bytes_transferred) {
169 if (ec)
170 {
171 LOG(ERROR, LOG_TAG) << "Error while reading from pipe: " << ec.message() << "\n";
172 return;
173 }
174
175 // Extract up to the first delimiter.
176 std::string line{buffers_begin(streambuf_pipe_.data()), buffers_begin(streambuf_pipe_.data()) + bytes_transferred - delimiter.length()};
177 if (!line.empty())
178 {
179 if (line.back() == '\r')
180 line.resize(line.size() - 1);
181 #ifdef HAS_EXPAT
182 parse(line);
183 #endif
184 }
185 streambuf_pipe_.consume(bytes_transferred);
186 pipeReadLine();
187 });
188 }
189
190 #if 0
191 // This thread is replaced with asio based "pipeReadLine".
192 // Also seems that this thread was leaking.
193 // The new implementation is _not tested_
123194 void AirplayStream::pipeReader()
124195 {
125196 #ifdef HAS_EXPAT
146217 this_thread::sleep_for(chrono::milliseconds(500));
147218 }
148219 }
220 #endif
149221
150222 void AirplayStream::initExeAndPath(const string& filename)
151223 {
166238 }
167239
168240
169 void AirplayStream::onStderrMsg(const char* buffer, size_t n)
170 {
171 string logmsg = utils::string::trim_copy(string(buffer, n));
172 if (logmsg.empty())
241 void AirplayStream::onStderrMsg(const std::string& line)
242 {
243 if (line.empty())
173244 return;
174 LOG(INFO) << "(" << getName() << ") " << logmsg << "\n";
175 if (logmsg.find("Is another Shairport Sync running on this device") != string::npos)
176 {
177 LOG(ERROR) << "Seem there is another Shairport Sync runnig on port " << port_ << ", switching to port " << port_ + 1 << "\n";
245 LOG(INFO, LOG_TAG) << "(" << getName() << ") " << line << "\n";
246 if (line.find("Is another Shairport Sync running on this device") != string::npos)
247 {
248 LOG(ERROR, LOG_TAG) << "Seem there is another Shairport Sync runnig on port " << port_ << ", switching to port " << port_ + 1 << "\n";
178249 ++port_;
179250 params_ = params_wo_port_ + " --port=" + cpt::to_string(port_);
180251 }
181 else if (logmsg.find("Invalid audio output specified") != string::npos)
182 {
183 LOG(ERROR) << "shairport sync compiled without stdout audio backend\n";
184 LOG(ERROR) << "build with: \"./configure --with-stdout --with-avahi --with-ssl=openssl --with-metadata\"\n";
252 else if (line.find("Invalid audio output specified") != string::npos)
253 {
254 LOG(ERROR, LOG_TAG) << "shairport sync compiled without stdout audio backend\n";
255 LOG(ERROR, LOG_TAG) << "build with: \"./configure --with-stdout --with-avahi --with-ssl=openssl --with-metadata\"\n";
185256 }
186257 }
187258
236307 string value(content, (size_t)length);
237308 self->buf_.append(value);
238309 }
239 #endif
310
311 #endif
312
313 } // namespace streamreader
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
1515 along with this program. If not, see <http://www.gnu.org/licenses/>.
1616 ***/
1717
18 #ifndef AIRPLAY_STREAM_H
19 #define AIRPLAY_STREAM_H
18 #ifndef AIRPLAY_STREAM_HPP
19 #define AIRPLAY_STREAM_HPP
2020
2121 #include "process_stream.hpp"
2222
2727 #ifdef HAS_EXPAT
2828 #include <expat.h>
2929 #endif
30
31 namespace streamreader
32 {
3033
3134 class TageEntry
3235 {
5558 {
5659 public:
5760 /// ctor. Encoded PCM data is passed to the PipeListener
58 AirplayStream(PcmListener* pcmListener, const StreamUri& uri);
61 AirplayStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri);
5962 ~AirplayStream() override;
6063
6164 protected:
6669 std::string buf_;
6770 json jtag_;
6871
69 void pipeReader();
72 void pipeReadLine();
7073 #ifdef HAS_EXPAT
7174 int parse(std::string line);
7275 void createParser();
7376 void push();
7477 #endif
7578
76 void onStderrMsg(const char* buffer, size_t n) override;
79 void do_connect() override;
80 void onStderrMsg(const std::string& line) override;
7781 void initExeAndPath(const std::string& filename) override;
82
7883 size_t port_;
7984 std::string pipePath_;
8085 std::string params_wo_port_;
81 std::thread pipeReaderThread_;
86 std::unique_ptr<boost::asio::posix::stream_descriptor> pipe_fd_;
87 boost::asio::steady_timer pipe_open_timer_;
88 boost::asio::streambuf streambuf_pipe_;
8289
8390 #ifdef HAS_EXPAT
8491 static void XMLCALL element_start(void* userdata, const char* element_name, const char** attr);
8794 #endif
8895 };
8996
97 } // namespace streamreader
9098
9199 #endif
0 /***
1 This file is part of snapcast
2 Copyright (C) 2014-2020 Johannes Pohl
3
4 This program is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, either version 3 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see <http://www.gnu.org/licenses/>.
16 ***/
17
18 #ifndef ASIO_STREAM_HPP
19 #define ASIO_STREAM_HPP
20
21 #include "common/aixlog.hpp"
22 #include "pcm_stream.hpp"
23 #include <atomic>
24 #include <boost/asio.hpp>
25
26 namespace streamreader
27 {
28
29 template <typename ReadStream>
30 class AsioStream : public PcmStream
31 {
32 public:
33 /// ctor. Encoded PCM data is passed to the PipeListener
34 AsioStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri);
35
36 void start() override;
37 void stop() override;
38
39 virtual void connect();
40 virtual void disconnect();
41
42 protected:
43 virtual void do_connect() = 0;
44 virtual void do_disconnect() = 0;
45 virtual void on_connect();
46 virtual void do_read();
47 void check_state();
48
49 template <typename Timer, typename Rep, typename Period>
50 void wait(Timer& timer, const std::chrono::duration<Rep, Period>& duration, std::function<void()> handler);
51
52 std::unique_ptr<msg::PcmChunk> chunk_;
53 timeval tv_chunk_;
54 bool first_;
55 long nextTick_;
56 uint32_t buffer_ms_;
57 boost::asio::steady_timer read_timer_;
58 boost::asio::steady_timer state_timer_;
59 std::unique_ptr<ReadStream> stream_;
60 std::atomic<std::uint64_t> bytes_read_;
61 };
62
63
64 template <typename ReadStream>
65 template <typename Timer, typename Rep, typename Period>
66 void AsioStream<ReadStream>::wait(Timer& timer, const std::chrono::duration<Rep, Period>& duration, std::function<void()> handler)
67 {
68 timer.expires_after(duration);
69 timer.async_wait([handler = std::move(handler)](const boost::system::error_code& ec) {
70 if (ec)
71 {
72 LOG(ERROR, "AsioStream") << "Error during async wait: " << ec.message() << "\n";
73 }
74 else
75 {
76 handler();
77 }
78 });
79 }
80
81
82 template <typename ReadStream>
83 AsioStream<ReadStream>::AsioStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri)
84 : PcmStream(pcmListener, ioc, uri), read_timer_(ioc), state_timer_(ioc)
85 {
86 chunk_ = std::make_unique<msg::PcmChunk>(sampleFormat_, chunk_ms_);
87 bytes_read_ = 0;
88 buffer_ms_ = 50;
89
90 try
91 {
92 buffer_ms_ = cpt::stoi(uri_.getQuery("buffer_ms", cpt::to_string(buffer_ms_)));
93 }
94 catch (...)
95 {
96 }
97 }
98
99
100 template <typename ReadStream>
101 void AsioStream<ReadStream>::check_state()
102 {
103 uint64_t last_read = bytes_read_;
104 wait(state_timer_, std::chrono::milliseconds(500 + chunk_ms_), [this, last_read] {
105 LOG(DEBUG, "AsioStream") << "check state last: " << last_read << ", read: " << bytes_read_ << "\n";
106 if (bytes_read_ != last_read)
107 setState(ReaderState::kPlaying);
108 else
109 setState(ReaderState::kIdle);
110 check_state();
111 });
112 }
113
114
115 template <typename ReadStream>
116 void AsioStream<ReadStream>::start()
117 {
118 encoder_->init(this, sampleFormat_);
119 active_ = true;
120 check_state();
121 connect();
122 }
123
124
125 template <typename ReadStream>
126 void AsioStream<ReadStream>::connect()
127 {
128 do_connect();
129 }
130
131
132 template <typename ReadStream>
133 void AsioStream<ReadStream>::disconnect()
134 {
135 do_disconnect();
136 }
137
138
139 template <typename ReadStream>
140 void AsioStream<ReadStream>::stop()
141 {
142 active_ = false;
143 read_timer_.cancel();
144 state_timer_.cancel();
145 disconnect();
146 }
147
148
149 template <typename ReadStream>
150 void AsioStream<ReadStream>::on_connect()
151 {
152 first_ = true;
153 chronos::systemtimeofday(&tvEncodedChunk_);
154 do_read();
155 }
156
157
158 template <typename ReadStream>
159 void AsioStream<ReadStream>::do_read()
160 {
161 // LOG(DEBUG, "AsioStream") << "do_read\n";
162 boost::asio::async_read(
163 *stream_, boost::asio::buffer(chunk_->payload, chunk_->payloadSize), [this](boost::system::error_code ec, std::size_t length) mutable {
164 if (ec)
165 {
166 LOG(ERROR, "AsioStream") << "Error reading message: " << ec.message() << ", length: " << length << "\n";
167 connect();
168 return;
169 }
170
171 bytes_read_ += length;
172 // LOG(DEBUG, "AsioStream") << "Read: " << length << " bytes\n";
173 // First read after connect. Set the initial read timestamp
174 // the timestamp will be incremented after encoding,
175 // since we do not know how much the encoder actually encoded
176
177 if (!first_)
178 {
179 timeval now;
180 chronos::systemtimeofday(&now);
181 auto stream2systime_diff = chronos::diff<std::chrono::milliseconds>(now, tvEncodedChunk_);
182 if (stream2systime_diff > chronos::sec(5) + chronos::msec(chunk_ms_))
183 {
184 LOG(WARNING, "AsioStream") << "Stream and system time out of sync: " << stream2systime_diff.count() << "ms, resetting stream time.\n";
185 first_ = true;
186 }
187 }
188 if (first_)
189 {
190 first_ = false;
191 chronos::systemtimeofday(&tvEncodedChunk_);
192 nextTick_ = chronos::getTickCount() + buffer_ms_;
193 }
194
195 encoder_->encode(chunk_.get());
196 nextTick_ += chunk_ms_;
197 long currentTick = chronos::getTickCount();
198
199 // Synchronize read to chunk_ms_
200 if (nextTick_ >= currentTick)
201 {
202 read_timer_.expires_after(std::chrono::milliseconds(nextTick_ - currentTick));
203 read_timer_.async_wait([this](const boost::system::error_code& ec) {
204 if (ec)
205 {
206 LOG(ERROR, "AsioStream") << "Error during async wait: " << ec.message() << "\n";
207 }
208 else
209 {
210 do_read();
211 }
212 });
213 return;
214 }
215 // Read took longer, wait for the buffer to fill up
216 else
217 {
218 pcmListener_->onResync(this, currentTick - nextTick_);
219 nextTick_ = currentTick + buffer_ms_;
220 first_ = true;
221 do_read();
222 }
223 });
224 }
225
226 } // namespace streamreader
227
228 #endif
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
2727
2828 using namespace std;
2929
30 namespace streamreader
31 {
32
33 static constexpr auto LOG_TAG = "FileStream";
3034
3135
32 FileStream::FileStream(PcmListener* pcmListener, const StreamUri& uri) : PcmStream(pcmListener, uri)
36 FileStream::FileStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri) : PosixStream(pcmListener, ioc, uri)
3337 {
34 ifs.open(uri_.path.c_str(), std::ifstream::in | std::ifstream::binary);
35 if (!ifs.good())
38 struct stat buffer;
39 if (stat(uri_.path.c_str(), &buffer) != 0)
3640 {
37 LOG(ERROR) << "failed to open PCM file: \"" + uri_.path + "\"\n";
38 throw SnapException("failed to open PCM file: \"" + uri_.path + "\"");
41 throw SnapException("Failed to open PCM file: \"" + uri_.path + "\"");
42 }
43 else if ((buffer.st_mode & S_IFMT) != S_IFREG)
44 {
45 throw SnapException("Not a regular file: \"" + uri_.path + "\"");
3946 }
4047 }
4148
4249
43 FileStream::~FileStream()
50 void FileStream::do_connect()
4451 {
45 ifs.close();
52 LOG(DEBUG, LOG_TAG) << "connect\n";
53 int fd = open(uri_.path.c_str(), O_RDONLY | O_NONBLOCK);
54 stream_ = std::make_unique<boost::asio::posix::stream_descriptor>(ioc_, fd);
55 on_connect();
4656 }
4757
48
49 void FileStream::worker()
50 {
51 timeval tvChunk;
52 std::unique_ptr<msg::PcmChunk> chunk(new msg::PcmChunk(sampleFormat_, pcmReadMs_));
53
54 ifs.seekg(0, ifs.end);
55 size_t length = ifs.tellg();
56 ifs.seekg(0, ifs.beg);
57
58 setState(kPlaying);
59
60 while (active_)
61 {
62 chronos::systemtimeofday(&tvChunk);
63 tvEncodedChunk_ = tvChunk;
64 long nextTick = chronos::getTickCount();
65 try
66 {
67 while (active_)
68 {
69 chunk->timestamp.sec = tvChunk.tv_sec;
70 chunk->timestamp.usec = tvChunk.tv_usec;
71 size_t toRead = chunk->payloadSize;
72 size_t count = 0;
73
74 size_t pos = ifs.tellg();
75 size_t left = length - pos;
76 if (left < toRead)
77 {
78 ifs.read(chunk->payload, left);
79 ifs.seekg(0, ifs.beg);
80 count = left;
81 }
82 ifs.read(chunk->payload + count, toRead - count);
83
84 encoder_->encode(chunk.get());
85 if (!active_)
86 break;
87 nextTick += pcmReadMs_;
88 chronos::addUs(tvChunk, pcmReadMs_ * 1000);
89 long currentTick = chronos::getTickCount();
90
91 if (nextTick >= currentTick)
92 {
93 // LOG(INFO) << "sleep: " << nextTick - currentTick << "\n";
94 if (!sleep(nextTick - currentTick))
95 break;
96 }
97 else
98 {
99 chronos::systemtimeofday(&tvChunk);
100 tvEncodedChunk_ = tvChunk;
101 pcmListener_->onResync(this, currentTick - nextTick);
102 nextTick = currentTick;
103 }
104 }
105 }
106 catch (const std::exception& e)
107 {
108 LOG(ERROR) << "(FileStream) Exception: " << e.what() << std::endl;
109 }
110 }
111 }
58 } // namespace streamreader
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
1515 along with this program. If not, see <http://www.gnu.org/licenses/>.
1616 ***/
1717
18 #ifndef FILE_STREAM_H
19 #define FILE_STREAM_H
18 #ifndef FILE_STREAM_HPP
19 #define FILE_STREAM_HPP
2020
21 #include "pcm_stream.hpp"
22 #include <fstream>
21 #include "posix_stream.hpp"
2322
23 namespace streamreader
24 {
2425
2526 /// Reads and decodes PCM data from a file
2627 /**
2829 * Implements EncoderListener to get the encoded data.
2930 * Data is passed to the PcmListener
3031 */
31 class FileStream : public PcmStream
32 class FileStream : public PosixStream
3233 {
3334 public:
3435 /// ctor. Encoded PCM data is passed to the PipeListener
35 FileStream(PcmListener* pcmListener, const StreamUri& uri);
36 ~FileStream() override;
36 FileStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri);
3737
3838 protected:
39 void worker() override;
40 std::ifstream ifs;
39 void do_connect() override;
4140 };
4241
42 } // namespace streamreader
4343
4444 #endif
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
2525
2626 using namespace std;
2727
28 namespace streamreader
29 {
30
31 static constexpr auto LOG_TAG = "LibrespotStream";
2832
2933
30 LibrespotStream::LibrespotStream(PcmListener* pcmListener, const StreamUri& uri) : ProcessStream(pcmListener, uri)
34 LibrespotStream::LibrespotStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri) : ProcessStream(pcmListener, ioc, uri)
3135 {
3236 sampleFormat_ = SampleFormat("44100:16:2");
3337 uri_.query["sampleformat"] = sampleFormat_.getFormat();
38 // chunk is created in PcmStream ctor, using the (possibly wrongly) configured sample format
39 // we have to recreate it using spotify's native sample format
40 chunk_ = std::make_unique<msg::PcmChunk>(sampleFormat_, chunk_ms_);
41
42 wd_timeout_sec_ = cpt::stoul(uri_.getQuery("wd_timeout", "7800")); ///< 130min
3443
3544 string username = uri_.getQuery("username", "");
3645 string password = uri_.getQuery("password", "");
3746 string cache = uri_.getQuery("cache", "");
38 string volume = uri_.getQuery("volume", "");
47 string volume = uri_.getQuery("volume", "100");
3948 string bitrate = uri_.getQuery("bitrate", "320");
4049 string devicename = uri_.getQuery("devicename", "Snapcast");
4150 string onevent = uri_.getQuery("onevent", "");
51 bool normalize = (uri_.getQuery("normalize", "false") == "true");
4252
4353 if (username.empty() != password.empty())
4454 throw SnapException("missing parameter \"username\" or \"password\" (must provide both, or neither)");
5060 if (!cache.empty())
5161 params_ += " --cache \"" + cache + "\"";
5262 if (!volume.empty())
53 params_ += " --initial-volume \"" + volume + "\"";
63 params_ += " --initial-volume " + volume;
5464 if (!onevent.empty())
5565 params_ += " --onevent \"" + onevent + "\"";
66 if (normalize)
67 params_ += " --enable-volume-normalisation";
68 params_ += " --verbose";
5669
5770 if (uri_.query.find("username") != uri_.query.end())
5871 uri_.query["username"] = "xxx";
5972 if (uri_.query.find("password") != uri_.query.end())
6073 uri_.query["password"] = "xxx";
61 // LOG(INFO) << "params: " << params << "\n";
74 // LOG(INFO, LOG_TAG) << "params: " << params << "\n";
6275 }
63
64
65 LibrespotStream::~LibrespotStream() = default;
6676
6777
6878 void LibrespotStream::initExeAndPath(const std::string& filename)
8797 }
8898
8999
90 void LibrespotStream::onStderrMsg(const char* buffer, size_t n)
100 void LibrespotStream::onStderrMsg(const std::string& line)
91101 {
92102 static bool libreelec_patched = false;
93103 smatch m;
112122 // 2016-11-03 09-00-18 [out] INFO:librespot::main_helper: librespot 6fa4e4d (2016-09-21). Built on 2016-10-27.
113123 // 2016-11-03 09-00-18 [out] INFO:librespot::session: Connecting to AP lon3-accesspoint-a34.ap.spotify.com:443
114124 // 2016-11-03 09-00-18 [out] INFO:librespot::session: Authenticated !
115 watchdog_->trigger();
116 string logmsg = utils::string::trim_copy(string(buffer, n));
117125
118 if ((logmsg.find("allocated stream") == string::npos) && (logmsg.find("Got channel") == string::npos) && (logmsg.find('\0') == string::npos) &&
119 (logmsg.size() > 4))
126 if ((line.find("allocated stream") == string::npos) && (line.find("Got channel") == string::npos) && (line.find('\0') == string::npos) && (line.size() > 4))
120127 {
121 LOG(INFO) << "(" << getName() << ") " << logmsg << "\n";
128 LOG(INFO, LOG_TAG) << "(" << getName() << ") " << line << "\n";
122129 }
123130
124131 // Librespot patch:
131138 if (!libreelec_patched)
132139 {
133140 static regex re_nonpatched("Track \"(.*)\" loaded");
134 if (regex_search(logmsg, m, re_nonpatched))
141 if (regex_search(line, m, re_nonpatched))
135142 {
136 LOG(INFO) << "metadata: <" << m[1] << ">\n";
143 LOG(INFO, LOG_TAG) << "metadata: <" << m[1] << ">\n";
137144
138145 json jtag = {{"TITLE", (string)m[1]}};
139146 setMeta(jtag);
142149
143150 // Parse the patched version
144151 static regex re_patched("metadata:(.*)");
145 if (regex_search(logmsg, m, re_patched))
152 if (regex_search(line, m, re_patched))
146153 {
147 LOG(INFO) << "metadata: <" << m[1] << ">\n";
154 LOG(INFO, LOG_TAG) << "metadata: <" << m[1] << ">\n";
148155
149156 setMeta(json::parse(m[1].str()));
150157 libreelec_patched = true;
151158 }
152159 }
153160
154
155 void LibrespotStream::stderrReader()
156 {
157 watchdog_.reset(new Watchdog(this));
158 /// 130min
159 watchdog_->start(130 * 60 * 1000);
160 ProcessStream::stderrReader();
161 }
162
163
164 void LibrespotStream::onTimeout(const Watchdog* /*watchdog*/, size_t ms)
165 {
166 LOG(ERROR) << "Spotify timeout: " << ms / 1000 << "\n";
167 if (process_)
168 process_->kill();
169 }
161 } // namespace streamreader
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
1515 along with this program. If not, see <http://www.gnu.org/licenses/>.
1616 ***/
1717
18 #ifndef SPOTIFY_STREAM_H
19 #define SPOTIFY_STREAM_H
18 #ifndef SPOTIFY_STREAM_HPP
19 #define SPOTIFY_STREAM_HPP
2020
2121 #include "process_stream.hpp"
22 #include "watchdog.h"
22
23 namespace streamreader
24 {
2325
2426 /// Starts librespot and reads PCM data from stdout
2527 /**
3032 * snapserver -s "spotify:///librespot?name=Spotify&username=<my username>&password=<my password>[&devicename=Snapcast][&bitrate=320][&volume=<volume in
3133 * percent>][&cache=<cache dir>]"
3234 */
33 class LibrespotStream : public ProcessStream, WatchdogListener
35 class LibrespotStream : public ProcessStream
3436 {
3537 public:
3638 /// ctor. Encoded PCM data is passed to the PipeListener
37 LibrespotStream(PcmListener* pcmListener, const StreamUri& uri);
38 ~LibrespotStream() override;
39 LibrespotStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri);
3940
4041 protected:
41 std::unique_ptr<Watchdog> watchdog_;
42
43 void stderrReader() override;
44 void onStderrMsg(const char* buffer, size_t n) override;
42 void onStderrMsg(const std::string& line) override;
4543 void initExeAndPath(const std::string& filename) override;
46
47 void onTimeout(const Watchdog* watchdog, size_t ms) override;
4844 };
4945
46 } // namespace streamreader
5047
5148 #endif
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
2828
2929 using namespace std;
3030
31 namespace streamreader
32 {
3133
3234
33 PcmStream::PcmStream(PcmListener* pcmListener, const StreamUri& uri) : active_(false), pcmListener_(pcmListener), uri_(uri), pcmReadMs_(20), state_(kIdle)
35 PcmStream::PcmStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri)
36 : active_(false), pcmListener_(pcmListener), uri_(uri), chunk_ms_(20), state_(ReaderState::kIdle), ioc_(ioc)
3437 {
35 EncoderFactory encoderFactory;
36 if (uri_.query.find("codec") == uri_.query.end())
38 encoder::EncoderFactory encoderFactory;
39 if (uri_.query.find(kUriCodec) == uri_.query.end())
3740 throw SnapException("Stream URI must have a codec");
38 encoder_.reset(encoderFactory.createEncoder(uri_.query["codec"]));
41 encoder_ = encoderFactory.createEncoder(uri_.query[kUriCodec]);
3942
40 if (uri_.query.find("name") == uri_.query.end())
43 if (uri_.query.find(kUriName) == uri_.query.end())
4144 throw SnapException("Stream URI must have a name");
42 name_ = uri_.query["name"];
45 name_ = uri_.query[kUriName];
4346
44 if (uri_.query.find("sampleformat") == uri_.query.end())
47 if (uri_.query.find(kUriSampleFormat) == uri_.query.end())
4548 throw SnapException("Stream URI must have a sampleformat");
46 sampleFormat_ = SampleFormat(uri_.query["sampleformat"]);
49 sampleFormat_ = SampleFormat(uri_.query[kUriSampleFormat]);
4750 LOG(INFO) << "PcmStream sampleFormat: " << sampleFormat_.getFormat() << "\n";
4851
49 if (uri_.query.find("buffer_ms") != uri_.query.end())
50 pcmReadMs_ = cpt::stoul(uri_.query["buffer_ms"]);
52 if (uri_.query.find(kUriChunkMs) != uri_.query.end())
53 chunk_ms_ = cpt::stoul(uri_.query[kUriChunkMs]);
5154
52 if (uri_.query.find("dryout_ms") != uri_.query.end())
53 dryoutMs_ = cpt::stoul(uri_.query["dryout_ms"]);
54 else
55 dryoutMs_ = 2000;
56
57 // meta_.reset(new msg::StreamTags());
58 // meta_->msg["stream"] = name_;
5955 setMeta(json());
6056 }
6157
10197 LOG(DEBUG) << "PcmStream start: " << sampleFormat_.getFormat() << "\n";
10298 encoder_->init(this, sampleFormat_);
10399 active_ = true;
104 thread_ = thread(&PcmStream::worker, this);
105100 }
106101
107102
108103 void PcmStream::stop()
109104 {
110 if (!active_ && !thread_.joinable())
111 return;
112
113105 active_ = false;
114 cv_.notify_one();
115 if (thread_.joinable())
116 thread_.join();
117 }
118
119
120 bool PcmStream::sleep(int32_t ms)
121 {
122 if (ms < 0)
123 return true;
124 std::unique_lock<std::mutex> lck(mtx_);
125 return (!cv_.wait_for(lck, std::chrono::milliseconds(ms), [this] { return !active_; }));
126106 }
127107
128108
136116 {
137117 if (newState != state_)
138118 {
119 LOG(DEBUG) << "State changed: " << static_cast<int>(state_) << " => " << static_cast<int>(newState) << "\n";
139120 state_ = newState;
140121 if (pcmListener_)
141122 pcmListener_->onStateChanged(this, newState);
143124 }
144125
145126
146 void PcmStream::onChunkEncoded(const Encoder* /*encoder*/, msg::PcmChunk* chunk, double duration)
127 void PcmStream::onChunkEncoded(const encoder::Encoder* /*encoder*/, msg::PcmChunk* chunk, double duration)
147128 {
148129 // LOG(INFO) << "onChunkEncoded: " << duration << " us\n";
149130 if (duration <= 0)
160141 json PcmStream::toJson() const
161142 {
162143 string state("unknown");
163 if (state_ == kIdle)
144 if (state_ == ReaderState::kIdle)
164145 state = "idle";
165 else if (state_ == kPlaying)
146 else if (state_ == ReaderState::kPlaying)
166147 state = "playing";
167 else if (state_ == kDisabled)
148 else if (state_ == ReaderState::kDisabled)
168149 state = "disabled";
169150
170151 json j = {
182163 return meta_;
183164 }
184165
185 void PcmStream::setMeta(json jtag)
166 void PcmStream::setMeta(const json& jtag)
186167 {
187168 meta_.reset(new msg::StreamTags(jtag));
188169 meta_->msg["STREAM"] = name_;
192173 if (pcmListener_)
193174 pcmListener_->onMetaChanged(this);
194175 }
176
177 } // namespace streamreader
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
1515 along with this program. If not, see <http://www.gnu.org/licenses/>.
1616 ***/
1717
18 #ifndef PCM_STREAM_H
19 #define PCM_STREAM_H
18 #ifndef PCM_STREAM_HPP
19 #define PCM_STREAM_HPP
2020
2121 #include "common/json.hpp"
2222 #include "common/sample_format.hpp"
2525 #include "message/stream_tags.hpp"
2626 #include "stream_uri.hpp"
2727 #include <atomic>
28 #include <boost/asio/io_context.hpp>
2829 #include <condition_variable>
2930 #include <map>
3031 #include <mutex>
3132 #include <string>
32 #include <thread>
3333
34
35 namespace streamreader
36 {
3437
3538 class PcmStream;
3639
37 enum ReaderState
40 enum class ReaderState
3841 {
3942 kUnknown = 0,
4043 kIdle = 1,
4144 kPlaying = 2,
4245 kDisabled = 3
4346 };
47
48
49 static constexpr auto kUriCodec = "codec";
50 static constexpr auto kUriName = "name";
51 static constexpr auto kUriSampleFormat = "sampleformat";
52 static constexpr auto kUriChunkMs = "chunk_ms";
4453
4554
4655 /// Callback interface for users of PcmStream
6372 * Implements EncoderListener to get the encoded data.
6473 * Data is passed to the PcmListener
6574 */
66 class PcmStream : public EncoderListener
75 class PcmStream : public encoder::EncoderListener
6776 {
6877 public:
6978 /// ctor. Encoded PCM data is passed to the PcmListener
70 PcmStream(PcmListener* pcmListener, const StreamUri& uri);
79 PcmStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri);
7180 virtual ~PcmStream();
7281
7382 virtual void start();
7483 virtual void stop();
7584
7685 /// Implementation of EncoderListener::onChunkEncoded
77 void onChunkEncoded(const Encoder* encoder, msg::PcmChunk* chunk, double duration) override;
86 void onChunkEncoded(const encoder::Encoder* encoder, msg::PcmChunk* chunk, double duration) override;
7887 virtual std::shared_ptr<msg::CodecHeader> getHeader();
7988
8089 virtual const StreamUri& getUri() const;
8392 virtual const SampleFormat& getSampleFormat() const;
8493
8594 std::shared_ptr<msg::StreamTags> getMeta() const;
86 void setMeta(json j);
95 void setMeta(const json& j);
8796
8897 virtual ReaderState getState() const;
8998 virtual json toJson() const;
9099
91100
92101 protected:
93 std::condition_variable cv_;
94 std::mutex mtx_;
95 std::thread thread_;
96102 std::atomic<bool> active_;
97103
98 virtual void worker() = 0;
99 virtual bool sleep(int32_t ms);
100104 void setState(const ReaderState& newState);
101105
102106 timeval tvEncodedChunk_;
103107 PcmListener* pcmListener_;
104108 StreamUri uri_;
105109 SampleFormat sampleFormat_;
106 size_t pcmReadMs_;
107 size_t dryoutMs_;
108 std::unique_ptr<Encoder> encoder_;
110 size_t chunk_ms_;
111 std::unique_ptr<encoder::Encoder> encoder_;
109112 std::string name_;
110113 ReaderState state_;
111114 std::shared_ptr<msg::StreamTags> meta_;
115 boost::asio::io_context& ioc_;
112116 };
113117
118 } // namespace streamreader
114119
115120 #endif
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
2424 #include "common/aixlog.hpp"
2525 #include "common/snap_exception.hpp"
2626 #include "common/str_compat.hpp"
27 #include "encoder/encoder_factory.hpp"
2827 #include "pipe_stream.hpp"
2928
3029
3130 using namespace std;
3231
32 namespace streamreader
33 {
34
35 static constexpr auto LOG_TAG = "PipeStream";
3336
3437
35 PipeStream::PipeStream(PcmListener* pcmListener, const StreamUri& uri) : PcmStream(pcmListener, uri), fd_(-1)
38 PipeStream::PipeStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri) : PosixStream(pcmListener, ioc, uri)
3639 {
3740 umask(0);
3841 string mode = uri_.getQuery("mode", "create");
3942
40 LOG(INFO) << "PipeStream mode: " << mode << "\n";
43 LOG(INFO, LOG_TAG) << "PipeStream mode: " << mode << "\n";
4144 if ((mode != "read") && (mode != "create"))
4245 throw SnapException("create mode for fifo must be \"read\" or \"create\"");
4346
4952 }
5053
5154
52 PipeStream::~PipeStream()
55 void PipeStream::do_connect()
5356 {
54 if (fd_ != -1)
55 close(fd_);
57 LOG(DEBUG, LOG_TAG) << "connect\n";
58 int fd = open(uri_.path.c_str(), O_RDONLY | O_NONBLOCK);
59 stream_ = std::make_unique<boost::asio::posix::stream_descriptor>(ioc_, fd);
60 on_connect();
5661 }
57
58
59 void PipeStream::worker()
60 {
61 timeval tvChunk;
62 std::unique_ptr<msg::PcmChunk> chunk(new msg::PcmChunk(sampleFormat_, pcmReadMs_));
63 string lastException = "";
64
65 while (active_)
66 {
67 if (fd_ != -1)
68 close(fd_);
69 fd_ = open(uri_.path.c_str(), O_RDONLY | O_NONBLOCK);
70 chronos::systemtimeofday(&tvChunk);
71 tvEncodedChunk_ = tvChunk;
72 long nextTick = chronos::getTickCount();
73 int idleBytes = 0;
74 int maxIdleBytes = sampleFormat_.rate * sampleFormat_.frameSize * dryoutMs_ / 1000;
75 try
76 {
77 if (fd_ == -1)
78 throw SnapException("failed to open fifo: \"" + uri_.path + "\"");
79
80 while (active_)
81 {
82 chunk->timestamp.sec = tvChunk.tv_sec;
83 chunk->timestamp.usec = tvChunk.tv_usec;
84 int toRead = chunk->payloadSize;
85 int len = 0;
86 do
87 {
88 int count = read(fd_, chunk->payload + len, toRead - len);
89 if (count < 0 && idleBytes < maxIdleBytes)
90 {
91 memset(chunk->payload + len, 0, toRead - len);
92 idleBytes += toRead - len;
93 len += toRead - len;
94 continue;
95 }
96 if (count < 0)
97 {
98 setState(kIdle);
99 if (!sleep(100))
100 break;
101 }
102 else if (count == 0)
103 throw SnapException("end of file");
104 else
105 {
106 len += count;
107 idleBytes = 0;
108 }
109 } while ((len < toRead) && active_);
110
111 if (!active_)
112 break;
113
114 /// TODO: use less raw pointers, make this encoding more transparent
115 encoder_->encode(chunk.get());
116
117 if (!active_)
118 break;
119
120 nextTick += pcmReadMs_;
121 chronos::addUs(tvChunk, pcmReadMs_ * 1000);
122 long currentTick = chronos::getTickCount();
123
124 if (nextTick >= currentTick)
125 {
126 setState(kPlaying);
127 if (!sleep(nextTick - currentTick))
128 break;
129 }
130 else
131 {
132 chronos::systemtimeofday(&tvChunk);
133 tvEncodedChunk_ = tvChunk;
134 pcmListener_->onResync(this, currentTick - nextTick);
135 nextTick = currentTick;
136 }
137
138 lastException = "";
139 }
140 }
141 catch (const std::exception& e)
142 {
143 if (lastException != e.what())
144 {
145 LOG(ERROR) << "(PipeStream) Exception: " << e.what() << std::endl;
146 lastException = e.what();
147 }
148 if (!sleep(100))
149 break;
150 }
151 }
15262 }
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
1515 along with this program. If not, see <http://www.gnu.org/licenses/>.
1616 ***/
1717
18 #ifndef PIPE_STREAM_H
19 #define PIPE_STREAM_H
18 #ifndef PIPE_STREAM_HPP
19 #define PIPE_STREAM_HPP
2020
21 #include "pcm_stream.hpp"
21 #include "posix_stream.hpp"
2222
23 namespace streamreader
24 {
25
26 using boost::asio::posix::stream_descriptor;
2327
2428
2529 /// Reads and decodes PCM data from a named pipe
2832 * Implements EncoderListener to get the encoded data.
2933 * Data is passed to the PcmListener
3034 */
31 class PipeStream : public PcmStream
35 class PipeStream : public PosixStream
3236 {
3337 public:
3438 /// ctor. Encoded PCM data is passed to the PipeListener
35 PipeStream(PcmListener* pcmListener, const StreamUri& uri);
36 ~PipeStream() override;
39 PipeStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri);
3740
3841 protected:
39 void worker() override;
40 int fd_;
42 void do_connect() override;
4143 };
4244
45 } // namespace streamreader
4346
4447 #endif
0 /***
1 This file is part of snapcast
2 Copyright (C) 2014-2020 Johannes Pohl
3
4 This program is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, either version 3 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see <http://www.gnu.org/licenses/>.
16 ***/
17
18 #include <cerrno>
19 #include <fcntl.h>
20 #include <memory>
21 #include <sys/stat.h>
22 #include <unistd.h>
23
24 #include "common/aixlog.hpp"
25 #include "common/snap_exception.hpp"
26 #include "common/str_compat.hpp"
27 #include "posix_stream.hpp"
28
29
30 using namespace std;
31 using namespace std::chrono_literals;
32
33 namespace streamreader
34 {
35
36 static constexpr auto LOG_TAG = "PosixStream";
37
38
39 PosixStream::PosixStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri) : AsioStream<stream_descriptor>(pcmListener, ioc, uri)
40 {
41 if (uri_.query.find("dryout_ms") != uri_.query.end())
42 dryout_ms_ = cpt::stoul(uri_.query["dryout_ms"]);
43 else
44 dryout_ms_ = 2000;
45 }
46
47
48 void PosixStream::connect()
49 {
50 if (!active_)
51 return;
52
53 idle_bytes_ = 0;
54 max_idle_bytes_ = sampleFormat_.rate * sampleFormat_.frameSize * dryout_ms_ / 1000;
55
56 try
57 {
58 do_connect();
59 }
60 catch (const std::exception& e)
61 {
62 LOG(ERROR, LOG_TAG) << "Connect exception: " << e.what() << "\n";
63 wait(read_timer_, 100ms, [this] { connect(); });
64 }
65 }
66
67
68 void PosixStream::do_disconnect()
69 {
70 if (stream_ && stream_->is_open())
71 stream_->close();
72 }
73
74
75 void PosixStream::do_read()
76 {
77 try
78 {
79 if (!stream_->is_open())
80 throw SnapException("failed to open stream: \"" + uri_.path + "\"");
81
82 int toRead = chunk_->payloadSize;
83 int len = 0;
84 do
85 {
86 int count = read(stream_->native_handle(), chunk_->payload + len, toRead - len);
87 if (count < 0 && idle_bytes_ < max_idle_bytes_)
88 {
89 // nothing to read for a longer time now, set the chunk to silent
90 LOG(DEBUG, LOG_TAG) << "count < 0: " << errno
91 << " && idleBytes < maxIdleBytes, ms: " << 1000 * chunk_->payloadSize / (sampleFormat_.rate * sampleFormat_.frameSize)
92 << "\n";
93 memset(chunk_->payload + len, 0, toRead - len);
94 idle_bytes_ += toRead - len;
95 len += toRead - len;
96 break;
97 }
98 else if (count < 0)
99 {
100 // nothing to read, try again (chunk_ms_ / 2) later
101 wait(read_timer_, std::chrono::milliseconds(chunk_ms_ / 2), [this] { do_read(); });
102 return;
103 }
104 else if (count == 0)
105 {
106 throw SnapException("end of file");
107 }
108 else
109 {
110 // LOG(DEBUG) << "count: " << count << "\n";
111 len += count;
112 bytes_read_ += len;
113 idle_bytes_ = 0;
114 }
115 } while (len < toRead);
116
117 if (first_)
118 {
119 first_ = false;
120 chronos::systemtimeofday(&tvEncodedChunk_);
121 nextTick_ = chronos::getTickCount() + buffer_ms_;
122 }
123 encoder_->encode(chunk_.get());
124 nextTick_ += chunk_ms_;
125 long currentTick = chronos::getTickCount();
126
127 if (nextTick_ >= currentTick)
128 {
129 // synchronize reads to an interval of chunk_ms_
130 wait(read_timer_, std::chrono::milliseconds(nextTick_ - currentTick), [this] { do_read(); });
131 return;
132 }
133 else
134 {
135 // reading chunk_ms_ took longer than chunk_ms_
136 pcmListener_->onResync(this, currentTick - nextTick_);
137 nextTick_ = currentTick + buffer_ms_;
138 first_ = true;
139 do_read();
140 }
141
142 lastException_ = "";
143 }
144 catch (const std::exception& e)
145 {
146 if (lastException_ != e.what())
147 {
148 LOG(ERROR, LOG_TAG) << "Exception: " << e.what() << std::endl;
149 lastException_ = e.what();
150 }
151 disconnect();
152 wait(read_timer_, 100ms, [this] { connect(); });
153 }
154 }
155
156 } // namespace streamreader
0 /***
1 This file is part of snapcast
2 Copyright (C) 2014-2020 Johannes Pohl
3
4 This program is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, either version 3 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see <http://www.gnu.org/licenses/>.
16 ***/
17
18 #ifndef POSIX_STREAM_HPP
19 #define POSIX_STREAM_HPP
20
21 #include "asio_stream.hpp"
22
23 namespace streamreader
24 {
25
26 using boost::asio::posix::stream_descriptor;
27
28
29 /// Reads and decodes PCM data from a file descriptor
30 /**
31 * Reads PCM from a file descriptor and passes the data to an encoder.
32 * Implements EncoderListener to get the encoded data.
33 * Data is passed to the PcmListener
34 */
35 class PosixStream : public AsioStream<stream_descriptor>
36 {
37 public:
38 /// ctor. Encoded PCM data is passed to the PipeListener
39 PosixStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri);
40
41 protected:
42 void connect() override;
43 void do_disconnect() override;
44 void do_read() override;
45 std::string lastException_;
46 size_t dryout_ms_;
47 int idle_bytes_;
48 int max_idle_bytes_;
49 };
50
51 } // namespace streamreader
52
53 #endif
+0
-242
server/streamreader/process.hpp less more
0 /***
1 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
3 This program is free software: you can redistribute it and/or modify
4 it under the terms of the GNU General Public License as published by
5 the Free Software Foundation, either version 3 of the License, or
6 (at your option) any later version.
7 This program is distributed in the hope that it will be useful,
8 but WITHOUT ANY WARRANTY; without even the implied warranty of
9 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 GNU General Public License for more details.
11 You should have received a copy of the GNU General Public License
12 along with this program. If not, see <http://www.gnu.org/licenses/>.
13 ***/
14
15 #ifndef TINY_PROCESS_LIBRARY_HPP_
16 #define TINY_PROCESS_LIBRARY_HPP_
17
18 #include <cstdlib>
19 #include <mutex>
20 #include <signal.h>
21 #include <string>
22 #include <sys/wait.h>
23 #include <unistd.h>
24
25 // Forked from: https://github.com/eidheim/tiny-process-library
26 // Copyright (c) 2015-2016 Ole Christian Eidheim
27 // Thanks, Christian :-)
28
29 /// Create a new process given command and run path.
30 /// Thus, at the moment, if read_stdout==nullptr, read_stderr==nullptr and open_stdin==false,
31 /// the stdout, stderr and stdin are sent to the parent process instead.
32 /// Compile with -DMSYS_PROCESS_USE_SH to run command using "sh -c [command]" on Windows as well.
33 class Process
34 {
35
36 public:
37 typedef int fd_type;
38
39 Process(const std::string& command, const std::string& path = "") : closed(true)
40 {
41 open(command, path);
42 }
43
44 ~Process()
45 {
46 close_fds();
47 }
48
49 /// Get the process id of the started process.
50 pid_t getPid()
51 {
52 return pid;
53 }
54
55 /// Write to stdin. Convenience function using write(const char *, size_t).
56 bool write(const std::string& data)
57 {
58 return write(data.c_str(), data.size());
59 }
60
61 /// Wait until process is finished, and return exit status.
62 int get_exit_status()
63 {
64 if (pid <= 0)
65 return -1;
66
67 int exit_status;
68 waitpid(pid, &exit_status, 0);
69 {
70 std::lock_guard<std::mutex> lock(close_mutex);
71 closed = true;
72 }
73 close_fds();
74
75 if (exit_status >= 256)
76 exit_status = exit_status >> 8;
77
78 return exit_status;
79 }
80
81 /// Write to stdin.
82 bool write(const char* bytes, size_t n)
83 {
84 std::lock_guard<std::mutex> lock(stdin_mutex);
85 if (::write(stdin_fd, bytes, n) >= 0)
86 return true;
87 else
88 return false;
89 }
90
91 /// Close stdin. If the process takes parameters from stdin, use this to notify that all parameters have been sent.
92 void close_stdin()
93 {
94 std::lock_guard<std::mutex> lock(stdin_mutex);
95 if (pid > 0)
96 close(stdin_fd);
97 }
98
99 /// Kill the process.
100 void kill(bool force = false)
101 {
102 std::lock_guard<std::mutex> lock(close_mutex);
103 if (pid > 0 && !closed)
104 {
105 if (force)
106 ::kill(-pid, SIGTERM);
107 else
108 ::kill(-pid, SIGINT);
109 }
110 }
111
112 /// Kill a given process id. Use kill(bool force) instead if possible.
113 static void kill(pid_t id, bool force = false)
114 {
115 if (id <= 0)
116 return;
117 if (force)
118 ::kill(-id, SIGTERM);
119 else
120 ::kill(-id, SIGINT);
121 }
122
123 fd_type getStdout()
124 {
125 return stdout_fd;
126 }
127
128 fd_type getStderr()
129 {
130 return stderr_fd;
131 }
132
133 fd_type getStdin()
134 {
135 return stdin_fd;
136 }
137
138
139 private:
140 pid_t pid;
141 bool closed;
142 std::mutex close_mutex;
143 std::mutex stdin_mutex;
144
145 fd_type stdout_fd, stderr_fd, stdin_fd;
146
147 void closePipe(int pipefd[2])
148 {
149 close(pipefd[0]);
150 close(pipefd[1]);
151 }
152
153 pid_t open(const std::string& command, const std::string& path)
154 {
155 int stdin_p[2], stdout_p[2], stderr_p[2];
156
157 if (pipe(stdin_p) != 0)
158 return -1;
159
160 if (pipe(stdout_p) != 0)
161 {
162 closePipe(stdin_p);
163 return -1;
164 }
165
166 if (pipe(stderr_p) != 0)
167 {
168 closePipe(stdin_p);
169 closePipe(stdout_p);
170 return -1;
171 }
172
173 pid = fork();
174
175 if (pid < 0)
176 {
177 closePipe(stdin_p);
178 closePipe(stdout_p);
179 closePipe(stderr_p);
180 return pid;
181 }
182 else if (pid == 0)
183 {
184 dup2(stdin_p[0], 0);
185 dup2(stdout_p[1], 1);
186 dup2(stderr_p[1], 2);
187
188 closePipe(stdin_p);
189 closePipe(stdout_p);
190 closePipe(stderr_p);
191
192 // Based on http://stackoverflow.com/a/899533/3808293
193 int fd_max = sysconf(_SC_OPEN_MAX);
194 for (int fd = 3; fd < fd_max; fd++)
195 close(fd);
196
197 setpgid(0, 0);
198
199 if (!path.empty())
200 {
201 auto path_escaped = path;
202 size_t pos = 0;
203 // Based on https://www.reddit.com/r/cpp/comments/3vpjqg/a_new_platform_independent_process_library_for_c11/cxsxyb7
204 while ((pos = path_escaped.find('\'', pos)) != std::string::npos)
205 {
206 path_escaped.replace(pos, 1, "'\\''");
207 pos += 4;
208 }
209 execl("/bin/sh", "sh", "-c", ("cd '" + path_escaped + "' && " + command).c_str(), NULL);
210 }
211 else
212 execl("/bin/sh", "sh", "-c", command.c_str(), NULL);
213
214 _exit(EXIT_FAILURE);
215 }
216
217 close(stdin_p[0]);
218 close(stdout_p[1]);
219 close(stderr_p[1]);
220
221 stdin_fd = stdin_p[1];
222 stdout_fd = stdout_p[0];
223 stderr_fd = stderr_p[0];
224
225 closed = false;
226 return pid;
227 }
228
229
230 void close_fds()
231 {
232 close_stdin();
233 if (pid > 0)
234 {
235 close(stdout_fd);
236 close(stderr_fd);
237 }
238 }
239 };
240
241 #endif // TINY_PROCESS_LIBRARY_HPP_
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
2828
2929 using namespace std;
3030
31 namespace streamreader
32 {
33
34 static constexpr auto LOG_TAG = "ProcessStream";
3135
3236
33 ProcessStream::ProcessStream(PcmListener* pcmListener, const StreamUri& uri) : PcmStream(pcmListener, uri), path_(""), process_(nullptr)
37 ProcessStream::ProcessStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri) : PosixStream(pcmListener, ioc, uri)
3438 {
3539 params_ = uri_.getQuery("params");
36 logStderr_ = (uri_.getQuery("logStderr", "false") == "true");
37 }
38
39
40 ProcessStream::~ProcessStream()
41 {
42 if (process_)
43 process_->kill();
40 wd_timeout_sec_ = cpt::stoul(uri_.getQuery("wd_timeout", "0"));
41 LOG(DEBUG, LOG_TAG) << "Watchdog timeout: " << wd_timeout_sec_ << "\n";
42 logStderr_ = (uri_.getQuery("log_stderr", "false") == "true");
4443 }
4544
4645
9695 }
9796
9897
99 void ProcessStream::start()
98 void ProcessStream::do_connect()
10099 {
100 if (!active_)
101 return;
101102 initExeAndPath(uri_.path);
102 PcmStream::start();
103 LOG(DEBUG, LOG_TAG) << "Launching: '" << path_ + exe_ << "', with params: '" << params_ << "', in path: '" << path_ << "'\n";
104
105 pipe_stdout_ = bp::pipe();
106 // could use bp::async_pipe, but this is broken in boost 1.72:
107 // https://github.com/boostorg/process/issues/116
108 pipe_stderr_ = bp::pipe();
109 // stdout pipe should not block
110 int flags = fcntl(pipe_stdout_.native_source(), F_GETFL, 0);
111 fcntl(pipe_stdout_.native_source(), F_SETFL, flags | O_NONBLOCK);
112
113 process_ = bp::child(path_ + exe_ + " " + params_, bp::std_out > pipe_stdout_, bp::std_err > pipe_stderr_, bp::start_dir = path_);
114 stream_ = make_unique<stream_descriptor>(ioc_, pipe_stdout_.native_source());
115 stream_stderr_ = make_unique<stream_descriptor>(ioc_, pipe_stderr_.native_source());
116 on_connect();
117 if (wd_timeout_sec_ > 0)
118 {
119 watchdog_ = make_unique<Watchdog>(ioc_, this);
120 watchdog_->start(std::chrono::seconds(wd_timeout_sec_));
121 }
122 else
123 {
124 watchdog_ = nullptr;
125 }
126 stderrReadLine();
103127 }
104128
105129
106 void ProcessStream::stop()
130 void ProcessStream::do_disconnect()
107131 {
108 if (process_)
109 process_->kill();
110 PcmStream::stop();
111
112 /// thread is detached, so it is not joinable
113 if (stderrReaderThread_.joinable())
114 stderrReaderThread_.join();
132 if (process_.running())
133 ::kill(-process_.native_handle(), SIGINT);
115134 }
116135
117136
118 void ProcessStream::onStderrMsg(const char* buffer, size_t n)
137 void ProcessStream::onStderrMsg(const std::string& line)
119138 {
120139 if (logStderr_)
121140 {
122 string line = utils::string::trim_copy(string(buffer, n));
123 if ((line.find('\0') == string::npos) && !line.empty())
124 LOG(INFO) << "(" << getName() << ") " << line << "\n";
141 LOG(INFO, LOG_TAG) << "(" << getName() << ") " << line << "\n";
125142 }
126143 }
127144
128145
129 void ProcessStream::stderrReader()
146 void ProcessStream::stderrReadLine()
130147 {
131 size_t buffer_size = 8192;
132 auto buffer = std::unique_ptr<char[]>(new char[buffer_size]);
133 ssize_t n;
134 stringstream message;
135 while (active_ && (n = read(process_->getStderr(), buffer.get(), buffer_size)) > 0)
136 onStderrMsg(buffer.get(), n);
148 const std::string delimiter = "\n";
149 boost::asio::async_read_until(*stream_stderr_, streambuf_stderr_, delimiter, [this, delimiter](const std::error_code& ec, std::size_t bytes_transferred) {
150 if (ec)
151 {
152 LOG(ERROR, LOG_TAG) << "Error while reading from stderr: " << ec.message() << "\n";
153 return;
154 }
155
156 if (watchdog_)
157 watchdog_->trigger();
158
159 // Extract up to the first delimiter.
160 std::string line{buffers_begin(streambuf_stderr_.data()), buffers_begin(streambuf_stderr_.data()) + bytes_transferred - delimiter.length()};
161 if (!line.empty())
162 {
163 if (line.back() == '\r')
164 line.resize(line.size() - 1);
165 onStderrMsg(line);
166 }
167 streambuf_stderr_.consume(bytes_transferred);
168 stderrReadLine();
169 });
137170 }
138171
139172
140 void ProcessStream::worker()
173 void ProcessStream::onTimeout(const Watchdog& /*watchdog*/, std::chrono::milliseconds ms)
141174 {
142 timeval tvChunk;
143 std::unique_ptr<msg::PcmChunk> chunk(new msg::PcmChunk(sampleFormat_, pcmReadMs_));
144 setState(kPlaying);
145 string lastException = "";
175 LOG(ERROR, LOG_TAG) << "Watchdog timeout: " << ms.count() / 1000 << "s\n";
176 if (process_)
177 ::kill(-process_.native_handle(), SIGINT);
178 }
146179
147 while (active_)
148 {
149 process_.reset(new Process(path_ + exe_ + " " + params_, path_));
150 int flags = fcntl(process_->getStdout(), F_GETFL, 0);
151 fcntl(process_->getStdout(), F_SETFL, flags | O_NONBLOCK);
152
153 stderrReaderThread_ = thread(&ProcessStream::stderrReader, this);
154 stderrReaderThread_.detach();
155
156 chronos::systemtimeofday(&tvChunk);
157 tvEncodedChunk_ = tvChunk;
158 long nextTick = chronos::getTickCount();
159 int idleBytes = 0;
160 int maxIdleBytes = sampleFormat_.rate * sampleFormat_.frameSize * dryoutMs_ / 1000;
161 try
162 {
163 while (active_)
164 {
165 chunk->timestamp.sec = tvChunk.tv_sec;
166 chunk->timestamp.usec = tvChunk.tv_usec;
167 int toRead = chunk->payloadSize;
168 int len = 0;
169 do
170 {
171 int count = read(process_->getStdout(), chunk->payload + len, toRead - len);
172 if (count < 0 && idleBytes < maxIdleBytes)
173 {
174 memset(chunk->payload + len, 0, toRead - len);
175 idleBytes += toRead - len;
176 len += toRead - len;
177 continue;
178 }
179 if (count < 0)
180 {
181 setState(kIdle);
182 if (!sleep(100))
183 break;
184 }
185 else if (count == 0)
186 throw SnapException("end of file");
187 else
188 {
189 len += count;
190 idleBytes = 0;
191 }
192 } while ((len < toRead) && active_);
193
194 if (!active_)
195 break;
196
197 encoder_->encode(chunk.get());
198
199 if (!active_)
200 break;
201
202 nextTick += pcmReadMs_;
203 chronos::addUs(tvChunk, pcmReadMs_ * 1000);
204 long currentTick = chronos::getTickCount();
205
206 if (nextTick >= currentTick)
207 {
208 setState(kPlaying);
209 if (!sleep(nextTick - currentTick))
210 break;
211 }
212 else
213 {
214 chronos::systemtimeofday(&tvChunk);
215 tvEncodedChunk_ = tvChunk;
216 pcmListener_->onResync(this, currentTick - nextTick);
217 nextTick = currentTick;
218 }
219
220 lastException = "";
221 }
222 }
223 catch (const std::exception& e)
224 {
225 if (lastException != e.what())
226 {
227 LOG(ERROR) << "(PipeStream) Exception: " << e.what() << std::endl;
228 lastException = e.what();
229 }
230 process_->kill();
231 if (!sleep(30000))
232 break;
233 }
234 }
235 }
180 } // namespace streamreader
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
1515 along with this program. If not, see <http://www.gnu.org/licenses/>.
1616 ***/
1717
18 #ifndef PROCESS_STREAM_H
19 #define PROCESS_STREAM_H
18 #ifndef PROCESS_STREAM_HPP
19 #define PROCESS_STREAM_HPP
2020
21 #include <boost/process.hpp>
2122 #include <memory>
2223 #include <string>
2324
24 #include "pcm_stream.hpp"
25 #include "process.hpp"
25 #include "posix_stream.hpp"
26 #include "watchdog.hpp"
2627
28
29 namespace bp = boost::process;
30
31
32 namespace streamreader
33 {
2734
2835 /// Starts an external process and reads and PCM data from stdout
2936 /**
3138 * Implements EncoderListener to get the encoded data.
3239 * Data is passed to the PcmListener
3340 */
34 class ProcessStream : public PcmStream
41 class ProcessStream : public PosixStream, public WatchdogListener
3542 {
3643 public:
3744 /// ctor. Encoded PCM data is passed to the PipeListener
38 ProcessStream(PcmListener* pcmListener, const StreamUri& uri);
39 ~ProcessStream() override;
40
41 void start() override;
42 void stop() override;
45 ProcessStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri);
46 ~ProcessStream() override = default;
4347
4448 protected:
49 void do_connect() override;
50 void do_disconnect() override;
51
4552 std::string exe_;
4653 std::string path_;
4754 std::string params_;
48 std::unique_ptr<Process> process_;
49 std::thread stderrReaderThread_;
55 bp::pipe pipe_stdout_;
56 bp::pipe pipe_stderr_;
57 bp::child process_;
58
5059 bool logStderr_;
60 boost::asio::streambuf streambuf_stderr_;
61 std::unique_ptr<stream_descriptor> stream_stderr_;
5162
52 void worker() override;
53 virtual void stderrReader();
54 virtual void onStderrMsg(const char* buffer, size_t n);
63 // void worker() override;
64 virtual void stderrReadLine();
65 virtual void onStderrMsg(const std::string& line);
5566 virtual void initExeAndPath(const std::string& filename);
5667
5768 bool fileExists(const std::string& filename);
5869 std::string findExe(const std::string& filename);
70
71 size_t wd_timeout_sec_;
72 std::unique_ptr<Watchdog> watchdog_;
73 void onTimeout(const Watchdog& watchdog, std::chrono::milliseconds ms) override;
5974 };
6075
76 } // namespace streamreader
6177
6278 #endif
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
2525 #include "librespot_stream.hpp"
2626 #include "pipe_stream.hpp"
2727 #include "process_stream.hpp"
28 #include "tcp_stream.hpp"
2829
2930
3031 using namespace std;
3132
33 namespace streamreader
34 {
3235
33 StreamManager::StreamManager(PcmListener* pcmListener, const std::string& defaultSampleFormat, const std::string& defaultCodec, size_t defaultReadBufferMs)
34 : pcmListener_(pcmListener), sampleFormat_(defaultSampleFormat), codec_(defaultCodec), readBufferMs_(defaultReadBufferMs)
36 StreamManager::StreamManager(PcmListener* pcmListener, boost::asio::io_context& ioc, const std::string& defaultSampleFormat, const std::string& defaultCodec,
37 size_t defaultChunkBufferMs)
38 : pcmListener_(pcmListener), sampleFormat_(defaultSampleFormat), codec_(defaultCodec), chunkBufferMs_(defaultChunkBufferMs), ioc_(ioc)
3539 {
3640 }
3741
4044 {
4145 StreamUri streamUri(uri);
4246
43 if (streamUri.query.find("sampleformat") == streamUri.query.end())
44 streamUri.query["sampleformat"] = sampleFormat_;
47 if (streamUri.query.find(kUriSampleFormat) == streamUri.query.end())
48 streamUri.query[kUriSampleFormat] = sampleFormat_;
4549
46 if (streamUri.query.find("codec") == streamUri.query.end())
47 streamUri.query["codec"] = codec_;
50 if (streamUri.query.find(kUriCodec) == streamUri.query.end())
51 streamUri.query[kUriCodec] = codec_;
4852
49 if (streamUri.query.find("buffer_ms") == streamUri.query.end())
50 streamUri.query["buffer_ms"] = cpt::to_string(readBufferMs_);
53 if (streamUri.query.find(kUriChunkMs) == streamUri.query.end())
54 streamUri.query[kUriChunkMs] = cpt::to_string(chunkBufferMs_);
5155
5256 // LOG(DEBUG) << "\nURI: " << streamUri.uri << "\nscheme: " << streamUri.scheme << "\nhost: "
5357 // << streamUri.host << "\npath: " << streamUri.path << "\nfragment: " << streamUri.fragment << "\n";
5862
5963 if (streamUri.scheme == "pipe")
6064 {
61 stream = make_shared<PipeStream>(pcmListener_, streamUri);
65 stream = make_shared<PipeStream>(pcmListener_, ioc_, streamUri);
6266 }
6367 else if (streamUri.scheme == "file")
6468 {
65 stream = make_shared<FileStream>(pcmListener_, streamUri);
69 stream = make_shared<FileStream>(pcmListener_, ioc_, streamUri);
6670 }
6771 else if (streamUri.scheme == "process")
6872 {
69 stream = make_shared<ProcessStream>(pcmListener_, streamUri);
73 stream = make_shared<ProcessStream>(pcmListener_, ioc_, streamUri);
7074 }
7175 else if ((streamUri.scheme == "spotify") || (streamUri.scheme == "librespot"))
7276 {
73 stream = make_shared<LibrespotStream>(pcmListener_, streamUri);
77 stream = make_shared<LibrespotStream>(pcmListener_, ioc_, streamUri);
7478 }
7579 else if (streamUri.scheme == "airplay")
7680 {
77 stream = make_shared<AirplayStream>(pcmListener_, streamUri);
81 stream = make_shared<AirplayStream>(pcmListener_, ioc_, streamUri);
82 }
83 else if (streamUri.scheme == "tcp")
84 {
85 stream = make_shared<TcpStream>(pcmListener_, ioc_, streamUri);
7886 }
7987 else
8088 {
8391
8492 if (stream)
8593 {
86 for (auto s : streams_)
94 for (const auto& s : streams_)
8795 {
8896 if (s->getName() == stream->getName())
8997 throw SnapException("Stream with name \"" + stream->getName() + "\" already exists");
97105
98106 void StreamManager::removeStream(const std::string& name)
99107 {
100 if (streams_.empty())
101 return;
102 for (std::vector<PcmStreamPtr>::iterator iter = streams_.begin(); iter != streams_.end(); ++iter)
108 auto iter = std::find_if(streams_.begin(), streams_.end(), [&name](const PcmStreamPtr& stream) { return stream->getName() == name; });
109 if (iter != streams_.end())
103110 {
104 auto s = *iter;
105 if (s->getName() == name)
106 {
107 s->stop();
108 streams_.erase(iter);
109 break;
110 }
111 (*iter)->stop();
112 streams_.erase(iter);
111113 }
112114 }
115
113116
114117 const std::vector<PcmStreamPtr>& StreamManager::getStreams()
115118 {
139142
140143 void StreamManager::start()
141144 {
142 for (auto stream : streams_)
145 for (const auto& stream : streams_)
143146 stream->start();
144147 }
145148
146149
147150 void StreamManager::stop()
148151 {
149 for (auto stream : streams_)
150 stream->stop();
152 for (const auto& stream : streams_)
153 if (stream)
154 stream->stop();
151155 }
152156
153157
158162 result.push_back(stream->toJson());
159163 return result;
160164 }
165
166 } // namespace streamreader
0 #ifndef PCM_READER_FACTORY_H
1 #define PCM_READER_FACTORY_H
0 /***
1 This file is part of snapcast
2 Copyright (C) 2014-2020 Johannes Pohl
3
4 This program is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, either version 3 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see <http://www.gnu.org/licenses/>.
16 ***/
17
18 #ifndef STREAM_MANAGER_HPP
19 #define STREAM_MANAGER_HPP
220
321 #include "pcm_stream.hpp"
22 #include <boost/asio/io_context.hpp>
423 #include <memory>
524 #include <string>
625 #include <vector>
26
27 namespace streamreader
28 {
729
830 typedef std::shared_ptr<PcmStream> PcmStreamPtr;
931
1032 class StreamManager
1133 {
1234 public:
13 StreamManager(PcmListener* pcmListener, const std::string& defaultSampleFormat, const std::string& defaultCodec, size_t defaultReadBufferMs = 20);
35 StreamManager(PcmListener* pcmListener, boost::asio::io_context& ioc, const std::string& defaultSampleFormat, const std::string& defaultCodec,
36 size_t defaultChunkBufferMs = 20);
1437
1538 PcmStreamPtr addStream(const std::string& uri);
1639 void removeStream(const std::string& name);
2649 PcmListener* pcmListener_;
2750 std::string sampleFormat_;
2851 std::string codec_;
29 size_t readBufferMs_;
52 size_t chunkBufferMs_;
53 boost::asio::io_context& ioc_;
3054 };
3155
56 } // namespace streamreader
3257
3358 #endif
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
2424 using namespace std;
2525 namespace strutils = utils::string;
2626
27 namespace streamreader
28 {
2729
2830 StreamUri::StreamUri(const std::string& uri)
2931 {
3032 parse(uri);
3133 }
32
3334
3435
3536 void StreamUri::parse(const std::string& streamUri)
6364
6465 pos = tmp.find('/');
6566 if (pos == string::npos)
66 throw invalid_argument("missing path separator: '/'");
67 {
68 pos = tmp.find('?');
69 if (pos == string::npos)
70 pos = tmp.length();
71 }
72
6773 host = strutils::trim_copy(tmp.substr(0, pos));
6874 tmp = tmp.substr(pos);
6975 path = tmp;
140146 return iter->second;
141147 return def;
142148 }
149 }
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
1515 along with this program. If not, see <http://www.gnu.org/licenses/>.
1616 ***/
1717
18 #ifndef READER_URI_H
19 #define READER_URI_H
18 #ifndef STREAM_URI_HPP
19 #define STREAM_URI_HPP
2020
2121 #include <map>
2222 #include <string>
2626
2727 using json = nlohmann::json;
2828
29 namespace streamreader
30 {
2931
3032 // scheme:[//[user:password@]host[:port]][/]path[?query][#fragment]
3133 struct StreamUri
5557 std::string toString() const;
5658 };
5759
60 } // namespace streamreader
5861
5962 #endif
0 /***
1 This file is part of snapcast
2 Copyright (C) 2014-2020 Johannes Pohl
3
4 This program is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, either version 3 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see <http://www.gnu.org/licenses/>.
16 ***/
17
18 #include <cerrno>
19 #include <fcntl.h>
20 #include <memory>
21 #include <sys/stat.h>
22 #include <unistd.h>
23
24 #include "common/aixlog.hpp"
25 #include "common/snap_exception.hpp"
26 #include "common/str_compat.hpp"
27 #include "common/utils/string_utils.hpp"
28 #include "encoder/encoder_factory.hpp"
29 #include "tcp_stream.hpp"
30
31
32 using namespace std;
33
34 namespace streamreader
35 {
36
37 TcpStream::TcpStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri)
38 : AsioStream<tcp::socket>(pcmListener, ioc, uri), reconnect_timer_(ioc)
39 {
40 host_ = uri_.host;
41 auto host_port = utils::string::split(host_, ':');
42 port_ = 4953;
43 if (host_port.size() == 2)
44 {
45 host_ = host_port[0];
46 port_ = cpt::stoi(host_port[1], port_);
47 }
48
49 auto mode = uri_.getQuery("mode", "server");
50 if (mode == "server")
51 is_server_ = true;
52 else if (mode == "client")
53 is_server_ = false;
54 else
55 throw SnapException("mode must be 'client' or 'server'");
56
57 port_ = cpt::stoi(uri_.getQuery("port", cpt::to_string(port_)), port_);
58
59 LOG(INFO) << "TcpStream host: " << host_ << ", port: " << port_ << ", is server: " << is_server_ << "\n";
60 if (is_server_)
61 acceptor_ = make_unique<tcp::acceptor>(ioc_, tcp::endpoint(boost::asio::ip::address::from_string(host_), port_));
62 }
63
64
65 void TcpStream::do_connect()
66 {
67 if (!active_)
68 return;
69
70 if (is_server_)
71 {
72 acceptor_->async_accept([this](boost::system::error_code ec, tcp::socket socket) {
73 if (!ec)
74 {
75 LOG(DEBUG) << "New client connection\n";
76 stream_ = make_unique<tcp::socket>(move(socket));
77 on_connect();
78 }
79 else
80 {
81 LOG(ERROR) << "Accept failed: " << ec.message() << "\n";
82 }
83 });
84 }
85 else
86 {
87 stream_ = make_unique<tcp::socket>(ioc_);
88 boost::asio::ip::tcp::endpoint endpoint(boost::asio::ip::address::from_string(host_), port_);
89 stream_->async_connect(endpoint, [this](const boost::system::error_code& ec) {
90 if (!ec)
91 {
92 LOG(DEBUG) << "Connected\n";
93 on_connect();
94 }
95 else
96 {
97 LOG(DEBUG) << "Connect failed: " << ec.message() << "\n";
98 wait(reconnect_timer_, 1s, [this] { connect(); });
99 }
100 });
101 }
102 }
103
104
105 void TcpStream::do_disconnect()
106 {
107 if (stream_)
108 stream_->close();
109 if (acceptor_)
110 acceptor_->cancel();
111 reconnect_timer_.cancel();
112 }
113 } // namespace streamreader
0 /***
1 This file is part of snapcast
2 Copyright (C) 2014-2020 Johannes Pohl
3
4 This program is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, either version 3 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see <http://www.gnu.org/licenses/>.
16 ***/
17
18 #ifndef TCP_STREAM_HPP
19 #define TCP_STREAM_HPP
20
21 #include "asio_stream.hpp"
22
23 using boost::asio::ip::tcp;
24
25 namespace streamreader
26 {
27
28 /// Reads and decodes PCM data from a named pipe
29 /**
30 * Reads PCM from a named pipe and passes the data to an encoder.
31 * Implements EncoderListener to get the encoded data.
32 * Data is passed to the PcmListener
33 */
34 class TcpStream : public AsioStream<tcp::socket>
35 {
36 public:
37 /// ctor. Encoded PCM data is passed to the PipeListener
38 TcpStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri);
39
40 protected:
41 void do_connect() override;
42 void do_disconnect() override;
43 std::unique_ptr<tcp::acceptor> acceptor_;
44 std::string host_;
45 size_t port_;
46 bool is_server_;
47 boost::asio::steady_timer reconnect_timer_;
48 };
49
50 } // namespace streamreader
51
52 #endif
00 /***
11 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
2 Copyright (C) 2014-2020 Johannes Pohl
33
44 This program is free software: you can redistribute it and/or modify
55 it under the terms of the GNU General Public License as published by
1515 along with this program. If not, see <http://www.gnu.org/licenses/>.
1616 ***/
1717
18 #include "watchdog.h"
18 #include "watchdog.hpp"
19 #include "common/aixlog.hpp"
1920 #include <chrono>
21
22
23 static constexpr auto LOG_TAG = "Watchdog";
2024
2125
2226 using namespace std;
2327
28 namespace streamreader
29 {
2430
25 Watchdog::Watchdog(WatchdogListener* listener) : listener_(listener), thread_(nullptr), active_(false)
31 Watchdog::Watchdog(boost::asio::io_context& ioc, WatchdogListener* listener) : timer_(ioc), listener_(listener)
2632 {
2733 }
2834
3339 }
3440
3541
36 void Watchdog::start(size_t timeoutMs)
42 void Watchdog::start(const std::chrono::milliseconds& timeout)
3743 {
38 timeoutMs_ = timeoutMs;
39 if (!thread_ || !active_)
40 {
41 active_ = true;
42 thread_.reset(new thread(&Watchdog::worker, this));
43 }
44 else
45 trigger();
44 LOG(INFO, LOG_TAG) << "Starting watchdog, timeout: " << std::chrono::duration_cast<std::chrono::seconds>(timeout).count() << "s\n";
45 timeout_ms_ = timeout;
46 trigger();
4647 }
4748
4849
4950 void Watchdog::stop()
5051 {
51 active_ = false;
52 trigger();
53 if (thread_ && thread_->joinable())
54 thread_->join();
55 thread_ = nullptr;
52 timer_.cancel();
5653 }
5754
5855
5956 void Watchdog::trigger()
6057 {
61 // std::unique_lock<std::mutex> lck(mtx_);
62 cv_.notify_one();
58 timer_.cancel();
59 timer_.expires_after(timeout_ms_);
60 timer_.async_wait([this](const boost::system::error_code& ec) {
61 if (!ec)
62 {
63 LOG(INFO, LOG_TAG) << "Timed out: " << std::chrono::duration_cast<std::chrono::seconds>(timeout_ms_).count() << "s\n";
64 listener_->onTimeout(*this, timeout_ms_);
65 }
66 });
6367 }
6468
65
66 void Watchdog::worker()
67 {
68 while (active_)
69 {
70 std::unique_lock<std::mutex> lck(mtx_);
71 if (cv_.wait_for(lck, std::chrono::milliseconds(timeoutMs_)) == std::cv_status::timeout)
72 {
73 if (listener_)
74 {
75 listener_->onTimeout(this, timeoutMs_);
76 break;
77 }
78 }
79 }
80 active_ = false;
81 }
69 } // namespace streamreader
+0
-62
server/streamreader/watchdog.h less more
0 /***
1 This file is part of snapcast
2 Copyright (C) 2014-2019 Johannes Pohl
3
4 This program is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, either version 3 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see <http://www.gnu.org/licenses/>.
16 ***/
17
18 #ifndef WATCH_DOG_H
19 #define WATCH_DOG_H
20
21 #include <atomic>
22 #include <condition_variable>
23 #include <memory>
24 #include <mutex>
25 #include <thread>
26
27
28 class Watchdog;
29
30
31 class WatchdogListener
32 {
33 public:
34 virtual void onTimeout(const Watchdog* watchdog, size_t ms) = 0;
35 };
36
37
38 /// Watchdog
39 class Watchdog
40 {
41 public:
42 Watchdog(WatchdogListener* listener = nullptr);
43 virtual ~Watchdog();
44
45 void start(size_t timeoutMs);
46 void stop();
47 void trigger();
48
49 private:
50 WatchdogListener* listener_;
51 std::condition_variable cv_;
52 std::mutex mtx_;
53 std::unique_ptr<std::thread> thread_;
54 size_t timeoutMs_;
55 std::atomic<bool> active_;
56
57 void worker();
58 };
59
60
61 #endif
0 /***
1 This file is part of snapcast
2 Copyright (C) 2014-2020 Johannes Pohl
3
4 This program is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, either version 3 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see <http://www.gnu.org/licenses/>.
16 ***/
17
18 #ifndef WATCH_DOG_HPP
19 #define WATCH_DOG_HPP
20
21 #include <boost/asio.hpp>
22 #include <memory>
23
24 namespace streamreader
25 {
26
27 class Watchdog;
28
29
30 class WatchdogListener
31 {
32 public:
33 virtual void onTimeout(const Watchdog& watchdog, std::chrono::milliseconds ms) = 0;
34 };
35
36
37 /// Watchdog
38 class Watchdog
39 {
40 public:
41 Watchdog(boost::asio::io_context& ioc, WatchdogListener* listener = nullptr);
42 virtual ~Watchdog();
43
44 void start(const std::chrono::milliseconds& timeout);
45 void stop();
46 void trigger();
47
48 private:
49 boost::asio::steady_timer timer_;
50 WatchdogListener* listener_;
51 std::chrono::milliseconds timeout_ms_;
52 };
53
54 } // namespace streamreader
55
56 #endif