New upstream version 0.18.0
Felix Geyer
4 years ago
6 | 6 | [submodule "externals/tremor"] |
7 | 7 | path = externals/tremor |
8 | 8 | url = https://git.xiph.org/tremor.git |
9 | [submodule "externals/opus"] | |
10 | path = externals/opus | |
11 | url = https://git.xiph.org/opus.git |
14 | 14 | - sourceline: 'ppa:mhier/libboost-latest' |
15 | 15 | - ubuntu-toolchain-r-test |
16 | 16 | 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 | |
18 | 18 | |
19 | 19 | - os: linux |
20 | 20 | compiler: gcc |
26 | 26 | - sourceline: 'ppa:mhier/libboost-latest' |
27 | 27 | - ubuntu-toolchain-r-test |
28 | 28 | 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 | |
30 | 30 | |
31 | 31 | - os: linux |
32 | 32 | compiler: gcc |
38 | 38 | - sourceline: 'ppa:mhier/libboost-latest' |
39 | 39 | - ubuntu-toolchain-r-test |
40 | 40 | 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 | |
42 | 42 | |
43 | 43 | - os: linux |
44 | 44 | compiler: gcc |
50 | 50 | - sourceline: 'ppa:mhier/libboost-latest' |
51 | 51 | - ubuntu-toolchain-r-test |
52 | 52 | 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 | |
54 | 54 | |
55 | 55 | - os: linux |
56 | 56 | compiler: gcc |
62 | 62 | - sourceline: 'ppa:mhier/libboost-latest' |
63 | 63 | - ubuntu-toolchain-r-test |
64 | 64 | 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 | |
66 | 66 | |
67 | 67 | - os: linux |
68 | 68 | compiler: gcc |
74 | 74 | - sourceline: 'ppa:mhier/libboost-latest' |
75 | 75 | - ubuntu-toolchain-r-test |
76 | 76 | 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 | |
78 | 78 | |
79 | 79 | |
80 | 80 | - os: linux |
89 | 89 | - sourceline: 'ppa:mhier/libboost-latest' |
90 | 90 | - llvm-toolchain-trusty-3.9 |
91 | 91 | 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 | |
93 | 93 | |
94 | 94 | - os: linux |
95 | 95 | compiler: clang |
103 | 103 | - sourceline: 'ppa:mhier/libboost-latest' |
104 | 104 | - llvm-toolchain-trusty-4.0 |
105 | 105 | 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 | |
107 | 107 | |
108 | 108 | - os: linux |
109 | 109 | compiler: clang |
117 | 117 | - sourceline: 'ppa:mhier/libboost-latest' |
118 | 118 | - llvm-toolchain-trusty-5.0 |
119 | 119 | 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 | |
121 | 121 | |
122 | 122 | - os: linux |
123 | 123 | compiler: clang |
131 | 131 | - llvm-toolchain-trusty-6.0 |
132 | 132 | - ubuntu-toolchain-r-test |
133 | 133 | 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 | |
135 | 135 | |
136 | 136 | - os: linux |
137 | 137 | compiler: clang |
145 | 145 | - llvm-toolchain-trusty-7 |
146 | 146 | - ubuntu-toolchain-r-test |
147 | 147 | 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 | |
149 | 149 | |
150 | 150 | # build on osx |
151 | 151 | - os: osx |
152 | 152 | osx_image: xcode9.4 |
153 | 153 | 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" | |
155 | 155 | |
156 | 156 | - os: osx |
157 | 157 | osx_image: xcode10.3 |
158 | 158 | env: |
159 | - MATRIX_EVAL="brew update && brew upgrade boost && brew install flac libvorbis" | |
159 | - MATRIX_EVAL="brew update && brew install flac opus libvorbis" | |
160 | 160 | |
161 | 161 | - os: osx |
162 | 162 | osx_image: xcode11 |
163 | 163 | env: |
164 | - MATRIX_EVAL="brew update && brew install flac libvorbis" | |
164 | - MATRIX_EVAL="brew update && brew install flac opus libvorbis" | |
165 | 165 | |
166 | 166 | before_install: |
167 | 167 | - eval "${MATRIX_EVAL}" |
172 | 172 | |
173 | 173 | - mkdir build |
174 | 174 | - 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 |
0 | 0 | cmake_minimum_required(VERSION 3.2) |
1 | 1 | |
2 | project(snapcast LANGUAGES CXX VERSION 0.16.0) | |
2 | project(snapcast LANGUAGES CXX VERSION 0.18.0) | |
3 | 3 | set(PROJECT_DESCRIPTION "Multi-room client-server audio player") |
4 | 4 | set(PROJECT_URL "https://github.com/badaix/snapcast") |
5 | 5 | |
13 | 13 | option(BUILD_WITH_FLAC "Build with FLAC support" ON) |
14 | 14 | option(BUILD_WITH_VORBIS "Build with VORBIS support" ON) |
15 | 15 | option(BUILD_WITH_TREMOR "Build with vorbis using TREMOR" ON) |
16 | option(BUILD_WITH_OPUS "Build with OPUS support" ON) | |
16 | 17 | option(BUILD_WITH_AVAHI "Build with AVAHI support" ON) |
17 | 18 | |
18 | 19 | |
76 | 77 | #message(STATUS "Architecture: ${ARCH}") |
77 | 78 | #message(STATUS "System processor: ${CMAKE_SYSTEM_PROCESSOR}") |
78 | 79 | |
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) | |
84 | 81 | |
85 | 82 | INCLUDE(TestBigEndian) |
86 | 83 | TEST_BIG_ENDIAN(BIGENDIAN) |
172 | 169 | if(BUILD_WITH_VORBIS) |
173 | 170 | pkg_search_module(VORBISENC vorbisenc) |
174 | 171 | if (VORBISENC_FOUND) |
175 | add_definitions("-DHAS_VORBISENC") | |
172 | add_definitions("-DHAS_VORBIS_ENC") | |
176 | 173 | endif(VORBISENC_FOUND) |
177 | 174 | endif() |
178 | 175 | |
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") | |
180 | 185 | |
181 | 186 | add_subdirectory(common) |
182 | 187 |
2 | 2 | |
3 | 3 | ![Snapcast](https://raw.githubusercontent.com/badaix/snapcast/master/doc/Snapcast_800.png) |
4 | 4 | |
5 | **S**y**n**chronous **a**udio **p**layer | |
6 | ||
5 | **S**y**n**chronous **a**udio **p**layer | |
6 | ||
7 | 7 | [![Build Status]( |
8 | 8 | https://travis-ci.org/badaix/snapcast.svg?branch=master)](https://travis-ci.org/badaix/snapcast) |
9 | 9 | [![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) | |
10 | 11 | |
11 | 12 | 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. | |
13 | 14 | |
14 | 15 | How does it work |
15 | 16 | ---------------- |
17 | 18 | * **PCM** lossless uncompressed |
18 | 19 | * **FLAC** lossless compressed [default] |
19 | 20 | * **Vorbis** lossy compression |
21 | * **Opus** lossy low-latency compression | |
20 | 22 | |
21 | 23 | 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. | |
23 | 25 | 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 |
24 | 26 | * skipping parts or whole chunks |
25 | 27 | * playing silence |
27 | 29 | |
28 | 30 | Typically the deviation is smaller than 1ms. |
29 | 31 | |
32 | For more information on the binary protocol, please see the [documentation](doc/binary_protocol.md). | |
33 | ||
30 | 34 | Installation |
31 | 35 | ------------ |
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 | |
33 | 37 | |
34 | 38 | ### Installation from source |
35 | 39 | Please follow this [guide](doc/build.md) to build Snapcast for |
39 | 43 | * [Android](doc/build.md#android-cross-compile) |
40 | 44 | * [OpenWrt](doc/build.md#openwrtlede-cross-compile) |
41 | 45 | * [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) | |
43 | 47 | |
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` | |
46 | 50 | Install the package: |
47 | 51 | |
48 | 52 | $ sudo dpkg -i snapclient_0.x.x_armhf.deb |
55 | 59 | |
56 | 60 | $ opkg install snapclient_0.x.x_ar71xx.ipk |
57 | 61 | |
58 | On Alpine Linux (testing repository) do: | |
62 | On Alpine Linux do: | |
59 | 63 | |
60 | 64 | $ 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 | |
61 | 73 | |
62 | 74 | On Gentoo Linux do: |
63 | 75 | |
64 | 76 | $ 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 | ||
66 | 84 | SnapOS |
67 | 85 | ------ |
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. | |
69 | 87 | |
70 | 88 | Configuration |
71 | 89 | ------------- |
81 | 99 | ``` |
82 | 100 | |
83 | 101 | 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 | ||
85 | 103 | stream = pipe:///tmp/snapfifo?name=Radio&mode=read" |
86 | ||
104 | ||
87 | 105 | Test |
88 | 106 | ---- |
89 | 107 | You can test your installation by copying random data into the server's fifo file |
91 | 109 | $ sudo cat /dev/urandom > /tmp/snapfifo |
92 | 110 | |
93 | 111 | 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: | |
95 | 113 | |
96 | 114 | ``` |
97 | 115 | [stream] |
98 | 116 | stream = file:///home/user/Musik/Some%20wave%20file.wav?name=test |
99 | 117 | ``` |
100 | 118 | |
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: | |
102 | 120 | |
103 | 121 | #The last number is the audio output with 1 being the 3.5 jack, 2 being HDMI and 0 being auto. |
104 | 122 | $ amixer cset numid=3 1 |
105 | 123 | |
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: | |
107 | 125 | https://www.raspberrypi.org/documentation/configuration/wireless/wireless-cli.md |
108 | 126 | |
109 | 127 | Control |
120 | 138 | ![Snapcast for Android](https://raw.githubusercontent.com/badaix/snapcast/master/doc/snapcast_android_scaled.png) |
121 | 139 | |
122 | 140 | 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. | |
124 | 142 | Once installed, you can use any mobile device, laptop, desktop, or browser. |
125 | 143 | |
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. | |
127 | 145 | |
128 | 146 | 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. |
129 | 147 | |
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. | |
131 | 149 | |
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. | |
133 | 151 | |
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. | |
135 | 153 | |
136 | 154 | Setup of audio players/server |
137 | 155 | ----------------------------- |
158 | 176 | Roadmap |
159 | 177 | ------- |
160 | 178 | 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,... | |
162 | 180 | - [X] **Android client** JSON-RPC client and Snapclient |
163 | 181 | - [X] **Streams** Support multiple streams |
164 | 182 | - [X] **Debian packages** prebuild deb packages |
168 | 186 | - [X] **Groups** support multiple Groups of clients ("Zones") |
169 | 187 | - [ ] **JSON-RPC** Possibility to add, remove, rename streams |
170 | 188 | - [ ] **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 |
0 | 0 | # 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_ | |
1 | 59 | |
2 | 60 | ## Version 0.16.0 |
3 | 61 | |
22 | 80 | - Snapcast depends on boost::asio and boost::beast (header only) |
23 | 81 | - Tidy up code base |
24 | 82 | - 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_ | |
27 | 86 | |
28 | 87 | ## Version 0.15.0 |
29 | 88 |
67 | 67 | list(APPEND CLIENT_INCLUDE ${FLAC_INCLUDE_DIRS}) |
68 | 68 | endif (FLAC_FOUND) |
69 | 69 | |
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 | ||
70 | 76 | include_directories(${CLIENT_INCLUDE}) |
71 | 77 | add_executable(snapclient ${CLIENT_SOURCES}) |
72 | 78 | target_link_libraries(snapclient ${CLIENT_LIBRARIES}) |
0 | 0 | # This file is part of snapcast |
1 | # Copyright (C) 2014-2019 Johannes Pohl | |
1 | # Copyright (C) 2014-2020 Johannes Pohl | |
2 | 2 | # |
3 | 3 | # This program is free software: you can redistribute it and/or modify |
4 | 4 | # it under the terms of the GNU General Public License as published by |
13 | 13 | # You should have received a copy of the GNU General Public License |
14 | 14 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
15 | 15 | |
16 | VERSION = 0.16.0 | |
16 | VERSION = 0.18.0 | |
17 | 17 | BIN = snapclient |
18 | 18 | |
19 | 19 | ifeq ($(TARGET), FREEBSD) |
29 | 29 | TARGET_DIR ?= /usr |
30 | 30 | endif |
31 | 31 | |
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 | |
39 | 47 | |
40 | 48 | |
41 | 49 | ifneq (,$(TARGET)) |
49 | 57 | ifeq ($(TARGET), ANDROID) |
50 | 58 | |
51 | 59 | CXX = $(PROGRAM_PREFIX)clang++ |
52 | STRIP = $(PROGRAM_PREFIX)strip | |
53 | 60 | 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++ | |
55 | 62 | OBJ += player/opensl_player.o |
56 | 63 | |
57 | 64 | else ifeq ($(TARGET), OPENWRT) |
58 | 65 | |
59 | STRIP = echo | |
60 | 66 | CXXFLAGS += -pthread -DNO_CPP11_STRING -DHAS_TREMOR -DHAS_ALSA -DHAS_AVAHI -DHAS_DAEMON |
61 | 67 | LDFLAGS += -lasound -lvorbisidec -lavahi-client -lavahi-common -latomic |
62 | 68 | OBJ += ../common/daemon.o player/alsa_player.o browseZeroConf/browse_avahi.o |
70 | 76 | else ifeq ($(TARGET), MACOS) |
71 | 77 | |
72 | 78 | CXX = g++ |
73 | STRIP = strip | |
74 | 79 | CXXFLAGS += -DHAS_COREAUDIO -DHAS_VORBIS -DFREEBSD -DHAS_BONJOUR -DHAS_DAEMON -I/usr/local/include -Wno-unused-local-typedef -Wno-deprecated |
75 | 80 | LDFLAGS += -lvorbis -lFLAC -L/usr/local/lib -framework AudioToolbox -framework CoreAudio -framework CoreFoundation -framework IOKit |
76 | 81 | OBJ += ../common/daemon.o player/coreaudio_player.o browseZeroConf/browse_bonjour.o |
78 | 83 | else |
79 | 84 | |
80 | 85 | CXX = g++ |
81 | STRIP = strip | |
82 | 86 | CXXFLAGS += -pthread -DHAS_VORBIS -DHAS_ALSA -DHAS_AVAHI -DHAS_DAEMON |
83 | 87 | LDFLAGS += -lrt -lasound -lvorbis -lavahi-client -lavahi-common -latomic |
84 | 88 | OBJ += ../common/daemon.o player/alsa_player.o browseZeroConf/browse_avahi.o |
101 | 105 | else ifeq ($(ARCH), mips) |
102 | 106 | $(eval CXXFLAGS:=$(CXXFLAGS) -DIS_BIG_ENDIAN) |
103 | 107 | $(eval PROGRAM_PREFIX:=$(NDK_DIR)/bin/mipsel-linux-android-) |
104 | else | |
108 | else ifeq ($(ARCH), arm) | |
105 | 109 | $(eval CXXFLAGS:=$(CXXFLAGS) -march=armv7) |
106 | 110 | $(eval PROGRAM_PREFIX:=$(NDK_DIR)/bin/arm-linux-androideabi-) |
111 | else ifeq ($(ARCH), arm64) | |
112 | $(eval PROGRAM_PREFIX:=$(NDK_DIR)/bin/aarch64-linux-android-) | |
107 | 113 | endif |
108 | 114 | endif |
109 | 115 | |
110 | 116 | $(BIN): $(OBJ) |
111 | 117 | $(CXX) $(CXXFLAGS) -o $(BIN) $(OBJ) $(LDFLAGS) |
112 | $(STRIP) $(BIN) | |
113 | 118 | |
114 | 119 | %.o: %.cpp |
115 | 120 | $(CXX) $(CXXFLAGS) -c $< -o $@ |
126 | 131 | |
127 | 132 | install: |
128 | 133 | 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) | |
130 | 135 | 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 | |
132 | 137 | launchctl load /Library/LaunchAgents/de.badaix.snapcast.$(BIN).plist |
133 | 138 | |
134 | 139 | else |
151 | 156 | endif |
152 | 157 | |
153 | 158 | 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) | |
155 | 160 | install -D -g root -o root $(BIN).1 $(TARGET_DIR)/share/man/man1/$(BIN).1 |
156 | 161 | |
157 | 162 | installsystemd: |
158 | 163 | @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); \ | |
161 | 166 | systemctl daemon-reload; \ |
162 | 167 | systemctl enable $(BIN); \ |
163 | 168 | systemctl start $(BIN); \ |
164 | 169 | |
165 | 170 | installsysv: |
166 | 171 | @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); \ | |
169 | 174 | update-rc.d $(BIN) defaults; \ |
170 | 175 | /etc/init.d/$(BIN) start; \ |
171 | 176 |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
166 | 166 | } |
167 | 167 | } |
168 | 168 | |
169 | bool BrowseBonjour::browse(const string& serviceName, mDNSResult& result, int timeout) | |
169 | bool BrowseBonjour::browse(const string& serviceName, mDNSResult& result, int /*timeout*/) | |
170 | 170 | { |
171 | 171 | result.valid = false; |
172 | 172 | // Discover |
174 | 174 | { |
175 | 175 | DNSServiceHandle service(new DNSServiceRef(NULL)); |
176 | 176 | 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, | |
178 | 178 | const char* serviceName, const char* regtype, const char* replyDomain, void* context) { |
179 | 179 | auto replyCollection = static_cast<deque<mDNSReply>*>(context); |
180 | 180 | |
191 | 191 | { |
192 | 192 | DNSServiceHandle service(new DNSServiceRef(NULL)); |
193 | 193 | 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)); | |
204 | 204 | |
205 | 205 | runService(service); |
206 | 206 | } |
214 | 214 | { |
215 | 215 | resultCollection[i].port = resolve.port; |
216 | 216 | 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) { | |
219 | 219 | auto result = static_cast<mDNSResult*>(context); |
220 | 220 | |
221 | 221 | result->host = string(hostname); |
0 | 0 | #/bin/sh |
1 | 1 | |
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]" | |
4 | 4 | exit |
5 | 5 | fi |
6 | 6 | |
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" | |
9 | 9 | exit |
10 | 10 | fi |
11 | 11 | |
12 | 12 | if [ -n "$NDK_DIR_ARM" ]; then |
13 | 13 | export NDK_DIR="$NDK_DIR_ARM" |
14 | 14 | 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" | |
16 | 22 | fi |
17 | 23 | |
18 | 24 | if [ -n "$NDK_DIR_X86" ]; then |
19 | 25 | export NDK_DIR="$NDK_DIR_X86" |
20 | 26 | 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" | |
22 | 28 | fi |
0 | 0 | #/bin/sh |
1 | 1 | |
2 | 2 | export NDK_DIR_ARM="$1-arm" |
3 | export NDK_DIR_ARM64="$1-arm64" | |
3 | 4 | export NDK_DIR_X86="$1-x86" |
4 | export ASSETS_DIR="$2" | |
5 | export JNI_LIBS_DIR="$2" | |
5 | 6 | |
6 | 7 | ./build_android.sh |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
19 | 19 | #include "common/aixlog.hpp" |
20 | 20 | #include "common/snap_exception.hpp" |
21 | 21 | #include "common/str_compat.hpp" |
22 | #include "message/factory.hpp" | |
22 | 23 | #include "message/hello.hpp" |
23 | 24 | #include <iostream> |
24 | 25 | #include <mutex> |
28 | 29 | |
29 | 30 | |
30 | 31 | 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), | |
32 | 33 | sumTimeout_(chronos::msec(0)) |
33 | 34 | { |
35 | base_msg_size_ = base_message_.getSize(); | |
36 | buffer_.resize(base_msg_size_); | |
34 | 37 | } |
35 | 38 | |
36 | 39 | |
47 | 50 | size_t len = 0; |
48 | 51 | do |
49 | 52 | { |
50 | len += socket_->read_some(boost::asio::buffer((char*)_to + len, toRead)); | |
53 | len += socket_.read_some(boost::asio::buffer((char*)_to + len, toRead)); | |
51 | 54 | // cout << "len: " << len << ", error: " << error << endl; |
52 | 55 | toRead = _bytes - len; |
53 | 56 | } while (toRead > 0); |
54 | 57 | } |
55 | 58 | |
56 | 59 | |
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()); | |
63 | 63 | if (mac.empty()) |
64 | 64 | 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"; | |
66 | 66 | return mac; |
67 | 67 | } |
68 | 68 | |
73 | 73 | tcp::resolver::query query(host_, cpt::to_string(port_), boost::asio::ip::resolver_query_base::numeric_service); |
74 | 74 | auto iterator = resolver.resolve(query); |
75 | 75 | LOG(DEBUG) << "Connecting\n"; |
76 | socket_.reset(new tcp::socket(io_context_)); | |
77 | 76 | // struct timeval tv; |
78 | 77 | // tv.tv_sec = 5; |
79 | 78 | // tv.tv_usec = 0; |
80 | 79 | // cout << "socket: " << socket->native_handle() << "\n"; |
81 | 80 | // setsockopt(socket->native_handle(), SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); |
82 | 81 | // 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; | |
86 | 84 | active_ = true; |
87 | 85 | sumTimeout_ = chronos::msec(0); |
88 | 86 | readerThread_ = new thread(&ClientConnection::reader, this); |
91 | 89 | |
92 | 90 | void ClientConnection::stop() |
93 | 91 | { |
94 | connected_ = false; | |
95 | 92 | active_ = false; |
96 | 93 | try |
97 | 94 | { |
98 | 95 | 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; | |
108 | 102 | if (readerThread_) |
109 | 103 | { |
110 | 104 | LOG(DEBUG) << "joining readerThread\n"; |
116 | 110 | { |
117 | 111 | } |
118 | 112 | readerThread_ = nullptr; |
119 | socket_.reset(); | |
120 | 113 | LOG(DEBUG) << "readerThread terminated\n"; |
121 | 114 | } |
122 | 115 | |
123 | 116 | |
124 | bool ClientConnection::send(const msg::BaseMessage* message) const | |
117 | bool ClientConnection::send(const msg::BaseMessage* message) | |
125 | 118 | { |
126 | 119 | // std::unique_lock<std::mutex> mlock(mutex_); |
127 | 120 | // LOG(DEBUG) << "send: " << message->type << ", size: " << message->getSize() << "\n"; |
128 | 121 | std::lock_guard<std::mutex> socketLock(socketMutex_); |
129 | if (!connected()) | |
122 | if (!socket_.is_open()) | |
130 | 123 | return false; |
131 | 124 | // LOG(DEBUG) << "send: " << message->type << ", size: " << message->getSize() << "\n"; |
132 | 125 | boost::asio::streambuf streambuf; |
134 | 127 | tv t; |
135 | 128 | message->sent = t; |
136 | 129 | message->serialize(stream); |
137 | boost::asio::write(*socket_.get(), streambuf); | |
130 | boost::asio::write(socket_, streambuf); | |
138 | 131 | return true; |
139 | 132 | } |
140 | 133 | |
141 | 134 | |
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); | |
145 | 139 | if (++reqId_ >= 10000) |
146 | 140 | reqId_ = 1; |
147 | 141 | message->id = reqId_; |
148 | 142 | // 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 | { | |
157 | 153 | sumTimeout_ = chronos::msec(0); |
158 | 154 | // LOG(INFO) << "Resp: " << pendingRequest->id << "\n"; |
159 | 155 | } |
164 | 160 | if (sumTimeout_ > chronos::sec(10)) |
165 | 161 | throw SnapException("sum timeout exceeded 10s"); |
166 | 162 | } |
167 | pendingRequests_.erase(pendingRequest); | |
163 | ||
164 | { // scope for lock | |
165 | std::unique_lock<std::mutex> lock(pendingRequestsMutex_); | |
166 | pendingRequests_.erase(pendingRequest); | |
167 | } | |
168 | 168 | return response; |
169 | 169 | } |
170 | 170 | |
171 | 171 | |
172 | 172 | void ClientConnection::getNextMessage() |
173 | 173 | { |
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()); | |
179 | 176 | // LOG(DEBUG) << "getNextMessage: " << baseMessage.type << ", size: " << baseMessage.size << ", id: " << baseMessage.id << ", refers: " << |
180 | 177 | // 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); | |
183 | 180 | // { |
184 | 181 | // 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; | |
186 | 185 | // } |
187 | tv t; | |
188 | baseMessage.received = t; | |
189 | ||
190 | { | |
186 | ||
187 | { // scope for lock | |
191 | 188 | 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_) | |
194 | 190 | { |
195 | for (auto req : pendingRequests_) | |
191 | if (req->id() == base_message_.refersTo) | |
196 | 192 | { |
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; | |
207 | 196 | } |
208 | 197 | } |
209 | 198 | } |
210 | 199 | |
211 | 200 | if (messageReceiver_ != nullptr) |
212 | messageReceiver_->onMessageReceived(this, baseMessage, &buffer[0]); | |
201 | messageReceiver_->onMessageReceived(this, base_message_, buffer_.data()); | |
213 | 202 | } |
214 | 203 | |
215 | 204 | |
231 | 220 | catch (...) |
232 | 221 | { |
233 | 222 | } |
234 | connected_ = false; | |
235 | 223 | active_ = false; |
236 | 224 | } |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
37 | 37 | |
38 | 38 | |
39 | 39 | /// Used to synchronize server requests (wait for server response) |
40 | struct PendingRequest | |
40 | class PendingRequest | |
41 | 41 | { |
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 | }; | |
43 | 47 | |
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_; | |
47 | 77 | }; |
48 | 78 | |
49 | 79 | |
75 | 105 | virtual ~ClientConnection(); |
76 | 106 | virtual void start(); |
77 | 107 | virtual void stop(); |
78 | virtual bool send(const msg::BaseMessage* message) const; | |
108 | virtual bool send(const msg::BaseMessage* message); | |
79 | 109 | |
80 | 110 | /// 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)); | |
82 | 112 | |
83 | 113 | /// Send request to the server and wait for answer of type T |
84 | 114 | 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)) | |
86 | 116 | { |
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) | |
89 | 119 | 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; | |
93 | 129 | } |
94 | 130 | |
95 | std::string getMacAddress() const; | |
131 | std::string getMacAddress(); | |
96 | 132 | |
97 | 133 | virtual bool active() const |
98 | 134 | { |
99 | 135 | return active_; |
100 | } | |
101 | ||
102 | virtual bool connected() const | |
103 | { | |
104 | return (socket_ != nullptr); | |
105 | // return (connected_ && socket); | |
106 | 136 | } |
107 | 137 | |
108 | 138 | protected: |
111 | 141 | void socketRead(void* to, size_t bytes); |
112 | 142 | void getNextMessage(); |
113 | 143 | |
144 | msg::BaseMessage base_message_; | |
145 | std::vector<char> buffer_; | |
146 | size_t base_msg_size_; | |
147 | ||
114 | 148 | boost::asio::io_context io_context_; |
115 | 149 | mutable std::mutex socketMutex_; |
116 | std::shared_ptr<tcp::socket> socket_; | |
150 | tcp::socket socket_; | |
117 | 151 | std::atomic<bool> active_; |
118 | std::atomic<bool> connected_; | |
119 | 152 | MessageReceiver* messageReceiver_; |
120 | 153 | mutable std::mutex pendingRequestsMutex_; |
121 | 154 | std::set<std::shared_ptr<PendingRequest>> pendingRequests_; |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
26 | 26 | #if defined(HAS_FLAC) |
27 | 27 | #include "decoder/flac_decoder.hpp" |
28 | 28 | #endif |
29 | #if defined(HAS_OPUS) | |
30 | #include "decoder/opus_decoder.hpp" | |
31 | #endif | |
29 | 32 | #include "common/aixlog.hpp" |
30 | 33 | #include "common/snap_exception.hpp" |
31 | 34 | #include "message/hello.hpp" |
58 | 61 | { |
59 | 62 | auto* pcmChunk = new msg::PcmChunk(sampleFormat_, 0); |
60 | 63 | pcmChunk->deserialize(baseMessage, buffer); |
61 | // LOG(DEBUG) << "chunk: " << pcmChunk->payloadSize << ", sampleFormat: " << sampleFormat_.rate << "\n"; | |
64 | // LOG(DEBUG) << "chunk: " << pcmChunk->payloadSize << ", sampleFormat: " << sampleFormat_.rate << "\n"; | |
62 | 65 | if (decoder_->decode(pcmChunk)) |
63 | 66 | { |
64 | 67 | // TODO: do decoding in thread? |
78 | 81 | } |
79 | 82 | else if (baseMessage.type == message_type::kServerSettings) |
80 | 83 | { |
81 | serverSettings_.reset(new msg::ServerSettings()); | |
84 | serverSettings_ = make_unique<msg::ServerSettings>(); | |
82 | 85 | serverSettings_->deserialize(baseMessage, buffer); |
83 | 86 | LOG(INFO) << "ServerSettings - buffer: " << serverSettings_->getBufferMs() << ", latency: " << serverSettings_->getLatency() |
84 | 87 | << ", volume: " << serverSettings_->getVolume() << ", muted: " << serverSettings_->isMuted() << "\n"; |
91 | 94 | } |
92 | 95 | else if (baseMessage.type == message_type::kCodecHeader) |
93 | 96 | { |
94 | headerChunk_.reset(new msg::CodecHeader()); | |
97 | headerChunk_ = make_unique<msg::CodecHeader>(); | |
95 | 98 | headerChunk_->deserialize(baseMessage, buffer); |
96 | 99 | |
97 | 100 | LOG(INFO) << "Codec: " << headerChunk_->codec << "\n"; |
100 | 103 | player_.reset(nullptr); |
101 | 104 | |
102 | 105 | if (headerChunk_->codec == "pcm") |
103 | decoder_.reset(new PcmDecoder()); | |
106 | decoder_ = make_unique<decoder::PcmDecoder>(); | |
104 | 107 | #if defined(HAS_OGG) && (defined(HAS_TREMOR) || defined(HAS_VORBIS)) |
105 | 108 | else if (headerChunk_->codec == "ogg") |
106 | decoder_.reset(new OggDecoder()); | |
109 | decoder_ = make_unique<decoder::OggDecoder>(); | |
107 | 110 | #endif |
108 | 111 | #if defined(HAS_FLAC) |
109 | 112 | 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>(); | |
111 | 118 | #endif |
112 | 119 | else |
113 | 120 | throw SnapException("codec not supported: \"" + headerChunk_->codec + "\""); |
119 | 126 | stream_->setBufferLen(serverSettings_->getBufferMs() - latency_); |
120 | 127 | |
121 | 128 | #ifdef HAS_ALSA |
122 | player_.reset(new AlsaPlayer(pcmDevice_, stream_)); | |
129 | player_ = make_unique<AlsaPlayer>(pcmDevice_, stream_); | |
123 | 130 | #elif HAS_OPENSL |
124 | player_.reset(new OpenslPlayer(pcmDevice_, stream_)); | |
131 | player_ = make_unique<OpenslPlayer>(pcmDevice_, stream_); | |
125 | 132 | #elif HAS_COREAUDIO |
126 | player_.reset(new CoreAudioPlayer(pcmDevice_, stream_)); | |
133 | player_ = make_unique<CoreAudioPlayer>(pcmDevice_, stream_); | |
127 | 134 | #else |
128 | 135 | throw SnapException("No audio player support"); |
129 | 136 | #endif |
133 | 140 | } |
134 | 141 | else if (baseMessage.type == message_type::kStreamTags) |
135 | 142 | { |
136 | streamTags_.reset(new msg::StreamTags()); | |
137 | streamTags_->deserialize(baseMessage, buffer); | |
138 | ||
139 | 143 | if (meta_) |
140 | meta_->push(streamTags_->msg); | |
144 | { | |
145 | msg::StreamTags streamTags_; | |
146 | streamTags_.deserialize(baseMessage, buffer); | |
147 | meta_->push(streamTags_.msg); | |
148 | } | |
141 | 149 | } |
142 | 150 | |
143 | 151 | if (baseMessage.type != message_type::kTime) |
166 | 174 | latency_ = latency; |
167 | 175 | clientConnection_.reset(new ClientConnection(this, host, port)); |
168 | 176 | 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); | |
169 | 187 | } |
170 | 188 | |
171 | 189 | |
206 | 224 | throw SnapException(async_exception_->what()); |
207 | 225 | } |
208 | 226 | |
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)); | |
210 | 228 | if (reply) |
211 | 229 | { |
212 | 230 | TimeProvider::getInstance().setDiff(reply->latency, reply->received - reply->sent); |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
49 | 49 | public: |
50 | 50 | Controller(const std::string& clientId, size_t instance, std::shared_ptr<MetadataAdapter> meta); |
51 | 51 | 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); | |
52 | 53 | void stop(); |
53 | 54 | |
54 | 55 | /// Implementation of MessageReceiver. |
72 | 73 | int latency_; |
73 | 74 | std::unique_ptr<ClientConnection> clientConnection_; |
74 | 75 | std::shared_ptr<Stream> stream_; |
75 | std::unique_ptr<Decoder> decoder_; | |
76 | std::unique_ptr<decoder::Decoder> decoder_; | |
76 | 77 | std::unique_ptr<Player> player_; |
77 | 78 | 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_; | |
81 | 81 | std::mutex receiveMutex_; |
82 | 82 | |
83 | 83 | shared_exception_ptr async_exception_; |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
22 | 22 | #include "message/pcm_chunk.hpp" |
23 | 23 | #include <mutex> |
24 | 24 | |
25 | namespace decoder | |
26 | { | |
25 | 27 | |
26 | 28 | class Decoder |
27 | 29 | { |
36 | 38 | std::mutex mutex_; |
37 | 39 | }; |
38 | 40 | |
41 | } // namespace decoder | |
39 | 42 | |
40 | 43 | #endif |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
26 | 26 | |
27 | 27 | using namespace std; |
28 | 28 | |
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 | |
43 | 49 | |
44 | 50 | |
45 | 51 | FlacDecoder::FlacDecoder() : Decoder(), lastError_(nullptr) |
51 | 57 | FlacDecoder::~FlacDecoder() |
52 | 58 | { |
53 | 59 | std::lock_guard<std::mutex> lock(mutex_); |
60 | FLAC__stream_decoder_delete(decoder); | |
54 | 61 | delete flacChunk; |
55 | delete decoder; | |
56 | 62 | } |
57 | 63 | |
58 | 64 | |
104 | 110 | throw SnapException("ERROR: allocating decoder"); |
105 | 111 | |
106 | 112 | // (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); | |
109 | 115 | if (init_status != FLAC__STREAM_DECODER_INIT_STATUS_OK) |
110 | 116 | throw SnapException("ERROR: initializing decoder: " + string(FLAC__StreamDecoderInitStatusString[init_status])); |
111 | 117 | |
117 | 123 | return sampleFormat; |
118 | 124 | } |
119 | 125 | |
120 | ||
126 | namespace callback | |
127 | { | |
121 | 128 | FLAC__StreamDecoderReadStatus read_callback(const FLAC__StreamDecoder* /*decoder*/, FLAC__byte buffer[], size_t* bytes, void* client_data) |
122 | 129 | { |
123 | 130 | if (flacHeader != nullptr) |
145 | 152 | } |
146 | 153 | |
147 | 154 | |
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[], | |
149 | 156 | void* client_data) |
150 | 157 | { |
151 | (void)decoder; | |
152 | ||
153 | 158 | if (pcmChunk != nullptr) |
154 | 159 | { |
155 | 160 | size_t bytes = frame->header.blocksize * sampleFormat.frameSize; |
194 | 199 | } |
195 | 200 | |
196 | 201 | |
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 | { | |
200 | 204 | /* print some stats */ |
201 | 205 | if (metadata->type == FLAC__METADATA_TYPE_STREAMINFO) |
202 | 206 | { |
206 | 210 | } |
207 | 211 | |
208 | 212 | |
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 | { | |
212 | 215 | SLOG(ERROR) << "Got error callback: " << FLAC__StreamDecoderErrorStatusString[status] << "\n"; |
213 | 216 | static_cast<FlacDecoder*>(client_data)->lastError_ = std::unique_ptr<FLAC__StreamDecoderErrorStatus>(new FLAC__StreamDecoderErrorStatus(status)); |
214 | 217 | } |
218 | } // namespace callback | |
219 | ||
220 | } // namespace decoder |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
24 | 24 | #include <atomic> |
25 | 25 | #include <memory> |
26 | 26 | |
27 | ||
27 | namespace decoder | |
28 | { | |
28 | 29 | |
29 | 30 | struct CacheInfo |
30 | 31 | { |
57 | 58 | std::unique_ptr<FLAC__StreamDecoderErrorStatus> lastError_; |
58 | 59 | }; |
59 | 60 | |
61 | } // namespace decoder | |
60 | 62 | |
61 | 63 | #endif |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
27 | 27 | |
28 | 28 | using namespace std; |
29 | 29 | |
30 | namespace decoder | |
31 | { | |
30 | 32 | |
31 | 33 | OggDecoder::OggDecoder() : Decoder() |
32 | 34 | { |
237 | 239 | |
238 | 240 | return sampleFormat_; |
239 | 241 | } |
242 | ||
243 | } // namespace decoder |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
24 | 24 | #include <vorbis/codec.h> |
25 | 25 | #endif |
26 | 26 | #include <ogg/ogg.h> |
27 | ||
28 | namespace decoder | |
29 | { | |
27 | 30 | |
28 | 31 | class OggDecoder : public Decoder |
29 | 32 | { |
58 | 61 | SampleFormat sampleFormat_; |
59 | 62 | }; |
60 | 63 | |
64 | } // namespace decoder | |
61 | 65 | |
62 | 66 | #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 |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
20 | 20 | #include "common/endian.hpp" |
21 | 21 | #include "common/snap_exception.hpp" |
22 | 22 | |
23 | namespace decoder | |
24 | { | |
23 | 25 | |
24 | 26 | #define ID_RIFF 0x46464952 |
25 | 27 | #define ID_WAVE 0x45564157 |
117 | 119 | |
118 | 120 | return sampleFormat; |
119 | 121 | } |
122 | ||
123 | } // namespace decoder |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
20 | 20 | #include "decoder.hpp" |
21 | 21 | |
22 | 22 | |
23 | namespace decoder | |
24 | { | |
25 | ||
23 | 26 | class PcmDecoder : public Decoder |
24 | 27 | { |
25 | 28 | public: |
28 | 31 | SampleFormat setHeader(msg::CodecHeader* chunk) override; |
29 | 32 | }; |
30 | 33 | |
34 | } // namespace decoder | |
31 | 35 | |
32 | 36 | #endif |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | 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>⏎ |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
43 | 43 | |
44 | 44 | void reset() |
45 | 45 | { |
46 | msg_.reset(new json); | |
46 | msg_ = json{}; | |
47 | 47 | } |
48 | 48 | |
49 | 49 | std::string serialize() |
50 | 50 | { |
51 | return METADATA + ":" + msg_->dump(); | |
51 | return METADATA + ":" + msg_.dump(); | |
52 | 52 | } |
53 | 53 | |
54 | void tag(std::string name, std::string value) | |
54 | void tag(const std::string& name, const std::string& value) | |
55 | 55 | { |
56 | (*msg_)[name] = value; | |
56 | msg_[name] = value; | |
57 | 57 | } |
58 | 58 | |
59 | std::string operator[](std::string key) | |
59 | std::string operator[](const std::string& key) | |
60 | 60 | { |
61 | 61 | try |
62 | 62 | { |
63 | return (*msg_)[key]; | |
63 | return msg_[key]; | |
64 | 64 | } |
65 | 65 | catch (std::domain_error&) |
66 | 66 | { |
74 | 74 | return 0; |
75 | 75 | } |
76 | 76 | |
77 | int push(json& jtag) | |
77 | int push(const json& jtag) | |
78 | 78 | { |
79 | msg_.reset(new json(jtag)); | |
79 | msg_ = jtag; | |
80 | 80 | return push(); |
81 | 81 | } |
82 | 82 | |
83 | 83 | protected: |
84 | std::shared_ptr<json> msg_; | |
84 | json msg_; | |
85 | 85 | }; |
86 | 86 | |
87 | 87 | /* |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
224 | 224 | adjustVolume(buff_, frames_); |
225 | 225 | if ((pcm = snd_pcm_writei(handle_, buff_, frames_)) == -EPIPE) |
226 | 226 | { |
227 | LOG(ERROR) << "XRUN\n"; | |
227 | LOG(ERROR) << "XRUN: " << snd_strerror(pcm) << "\n"; | |
228 | 228 | snd_pcm_prepare(handle_); |
229 | 229 | } |
230 | 230 | else if (pcm < 0) |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
75 | 75 | continue; |
76 | 76 | |
77 | 77 | UInt32 maxlen = 1024; |
78 | char buf[maxlen]; | |
78 | char buf[1024]; | |
79 | 79 | theAddress = {kAudioDevicePropertyDeviceName, kAudioDevicePropertyScopeOutput, 0}; |
80 | 80 | AudioObjectGetPropertyData(devids[i], &theAddress, 0, NULL, &maxlen, buf); |
81 | 81 | LOG(DEBUG) << "device: " << i << ", name: " << buf << ", channels: " << channels << "\n"; |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
24 | 24 | #include "opensl_player.hpp" |
25 | 25 | |
26 | 26 | using namespace std; |
27 | ||
28 | ||
29 | static constexpr auto kPhaseInit = "Init"; | |
30 | static constexpr auto kPhaseStart = "Start"; | |
31 | static constexpr auto kPhaseStop = "Stop"; | |
32 | ||
27 | 33 | |
28 | 34 | // http://stackoverflow.com/questions/35730050/android-with-nexus-6-how-to-avoid-decreased-opensl-audio-thread-priority-rela?rq=1 |
29 | 35 | |
164 | 170 | |
165 | 171 | |
166 | 172 | |
167 | void OpenslPlayer::throwUnsuccess(const std::string& what, SLresult result) | |
173 | void OpenslPlayer::throwUnsuccess(const std::string& phase, const std::string& what, SLresult result) | |
168 | 174 | { |
169 | 175 | if (SL_RESULT_SUCCESS == result) |
170 | 176 | return; |
171 | 177 | stringstream ss; |
172 | ss << what << " failed: " << resultToString(result) << "(" << result << ")"; | |
178 | ss << phase << " failed, operation: " << what << ", result: " << resultToString(result) << "(" << result << ")"; | |
173 | 179 | throw SnapException(ss.str()); |
174 | 180 | } |
175 | 181 | |
191 | 197 | // create engine |
192 | 198 | SLEngineOption engineOption[] = {{(SLuint32)SL_ENGINEOPTION_THREADSAFE, (SLuint32)SL_BOOLEAN_TRUE}}; |
193 | 199 | result = slCreateEngine(&engineObject, 1, engineOption, 0, NULL, NULL); |
194 | throwUnsuccess("slCreateEngine", result); | |
200 | throwUnsuccess(kPhaseInit, "slCreateEngine", result); | |
195 | 201 | result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE); |
196 | throwUnsuccess("EngineObject::Realize", result); | |
202 | throwUnsuccess(kPhaseInit, "EngineObject::Realize", result); | |
197 | 203 | result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine); |
198 | throwUnsuccess("EngineObject::GetInterface", result); | |
204 | throwUnsuccess(kPhaseInit, "EngineObject::GetInterface", result); | |
199 | 205 | result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 0, 0, 0); |
200 | throwUnsuccess("EngineEngine::CreateOutputMix", result); | |
206 | throwUnsuccess(kPhaseInit, "EngineEngine::CreateOutputMix", result); | |
201 | 207 | result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE); |
202 | throwUnsuccess("OutputMixObject::Realize", result); | |
208 | throwUnsuccess(kPhaseInit, "OutputMixObject::Realize", result); | |
203 | 209 | |
204 | 210 | SLuint32 samplesPerSec = SL_SAMPLINGRATE_48; |
205 | 211 | switch (format.rate) |
284 | 290 | const SLInterfaceID ids[3] = {SL_IID_ANDROIDCONFIGURATION, SL_IID_PLAY, SL_IID_BUFFERQUEUE}; //, SL_IID_VOLUME}; |
285 | 291 | const SLboolean req[3] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE}; //, SL_BOOLEAN_TRUE}; |
286 | 292 | result = (*engineEngine)->CreateAudioPlayer(engineEngine, &bqPlayerObject, &audioSrc, &audioSnk, 3, ids, req); |
287 | throwUnsuccess("Engine::CreateAudioPlayer", result); | |
293 | throwUnsuccess(kPhaseInit, "Engine::CreateAudioPlayer", result); | |
288 | 294 | |
289 | 295 | SLAndroidConfigurationItf playerConfig; |
290 | 296 | result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_ANDROIDCONFIGURATION, &playerConfig); |
291 | throwUnsuccess("PlayerObject::GetInterface", result); | |
297 | throwUnsuccess(kPhaseInit, "PlayerObject::GetInterface", result); | |
292 | 298 | SLint32 streamType = SL_ANDROID_STREAM_MEDIA; |
293 | 299 | //// SLint32 streamType = SL_ANDROID_STREAM_VOICE; |
294 | 300 | result = (*playerConfig)->SetConfiguration(playerConfig, SL_ANDROID_KEY_STREAM_TYPE, &streamType, sizeof(SLint32)); |
295 | throwUnsuccess("PlayerConfig::SetConfiguration", result); | |
301 | throwUnsuccess(kPhaseInit, "PlayerConfig::SetConfiguration", result); | |
296 | 302 | |
297 | 303 | result = (*bqPlayerObject)->Realize(bqPlayerObject, SL_BOOLEAN_FALSE); |
298 | throwUnsuccess("PlayerObject::Realize", result); | |
304 | throwUnsuccess(kPhaseInit, "PlayerObject::Realize", result); | |
299 | 305 | result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_PLAY, &bqPlayerPlay); |
300 | throwUnsuccess("PlayerObject::GetInterface", result); | |
306 | throwUnsuccess(kPhaseInit, "PlayerObject::GetInterface", result); | |
301 | 307 | result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_BUFFERQUEUE, &bqPlayerBufferQueue); |
302 | throwUnsuccess("PlayerObject::GetInterface", result); | |
308 | throwUnsuccess(kPhaseInit, "PlayerObject::GetInterface", result); | |
303 | 309 | result = (*bqPlayerBufferQueue)->RegisterCallback(bqPlayerBufferQueue, bqPlayerCallback, this); |
304 | throwUnsuccess("PlayerBufferQueue::RegisterCallback", result); | |
310 | throwUnsuccess(kPhaseInit, "PlayerBufferQueue::RegisterCallback", result); | |
305 | 311 | // result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_VOLUME, &bqPlayerVolume); |
306 | 312 | // throwUnsuccess("PlayerObject::GetInterface", result); |
307 | 313 | result = (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_PAUSED); |
308 | throwUnsuccess("PlayerPlay::SetPlayState", result); | |
314 | throwUnsuccess(kPhaseInit, "PlayerPlay::SetPlayState", result); | |
309 | 315 | |
310 | 316 | // Render and enqueue a first buffer. (or should we just play the buffer empty?) |
311 | 317 | curBuffer = 0; |
316 | 322 | |
317 | 323 | memset(buffer[curBuffer], 0, buff_size); |
318 | 324 | result = (*bqPlayerBufferQueue)->Enqueue(bqPlayerBufferQueue, buffer[curBuffer], sizeof(buffer[curBuffer])); |
319 | throwUnsuccess("PlayerBufferQueue::Enqueue", result); | |
325 | throwUnsuccess(kPhaseInit, "PlayerBufferQueue::Enqueue", result); | |
320 | 326 | curBuffer ^= 1; |
321 | 327 | } |
322 | 328 | |
378 | 384 | void OpenslPlayer::start() |
379 | 385 | { |
380 | 386 | SLresult result = (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_PLAYING); |
381 | throwUnsuccess("PlayerPlay::SetPlayState", result); | |
387 | throwUnsuccess(kPhaseStart, "PlayerPlay::SetPlayState", result); | |
382 | 388 | } |
383 | 389 | |
384 | 390 | |
385 | 391 | void OpenslPlayer::stop() |
386 | 392 | { |
387 | 393 | SLresult result = (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_STOPPED); |
388 | throwUnsuccess("PlayerPlay::SetPlayState", result); | |
394 | (*bqPlayerBufferQueue)->Clear(bqPlayerBufferQueue); | |
395 | throwUnsuccess(kPhaseStop, "PlayerPlay::SetPlayState", result); | |
389 | 396 | } |
390 | 397 | |
391 | 398 |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
47 | 47 | void uninitOpensl(); |
48 | 48 | |
49 | 49 | 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); | |
51 | 51 | std::string resultToString(SLresult result) const; |
52 | 52 | |
53 | 53 | // engine interfaces |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
0 | 0 | .\"groff -Tascii -man snapclient.1 |
1 | .TH SNAPCLIENT 1 "July 2018" | |
1 | .TH SNAPCLIENT 1 "January 2020" | |
2 | 2 | .SH NAME |
3 | 3 | snapclient - Snapcast client |
4 | 4 | .SH SYNOPSIS |
55 | 55 | \fI/etc/default/snapclient\fR |
56 | 56 | the daemon default configuration file |
57 | 57 | .SH "COPYRIGHT" |
58 | Copyright (C) 2014-2019 Johannes Pohl (snapcast@badaix.de). | |
58 | Copyright (C) 2014-2020 Johannes Pohl (snapcast@badaix.de). | |
59 | 59 | License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>. |
60 | 60 | This is free software: you are free to change and redistribute it. |
61 | 61 | There is NO WARRANTY, to the extent permitted by law. |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
15 | 15 | along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | ***/ |
17 | 17 | |
18 | #include <chrono> | |
18 | 19 | #include <iostream> |
19 | 20 | #include <sys/resource.h> |
20 | 21 | |
38 | 39 | using namespace std; |
39 | 40 | using namespace popl; |
40 | 41 | |
41 | volatile sig_atomic_t g_terminated = false; | |
42 | using namespace std::chrono_literals; | |
42 | 43 | |
43 | 44 | PcmDevice getPcmDevice(const std::string& soundcard) |
44 | 45 | { |
59 | 60 | for (auto dev : pcmDevices) |
60 | 61 | if (dev.name.find(soundcard) != string::npos) |
61 | 62 | return dev; |
62 | #endif | |
63 | #else | |
64 | std::ignore = soundcard; | |
65 | #endif | |
66 | ||
63 | 67 | PcmDevice pcmDevice; |
64 | 68 | return pcmDevice; |
65 | 69 | } |
116 | 120 | if (versionSwitch->is_set()) |
117 | 121 | { |
118 | 122 | 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" | |
120 | 124 | << "License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.\n" |
121 | 125 | << "This is free software: you are free to change and redistribute it.\n" |
122 | 126 | << "There is NO WARRANTY, to the extent permitted by law.\n\n" |
167 | 171 | { |
168 | 172 | AixLog::Log::instance().add_logsink<AixLog::SinkCout>(AixLog::Severity::info, AixLog::Type::all, "%Y-%m-%d %H-%M-%S [#severity]"); |
169 | 173 | } |
170 | ||
171 | ||
172 | signal(SIGHUP, signal_handler); | |
173 | signal(SIGTERM, signal_handler); | |
174 | signal(SIGINT, signal_handler); | |
175 | 174 | |
176 | 175 | #ifdef HAS_DAEMON |
177 | 176 | std::unique_ptr<Daemon> daemon; |
214 | 213 | } |
215 | 214 | #endif |
216 | 215 | |
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 | }); | |
217 | 228 | if (host.empty()) |
218 | 229 | { |
219 | 230 | #if defined(HAS_AVAHI) || defined(HAS_BONJOUR) |
220 | 231 | BrowseZeroConf browser; |
221 | 232 | mDNSResult avahiResult; |
222 | while (!g_terminated) | |
233 | while (active) | |
223 | 234 | { |
235 | signal_handler.wait_for(500ms); | |
236 | if (!active) | |
237 | break; | |
224 | 238 | try |
225 | 239 | { |
226 | 240 | if (browser.browse("_snapcast._tcp", avahiResult, 5000)) |
237 | 251 | { |
238 | 252 | SLOG(ERROR) << "Exception: " << e.what() << std::endl; |
239 | 253 | } |
240 | chronos::sleep(500); | |
241 | 254 | } |
242 | 255 | #endif |
243 | 256 | } |
244 | 257 | |
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); | |
254 | 267 | 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(); | |
259 | 271 | } |
260 | 272 | } |
261 | 273 | catch (const std::exception& e) |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
177 | 177 | return tp; |
178 | 178 | } |
179 | 179 | |
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 | */ | |
181 | 208 | |
182 | 209 | void Stream::updateBuffers(int age) |
183 | 210 | { |
311 | 338 | LOG(INFO) << "pBuffer->full() && (abs(median_) > 1): " << median_ << "\n"; |
312 | 339 | sleep_ = cs::usec(median_); |
313 | 340 | } |
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 | } | |
340 | 367 | } |
341 | 368 | |
342 | 369 | if (sleep_.count() != 0) |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | 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 |
0 | 0 | # This file is part of snapcast |
1 | # Copyright (C) 2014-2019 Johannes Pohl | |
1 | # Copyright (C) 2014-2020 Johannes Pohl | |
2 | 2 | |
3 | 3 | # This program is free software: you can redistribute it and/or modify |
4 | 4 | # it under the terms of the GNU General Public License as published by |
2 | 2 | / _\ ( )( \/ )( ) / \ / __) |
3 | 3 | / \ )( ) ( / (_/\( O )( (_ \ |
4 | 4 | \_/\_/(__)(_/\_)\____/ \__/ \___/ |
5 | version 1.2.2 | |
5 | version 1.2.5 | |
6 | 6 | https://github.com/badaix/aixlog |
7 | 7 | |
8 | 8 | This file is part of aixlog |
9 | Copyright (C) 2017-2019 Johannes Pohl | |
9 | Copyright (C) 2017-2020 Johannes Pohl | |
10 | 10 | |
11 | 11 | This software may be modified and distributed under the terms |
12 | 12 | of the MIT license. See the LICENSE file for details. |
78 | 78 | #endif |
79 | 79 | |
80 | 80 | /// 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() | |
82 | 82 | #define AIXLOG_INTERNAL__LOG_SEVERITY_TAG(SEVERITY_, TAG_) std::clog << static_cast<AixLog::Severity>(SEVERITY_) << TAG(TAG_) |
83 | 83 | |
84 | 84 | #define AIXLOG_INTERNAL__ONE_COLOR(FG_) AixLog::Color::FG_ |
274 | 274 | std::string to_string(const std::string& format = "%Y-%m-%d %H-%M-%S.#ms") const |
275 | 275 | { |
276 | 276 | 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); | |
278 | 278 | char buffer[256]; |
279 | strftime(buffer, sizeof buffer, format.c_str(), now_tm); | |
279 | strftime(buffer, sizeof buffer, format.c_str(), &now_tm); | |
280 | 280 | std::string result(buffer); |
281 | 281 | size_t pos = result.find("#ms"); |
282 | 282 | if (pos != std::string::npos) |
283 | 283 | { |
284 | 284 | int ms_part = std::chrono::time_point_cast<std::chrono::milliseconds>(time_point).time_since_epoch().count() % 1000; |
285 | 285 | 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); | |
288 | 288 | } |
289 | 289 | return result; |
290 | 290 | } |
293 | 293 | |
294 | 294 | private: |
295 | 295 | 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 | } | |
296 | 311 | }; |
297 | 312 | |
298 | 313 | /** |
499 | 514 | case Severity::warning: |
500 | 515 | return "Warn"; |
501 | 516 | case Severity::error: |
502 | return "Err"; | |
517 | return "Error"; | |
503 | 518 | case Severity::fatal: |
504 | 519 | return "Fatal"; |
505 | 520 | default: |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
29 | 29 | class CodecHeader : public BaseMessage |
30 | 30 | { |
31 | 31 | 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) | |
33 | 34 | { |
34 | payload = (char*)malloc(size); | |
35 | if (size > 0) | |
36 | payload = (char*)malloc(size * sizeof(char)); | |
35 | 37 | } |
36 | 38 | |
37 | 39 | ~CodecHeader() override |
61 | 63 | writeVal(stream, payload, payloadSize); |
62 | 64 | } |
63 | 65 | }; |
64 | } | |
66 | } // namespace msg | |
65 | 67 | |
66 | 68 | |
67 | 69 | #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 |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
290 | 290 | virtual void doserialize(std::ostream& /*stream*/) const {}; |
291 | 291 | }; |
292 | 292 | |
293 | ||
294 | struct SerializedMessage | |
295 | { | |
296 | ~SerializedMessage() | |
297 | { | |
298 | free(buffer); | |
299 | } | |
300 | ||
301 | BaseMessage message; | |
302 | char* buffer; | |
303 | }; | |
304 | } | |
293 | } // namespace msg | |
305 | 294 | |
306 | 295 | #endif |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
39 | 39 | { |
40 | 40 | } |
41 | 41 | |
42 | PcmChunk(const PcmChunk& pcmChunk) : WireChunk(pcmChunk), format(pcmChunk.format), idx_(0) | |
43 | { | |
44 | } | |
45 | ||
46 | 42 | PcmChunk() : WireChunk(), idx_(0) |
47 | 43 | { |
48 | 44 | } |
49 | 45 | |
50 | 46 | ~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 | |
51 | 58 | |
52 | 59 | int readFrames(void* outputBuffer, size_t frameCount) |
53 | 60 | { |
122 | 129 | private: |
123 | 130 | uint32_t idx_; |
124 | 131 | }; |
125 | } | |
132 | } // namespace msg | |
126 | 133 | |
127 | 134 | #endif |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
38 | 38 | class WireChunk : public BaseMessage |
39 | 39 | { |
40 | 40 | 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) | |
42 | 42 | { |
43 | payload = (char*)malloc(size); | |
43 | if (size > 0) | |
44 | payload = (char*)malloc(size * sizeof(char)); | |
44 | 45 | } |
45 | 46 | |
46 | 47 | WireChunk(const WireChunk& wireChunk) : BaseMessage(message_type::kWireChunk), timestamp(wireChunk.timestamp), payloadSize(wireChunk.payloadSize) |
83 | 84 | writeVal(stream, payload, payloadSize); |
84 | 85 | } |
85 | 86 | }; |
86 | } | |
87 | } // namespace msg | |
87 | 88 | |
88 | 89 | |
89 | 90 | #endif |
937 | 937 | std::string line; |
938 | 938 | |
939 | 939 | 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()); | |
942 | 942 | return s; |
943 | 943 | }; |
944 | 944 |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
15 | 15 | along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | ***/ |
17 | 17 | |
18 | #ifndef SIGNAL_HANDLER_H | |
19 | #define SIGNAL_HANDLER_H | |
18 | #ifndef SIGNAL_HANDLER_HPP | |
19 | #define SIGNAL_HANDLER_HPP | |
20 | 20 | |
21 | #include <functional> | |
22 | #include <future> | |
23 | #include <set> | |
21 | 24 | #include <signal.h> |
22 | #include <syslog.h> | |
23 | 25 | |
24 | extern volatile sig_atomic_t g_terminated; | |
26 | using signal_callback = std::function<void(int signal, const std::string& name)>; | |
25 | 27 | |
26 | void signal_handler(int sig) | |
28 | static std::future<int> install_signal_handler(std::set<int> signals, const signal_callback& on_signal = nullptr) | |
27 | 29 | { |
30 | static std::promise<int> promise; | |
31 | std::future<int> future = promise.get_future(); | |
32 | static signal_callback callback = on_signal; | |
28 | 33 | |
29 | switch (sig) | |
34 | for (auto signal : signals) | |
30 | 35 | { |
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 | }); | |
45 | 47 | } |
48 | return future; | |
46 | 49 | } |
47 | 50 | |
48 | 51 | #endif |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
25 | 25 | // text_exception uses a dynamically-allocated internal c-string for what(): |
26 | 26 | class SnapException : public std::exception |
27 | 27 | { |
28 | char* text_; | |
28 | std::string text_; | |
29 | 29 | |
30 | 30 | public: |
31 | SnapException(const char* text) | |
31 | SnapException(const char* text) : text_(text) | |
32 | 32 | { |
33 | text_ = new char[std::strlen(text) + 1]; | |
34 | std::strcpy(text_, text); | |
35 | 33 | } |
36 | 34 | |
37 | 35 | SnapException(const std::string& text) : SnapException(text.c_str()) |
38 | 36 | { |
39 | 37 | } |
40 | 38 | |
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; | |
49 | 40 | |
50 | 41 | const char* what() const noexcept override |
51 | 42 | { |
52 | return text_; | |
43 | return text_.c_str(); | |
53 | 44 | } |
54 | 45 | }; |
55 | 46 | |
66 | 57 | { |
67 | 58 | } |
68 | 59 | |
69 | AsyncSnapException(const AsyncSnapException& e) : SnapException(e.what()) | |
70 | { | |
71 | } | |
72 | ||
73 | ||
74 | 60 | ~AsyncSnapException() throw() override = default; |
75 | 61 | }; |
76 | 62 |
65 | 65 | #endif |
66 | 66 | } |
67 | 67 | |
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 | ||
68 | 80 | static double stod(const std::string& str) |
69 | 81 | { |
70 | 82 | #ifdef NO_CPP11_STRING |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
49 | 49 | gettimeofday(tv, nullptr); |
50 | 50 | // timeofday<std::chrono::system_clock>(tv); |
51 | 51 | } |
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 | ||
52 | 66 | |
53 | 67 | inline static void addUs(timeval& tv, int us) |
54 | 68 | { |
114 | 128 | return; |
115 | 129 | sleep(usec(microseconds)); |
116 | 130 | } |
117 | } | |
131 | } // namespace chronos | |
118 | 132 | |
119 | 133 | |
120 | 134 | #endif |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
32 | 32 | // trim from start |
33 | 33 | static inline std::string& ltrim(std::string& s) |
34 | 34 | { |
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); })); | |
36 | 36 | return s; |
37 | 37 | } |
38 | 38 | |
39 | 39 | // trim from end |
40 | 40 | static inline std::string& rtrim(std::string& s) |
41 | 41 | { |
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()); | |
43 | 43 | return s; |
44 | 44 | } |
45 | 45 | |
94 | 94 | } |
95 | 95 | |
96 | 96 | |
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 | ||
97 | 113 | |
98 | 114 | static std::vector<std::string>& split(const std::string& s, char delim, std::vector<std::string>& elems) |
99 | 115 | { |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
123 | 123 | std::string result = getProp("net.hostname"); |
124 | 124 | if (!result.empty()) |
125 | 125 | return result; |
126 | result = getProp("ro.product.model"); | |
127 | if (!result.empty()) | |
128 | return result; | |
126 | 129 | #endif |
127 | 130 | char hostname[1024]; |
128 | 131 | hostname[1023] = '\0'; |
1 | 1 | import sys |
2 | 2 | import telnetlib |
3 | 3 | import json |
4 | import threading | |
5 | import time | |
6 | 4 | |
7 | 5 | if len(sys.argv) < 3: |
8 | 6 | print("usage: control.py <SERVER HOST> [setVolume|setName]") |
21 | 19 | if jResponse['id'] == requestId: |
22 | 20 | print("recv: " + response) |
23 | 21 | return jResponse; |
24 | return; | |
25 | 22 | |
26 | 23 | def setVolume(client, volume): |
27 | 24 | global requestId |
1 | 1 | import sys |
2 | 2 | import telnetlib |
3 | 3 | import json |
4 | import threading | |
5 | import time | |
6 | 4 | |
7 | 5 | telnet = telnetlib.Telnet(sys.argv[1], 1705) |
8 | 6 | requestId = 1 |
17 | 15 | if jResponse['id'] == requestId: |
18 | 16 | # print("recv: " + response) |
19 | 17 | return jResponse; |
20 | return; | |
21 | 18 | |
22 | 19 | def setVolume(client, volume): |
23 | 20 | 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 | ||
0 | 47 | snapcast (0.16.0-1) unstable; urgency=medium |
1 | 48 | |
2 | 49 | * Features |
16 | 63 | -Tidy up code base |
17 | 64 | -Makefile doesn't statically link libgcc and libstdc++ |
18 | 65 | |
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 |
5 | 5 | libasound2-dev, |
6 | 6 | libvorbis-dev, |
7 | 7 | libflac-dev, |
8 | libopus-dev, | |
8 | 9 | libavahi-client-dev, |
9 | 10 | libasio-dev |
10 | 11 | Standards-Version: 4.1.4 |
15 | 15 | PATH=/sbin:/usr/sbin:/bin:/usr/bin |
16 | 16 | DESC="Snapcast client" |
17 | 17 | NAME=snapclient |
18 | USERNAME=snapclient | |
18 | 19 | DAEMON=/usr/bin/$NAME |
19 | 20 | PIDFILE=/var/run/$NAME/pid |
20 | 21 | SCRIPTNAME=/etc/init.d/$NAME |
46 | 47 | PIDDIR=$(dirname "$PIDFILE") |
47 | 48 | if [ ! -d "$PIDDIR" ]; then |
48 | 49 | mkdir -m 0755 $PIDDIR |
49 | chown _snapclient:_snapclient $PIDDIR | |
50 | chown $USERNAME:$USERNAME $PIDDIR | |
50 | 51 | fi |
51 | 52 | |
52 | 53 | # Return |
53 | 54 | # 0 if daemon has been started |
54 | 55 | # 1 if daemon was already running |
55 | 56 | # 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 | |
58 | 59 | # Add code here, if necessary, that waits for the process to be ready |
59 | 60 | # to handle requests from services started subsequently which depend |
60 | 61 | # on this one. As a last resort, sleep for some time. |
1 | 1 | |
2 | 2 | set -e |
3 | 3 | |
4 | USERNAME=_snapclient | |
4 | USERNAME=snapclient | |
5 | 5 | HOMEDIR=/var/lib/snapclient |
6 | 6 | |
7 | 7 | 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 | |
10 | 10 | adduser $USERNAME audio |
11 | 11 | fi |
12 | 12 |
3 | 3 | |
4 | 4 | #DEBHELPER# |
5 | 5 | |
6 | USERNAME=_snapclient | |
6 | USERNAME=snapclient | |
7 | 7 | HOMEDIR=/var/lib/snapclient |
8 | 8 | |
9 | 9 | if [ "$1" = "purge" ]; then |
1 | 1 | Description=Snapcast client |
2 | 2 | Documentation=man:snapclient(1) |
3 | 3 | 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 | |
5 | 5 | |
6 | 6 | [Service] |
7 | 7 | EnvironmentFile=-/etc/default/snapclient |
8 | 8 | ExecStart=/usr/bin/snapclient $SNAPCLIENT_OPTS |
9 | User=_snapclient | |
10 | Group=_snapclient | |
9 | User=snapclient | |
10 | Group=snapclient | |
11 | 11 | # very noisy on stdout |
12 | 12 | StandardOutput=null |
13 | 13 | Restart=on-failure |
15 | 15 | PATH=/sbin:/usr/sbin:/bin:/usr/bin |
16 | 16 | DESC="Snapcast server" |
17 | 17 | NAME=snapserver |
18 | USERNAME=snapserver | |
18 | 19 | DAEMON=/usr/bin/$NAME |
19 | 20 | PIDFILE=/var/run/$NAME/pid |
20 | 21 | SCRIPTNAME=/etc/init.d/$NAME |
46 | 47 | PIDDIR=$(dirname "$PIDFILE") |
47 | 48 | if [ ! -d "$PIDDIR" ]; then |
48 | 49 | mkdir -m 0755 $PIDDIR |
49 | chown _snapserver:_snapserver $PIDDIR | |
50 | chown $USERNAME:$USERNAME $PIDDIR | |
50 | 51 | fi |
51 | 52 | |
52 | 53 | # Return |
53 | 54 | # 0 if daemon has been started |
54 | 55 | # 1 if daemon was already running |
55 | 56 | # 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 | |
58 | 59 | # Add code here, if necessary, that waits for the process to be ready |
59 | 60 | # to handle requests from services started subsequently which depend |
60 | 61 | # on this one. As a last resort, sleep for some time. |
1 | 1 | |
2 | 2 | set -e |
3 | 3 | |
4 | USERNAME=_snapserver | |
4 | USERNAME=snapserver | |
5 | 5 | HOMEDIR=/var/lib/snapserver |
6 | 6 | |
7 | 7 | 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 | |
9 | 9 | |
10 | 10 | if [ ! -d $HOMEDIR ]; then |
11 | 11 | mkdir -m 0750 $HOMEDIR |
3 | 3 | |
4 | 4 | #DEBHELPER# |
5 | 5 | |
6 | USERNAME=_snapserver | |
6 | USERNAME=snapserver | |
7 | 7 | HOMEDIR=/var/lib/snapserver |
8 | 8 | |
9 | 9 | if [ "$1" = "purge" ]; then |
6 | 6 | [Service] |
7 | 7 | EnvironmentFile=-/etc/default/snapserver |
8 | 8 | ExecStart=/usr/bin/snapserver $SNAPSERVER_OPTS |
9 | User=_snapserver | |
10 | Group=_snapserver | |
9 | User=snapserver | |
10 | Group=snapserver | |
11 | 11 | Restart=on-failure |
12 | 12 | |
13 | 13 | [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. |
21 | 21 | $ cd <snapcast dir>/externals |
22 | 22 | $ git submodule update --init --recursive |
23 | 23 | |
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` | |
24 | 26 | |
25 | 27 | ## Linux (Native) |
26 | 28 | Install the build tools and required libs: |
27 | 29 | For Debian derivates (e.g. Raspbian, Debian, Ubuntu, Mint): |
28 | 30 | |
29 | 31 | $ 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 | |
31 | 33 | |
32 | 34 | Compilation requires gcc 4.8 or higher, so it's highly recommended to use Debian (Raspbian) Jessie. |
33 | 35 | |
34 | 36 | For Arch derivates: |
35 | 37 | |
36 | 38 | $ 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): | |
40 | 42 | |
41 | 43 | $ 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 | |
43 | 45 | |
44 | 46 | ### Build Snapclient and Snapserver |
45 | 47 | `cd` into the Snapcast src-root directory: |
86 | 88 | $ cd <snapcast dir> |
87 | 89 | $ fakeroot make -f debian/rules binary |
88 | 90 | |
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 | ||
89 | 95 | ## FreeBSD (Native) |
90 | 96 | Install the build tools and required libs: |
91 | 97 | |
92 | $ sudo pkg install gmake gcc bash avahi libogg libvorbis flac | |
98 | $ sudo pkg install gmake gcc bash avahi libogg libvorbis libopus flac | |
93 | 99 | |
94 | 100 | ### Build Snapserver |
95 | 101 | `cd` into the Snapserver src-root directory: |
138 | 144 | fi |
139 | 145 | echo 'media-sound/snapcast client server flac |
140 | 146 | |
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. | |
142 | 148 | |
143 | 149 | echo 'media-sound/snapcast client -server |
144 | 150 | |
167 | 173 | 3. Install the required libs |
168 | 174 | |
169 | 175 | ``` |
170 | $ brew install flac libvorbis | |
176 | $ brew install flac libvorbis boost opus | |
171 | 177 | ``` |
172 | 178 | |
173 | 179 | ### Build Snapclient |
206 | 212 | ``` |
207 | 213 | $ cd /SOME/LOCAL/PATH/android-ndk-r17/build/tools |
208 | 214 | $ ./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 | |
209 | 216 | $ ./make_standalone_toolchain.py --arch x86 --api 16 --stl libc++ --install-dir <android-ndk dir>-x86 |
210 | 217 | ``` |
211 | 218 | |
212 | 219 | ### 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): | |
214 | 221 | |
215 | 222 | $ cd <snapcast dir>/externals |
216 | 223 | $ make NDK_DIR=<android-ndk dir>-arm ARCH=arm |
224 | $ make NDK_DIR=<android-ndk dir>-arm64 ARCH=aarch64 | |
217 | 225 | $ make NDK_DIR=<android-ndk dir>-x86 ARCH=x86 |
218 | 226 | |
219 | 227 | Compile the Snapclient: |
220 | 228 | |
221 | 229 | $ 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. | |
225 | 233 | |
226 | 234 | |
227 | 235 | ## OpenWrt/LEDE (Cross compile) |
281 | 289 | $ BUILDROOT_VERSION=2016.11.2 |
282 | 290 | $ git clone --branch $BUILDROOT_VERSION --depth=1 git://git.buildroot.net/buildroot |
283 | 291 | |
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. | |
285 | 293 | |
286 | 294 | Now configure buildroot with the [required packages](/buildroot/configs/snapcast_defconfig) (you can also manually add them to your project's existing defconfig): |
287 | 295 |
45 | 45 | |
46 | 46 | #### Stream |
47 | 47 | ```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"}} | |
49 | 49 | ``` |
50 | 50 | |
51 | 51 | #### Server |
52 | 52 | ```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"}}]} | |
54 | 54 | ``` |
55 | 55 | |
56 | 56 | |
205 | 205 | |
206 | 206 | #### Response |
207 | 207 | ```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"}}]}}} | |
214 | 214 | ``` |
215 | 215 | ### Group.SetName |
216 | 216 | #### Request |
250 | 250 | |
251 | 251 | #### Response |
252 | 252 | ```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"}}]}}} | |
254 | 254 | ``` |
255 | 255 | |
256 | 256 | |
262 | 262 | |
263 | 263 | #### Response |
264 | 264 | ```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"}}]}}} | |
271 | 271 | ``` |
272 | 272 | |
273 | 273 | |
337 | 337 | |
338 | 338 | ### Stream.OnUpdate |
339 | 339 | ```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"}}}} | |
341 | 341 | ``` |
342 | 342 | |
343 | 343 | ### Server.OnUpdate |
344 | 344 | ```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 |
0 | 0 | Setup of audio players/server |
1 | 1 | ----------------------------- |
2 | 2 | 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). | |
4 | 4 | |
5 | 5 | The goal is to build the following chain: |
6 | 6 | |
7 | 7 | audio player software -> snapfifo -> snapserver -> network -> snapclient -> alsa |
8 | 8 | |
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 | ||
9 | 22 | #### About the notation |
10 | 23 | 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 | |
12 | 25 | * `[square brackets]`: the whole expression is optional and can be left out |
13 | 26 | * `[key=value]`: if you leave this option out, `value` will be the default for `key` |
14 | 27 | |
15 | 28 | For example: |
16 | 29 | ``` |
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] | |
18 | 31 | ``` |
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. | |
21 | 35 | |
22 | A valid usage would be for instance: | |
36 | For instance, a valid usage would be: | |
23 | 37 | ``` |
24 | -s "spotify:///librespot?name=Spotify&bitrate=160" | |
38 | stream = spotify:///librespot?name=Spotify&bitrate=160 | |
25 | 39 | ``` |
26 | 40 | |
27 | 41 | ### 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. | |
29 | 43 | |
30 | 44 | Disable alsa audio output by commenting out this section: |
31 | 45 | |
40 | 54 | #} |
41 | 55 | |
42 | 56 | 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). | |
44 | 58 | |
45 | 59 | audio_output { |
46 | 60 | type "fifo" |
62 | 76 | #output = autoaudiosink |
63 | 77 | output = audioresample ! audioconvert ! audio/x-raw,rate=48000,channels=2,format=S16LE ! wavenc ! filesink location=/tmp/snapfifo |
64 | 78 | |
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` | |
66 | 80 | |
67 | 81 | 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. |
68 | 82 | |
114 | 128 | ### PulseAudio |
115 | 129 | Redirect the PulseAudio stream into the snapfifo: |
116 | 130 | |
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 | |
118 | 132 | |
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. | |
120 | 134 | |
121 | 135 | Load the module `pipe-sink` like this: |
122 | 136 | |
123 | 137 | pacmd load-module module-pipe-sink file=/tmp/snapfifo sink_name=Snapcast rate=48000 |
124 | 138 | pacmd update-sink-proplist Snapcast device.description=Snapcast |
125 | 139 | |
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` | |
127 | 141 | |
128 | 142 | |
129 | 143 | ### 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. | |
131 | 145 | 1. Build shairport-sync with `stdout` backend: `./configure --with-stdout --with-avahi --with-ssl=openssl --with-metadata` |
132 | 146 | 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]` | |
134 | 148 | |
135 | 149 | |
136 | 150 | ### 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. | |
138 | 152 | 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>]` | |
140 | 154 | * Valid bitrates are 96, 160, 320 |
141 | 155 | * `start command` and `stop command` are executed by Librespot at start/stop |
142 | 156 | * For example: `onstart=/usr/bin/logger -t Snapcast Starting spotify...` |
145 | 159 | ### Process |
146 | 160 | Snapserver can start any process and read PCM data from the stdout of the process: |
147 | 161 | |
148 | Configure snapserver with `-s "process:///path/to/process?name=Process[¶ms=<--my list --of params>][&logStderr=false]"` | |
162 | Configure snapserver with `stream = process:///path/to/process?name=Process[¶ms=<--my list --of params>][&log_stderr=false]` | |
149 | 163 | |
150 | 164 | |
151 | 165 | ### Line-in |
155 | 169 | [cpipe](https://github.com/b-fitzpatrick/cpiped) |
156 | 170 | |
157 | 171 | #### PulseAudio |
158 | `parec >/tmp/snapfifo` (defaults to 44.1kHz,16bit,stereo) | |
172 | `parec >/tmp/snapfifo` (defaults to 44.1kHz, 16bit, stereo) |
0 | 0 | # This file is part of snapcast |
1 | # Copyright (C) 2014-2019 Johannes Pohl | |
1 | # Copyright (C) 2014-2020 Johannes Pohl | |
2 | 2 | # |
3 | 3 | # This program is free software: you can redistribute it and/or modify |
4 | 4 | # it under the terms of the GNU General Public License as published by |
13 | 13 | # You should have received a copy of the GNU General Public License |
14 | 14 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
15 | 15 | |
16 | .PHONY: all check-env flac ogg tremor | |
16 | .PHONY: all check-env flac ogg opus tremor | |
17 | 17 | |
18 | all: flac ogg tremor | |
18 | all: flac ogg opus tremor | |
19 | 19 | |
20 | 20 | check-env: |
21 | 21 | # if [ ! -d "flac" ]; then \ |
31 | 31 | $(error android NDK_DIR is not set) |
32 | 32 | endif |
33 | 33 | ifndef ARCH |
34 | $(error ARCH is not set ("arm" or "x86")) | |
34 | $(error ARCH is not set ("arm" or "aarch64" or "x86")) | |
35 | 35 | endif |
36 | 36 | ifeq ($(ARCH), x86) |
37 | 37 | $(eval CPPFLAGS:=-DLITTLE_ENDIAN=1234 -DBIG_ENDIAN=4321 -DBYTE_ORDER=LITTLE_ENDIAN) |
39 | 39 | else ifeq ($(ARCH), arm) |
40 | 40 | $(eval CPPFLAGS:=-U_ARM_ASSEM_) |
41 | 41 | $(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-) | |
42 | 45 | else |
43 | $(error ARCH must be "arm" or "x86") | |
46 | $(error ARCH must be "arm" or "aarch64" or "x86") | |
44 | 47 | endif |
45 | 48 | $(eval CC:=$(PROGRAM_PREFIX)clang) |
46 | 49 | $(eval CXX:=$(PROGRAM_PREFIX)clang++) |
59 | 62 | |
60 | 63 | ogg: check-env |
61 | 64 | @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; \ | |
62 | 76 | export CC="$(CC)"; \ |
63 | 77 | export CXX="$(CXX)"; \ |
64 | 78 | export CPPFLAGS="$(CPPFLAGS)"; \ |
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 |
11 | 11 | streamreader/stream_uri.cpp |
12 | 12 | streamreader/stream_manager.cpp |
13 | 13 | streamreader/pcm_stream.cpp |
14 | streamreader/tcp_stream.cpp | |
14 | 15 | streamreader/pipe_stream.cpp |
16 | streamreader/posix_stream.cpp | |
15 | 17 | streamreader/file_stream.cpp |
16 | 18 | streamreader/airplay_stream.cpp |
17 | 19 | streamreader/librespot_stream.cpp |
58 | 60 | list(APPEND SERVER_INCLUDE ${FLAC_INCLUDE_DIRS}) |
59 | 61 | endif (FLAC_FOUND) |
60 | 62 | |
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 | ||
61 | 69 | #list(APPEND SERVER_LIBRARIES Boost::boost) |
62 | 70 | |
63 | 71 | include_directories(${SERVER_INCLUDE}) |
0 | 0 | # This file is part of snapcast |
1 | # Copyright (C) 2014-2019 Johannes Pohl | |
1 | # Copyright (C) 2014-2020 Johannes Pohl | |
2 | 2 | # |
3 | 3 | # This program is free software: you can redistribute it and/or modify |
4 | 4 | # it under the terms of the GNU General Public License as published by |
13 | 13 | # You should have received a copy of the GNU General Public License |
14 | 14 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
15 | 15 | |
16 | VERSION = 0.16.0 | |
16 | VERSION = 0.18.0 | |
17 | 17 | BIN = snapserver |
18 | 18 | |
19 | 19 | ifeq ($(TARGET), FREEBSD) |
29 | 29 | TARGET_DIR ?= /usr |
30 | 30 | endif |
31 | 31 | |
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 | |
40 | 47 | |
41 | 48 | ifneq (,$(TARGET)) |
42 | 49 | CXXFLAGS += -D$(TARGET) |
49 | 56 | ifeq ($(TARGET), ANDROID) |
50 | 57 | |
51 | 58 | CXX = $(NDK_DIR)/bin/arm-linux-androideabi-g++ |
52 | STRIP = $(NDK_DIR)/bin/arm-linux-androideabi-strip | |
53 | 59 | CXXFLAGS += -pthread -DNO_CPP11_STRING -fPIC -I$(NDK_DIR)/include |
54 | 60 | LDFLAGS += -L$(NDK_DIR)/lib -pie -llog -latomic |
55 | 61 | |
56 | 62 | else ifeq ($(TARGET), OPENWRT) |
57 | 63 | |
58 | STRIP = echo | |
59 | 64 | CXXFLAGS += -DNO_CPP11_STRING -DHAS_AVAHI -DHAS_DAEMON -pthread |
60 | 65 | LDFLAGS += -lavahi-client -lavahi-common -latomic |
61 | 66 | OBJ += ../common/daemon.o publishZeroConf/publish_avahi.o |
69 | 74 | else ifeq ($(TARGET), FREEBSD) |
70 | 75 | |
71 | 76 | CXX = g++ |
72 | STRIP = echo | |
73 | 77 | CXXFLAGS += -DFREEBSD -DNO_CPP11_STRING -DHAS_AVAHI -DHAS_DAEMON -pthread |
74 | 78 | LDFLAGS += -lrt -lavahi-client -lavahi-common -latomic |
75 | 79 | OBJ += ../common/daemon.o publishZeroConf/publish_avahi.o |
77 | 81 | else ifeq ($(TARGET), MACOS) |
78 | 82 | |
79 | 83 | CXX = g++ |
80 | STRIP = strip | |
81 | 84 | CXXFLAGS += -DFREEBSD -DHAS_BONJOUR -DHAS_DAEMON -Wno-deprecated -I/usr/local/include |
82 | 85 | LDFLAGS += -L/usr/local/lib -framework CoreFoundation -framework IOKit |
83 | 86 | OBJ += ../common/daemon.o publishZeroConf/publish_bonjour.o |
85 | 88 | else |
86 | 89 | |
87 | 90 | CXX = g++ |
88 | STRIP = echo | |
89 | 91 | CXXFLAGS += -DHAS_AVAHI -DHAS_DAEMON -pthread |
90 | LDFLAGS += -lrt -lavahi-client -lavahi-common | |
92 | LDFLAGS += -lrt -lavahi-client -lavahi-common -latomic | |
91 | 93 | OBJ += ../common/daemon.o publishZeroConf/publish_avahi.o |
92 | 94 | |
93 | 95 | endif |
102 | 104 | |
103 | 105 | $(BIN): $(OBJ) |
104 | 106 | $(CXX) $(CXXFLAGS) -o $(BIN) $(OBJ) $(LDFLAGS) |
105 | $(STRIP) $(BIN) | |
106 | 107 | |
107 | 108 | %.o: %.cpp |
108 | 109 | $(CXX) $(CXXFLAGS) -c $< -o $@ |
119 | 120 | |
120 | 121 | install: |
121 | 122 | 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) | |
123 | 124 | 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) | |
125 | 126 | |
126 | 127 | else ifeq ($(TARGET), MACOS) |
127 | 128 | |
128 | 129 | install: |
129 | 130 | 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) | |
131 | 132 | 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 | |
133 | 135 | launchctl load /Library/LaunchAgents/de.badaix.snapcast.$(BIN).plist |
134 | 136 | |
135 | 137 | else |
152 | 154 | endif |
153 | 155 | |
154 | 156 | 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) | |
156 | 158 | install -D -g root -o root $(BIN).1 $(TARGET_DIR)/share/man/man1/$(BIN).1 |
157 | 159 | |
158 | 160 | installsystemd: |
159 | 161 | @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; \ | |
163 | 165 | systemctl daemon-reload; \ |
164 | 166 | systemctl enable $(BIN); \ |
165 | 167 | systemctl start $(BIN); \ |
166 | 168 | |
167 | 169 | installsysv: |
168 | 170 | @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; \ | |
172 | 174 | update-rc.d $(BIN) defaults; \ |
173 | 175 | /etc/init.d/$(BIN) start; \ |
174 | 176 | |
175 | 177 | installbsd: |
176 | 178 | @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); \ | |
178 | 180 | |
179 | 181 | adduser: |
180 | 182 | @if ! getent passwd snapserver >/dev/null; then \ |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
15 | 15 | along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | ***/ |
17 | 17 | |
18 | #include "config.h" | |
18 | #include "config.hpp" | |
19 | 19 | #include "common/aixlog.hpp" |
20 | 20 | #include "common/snap_exception.hpp" |
21 | 21 | #include "common/str_compat.hpp" |
120 | 120 | init(); |
121 | 121 | std::ofstream ofs(filename_.c_str(), std::ofstream::out | std::ofstream::trunc); |
122 | 122 | json clients = {{"ConfigVersion", 2}, {"Groups", getGroups()}}; |
123 | ofs << std::setw(4) << clients; | |
123 | // ofs << std::setw(4) << clients; | |
124 | ofs << clients; | |
124 | 125 | ofs.close(); |
125 | 126 | } |
126 | 127 |
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 |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
19 | 19 | #include "common/aixlog.hpp" |
20 | 20 | #include "common/snap_exception.hpp" |
21 | 21 | #include "common/utils.hpp" |
22 | #include "config.h" | |
22 | #include "config.hpp" | |
23 | 23 | #include "control_session_http.hpp" |
24 | 24 | #include "control_session_tcp.hpp" |
25 | 25 | #include "jsonrpcpp.hpp" |
31 | 31 | using json = nlohmann::json; |
32 | 32 | |
33 | 33 | |
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, | |
35 | 35 | const ServerSettings::HttpSettings& http_settings, ControlMessageReceiver* controlMessageReceiver) |
36 | 36 | : io_context_(io_context), tcp_settings_(tcp_settings), http_settings_(http_settings), controlMessageReceiver_(controlMessageReceiver) |
37 | 37 | { |
73 | 73 | |
74 | 74 | std::string ControlServer::onMessageReceived(ControlSession* connection, const std::string& message) |
75 | 75 | { |
76 | std::lock_guard<std::recursive_mutex> mlock(session_mutex_); | |
77 | LOG(DEBUG) << "received: \"" << message << "\"\n"; | |
76 | // LOG(DEBUG) << "received: \"" << message << "\"\n"; | |
78 | 77 | if (controlMessageReceiver_ != nullptr) |
79 | 78 | return controlMessageReceiver_->onMessageReceived(connection, message); |
80 | 79 | return ""; |
117 | 116 | setsockopt(socket.native_handle(), SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); |
118 | 117 | // socket->set_option(boost::asio::ip::tcp::no_delay(false)); |
119 | 118 | 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)...); | |
121 | 120 | { |
122 | 121 | std::lock_guard<std::recursive_mutex> mlock(session_mutex_); |
123 | 122 | session->start(); |
144 | 143 | { |
145 | 144 | LOG(INFO) << "Creating TCP acceptor for address: " << address << ", port: " << tcp_settings_.port << "\n"; |
146 | 145 | 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))); | |
148 | 147 | } |
149 | 148 | catch (const boost::system::system_error& e) |
150 | 149 | { |
160 | 159 | { |
161 | 160 | LOG(INFO) << "Creating HTTP acceptor for address: " << address << ", port: " << http_settings_.port << "\n"; |
162 | 161 | 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))); | |
164 | 163 | } |
165 | 164 | catch (const boost::system::system_error& e) |
166 | 165 | { |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
22 | 22 | #include <memory> |
23 | 23 | #include <mutex> |
24 | 24 | #include <set> |
25 | #include <sstream> | |
26 | #include <thread> | |
27 | 25 | #include <vector> |
28 | 26 | |
29 | 27 | #include "common/queue.h" |
44 | 42 | class ControlServer : public ControlMessageReceiver |
45 | 43 | { |
46 | 44 | 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, | |
48 | 46 | ControlMessageReceiver* controlMessageReceiver = nullptr); |
49 | 47 | virtual ~ControlServer(); |
50 | 48 | |
70 | 68 | std::vector<acceptor_ptr> acceptor_tcp_; |
71 | 69 | std::vector<acceptor_ptr> acceptor_http_; |
72 | 70 | |
73 | boost::asio::io_context* io_context_; | |
71 | boost::asio::io_context& io_context_; | |
74 | 72 | ServerSettings::TcpSettings tcp_settings_; |
75 | 73 | ServerSettings::HttpSettings http_settings_; |
76 | 74 | ControlMessageReceiver* controlMessageReceiver_; |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
28 | 28 | #include <mutex> |
29 | 29 | #include <set> |
30 | 30 | #include <string> |
31 | #include <thread> | |
32 | 31 | |
33 | 32 | using boost::asio::ip::tcp; |
34 | 33 |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
97 | 97 | } |
98 | 98 | } // namespace |
99 | 99 | |
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) | |
102 | 103 | { |
103 | 104 | LOG(DEBUG) << "ControlSessionHttp\n"; |
104 | 105 | } |
114 | 115 | void ControlSessionHttp::start() |
115 | 116 | { |
116 | 117 | 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); })); | |
118 | 120 | } |
119 | 121 | |
120 | 122 | |
269 | 271 | |
270 | 272 | // Write the response |
271 | 273 | 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 | })); | |
273 | 277 | }); |
274 | 278 | } |
275 | 279 | |
296 | 300 | req_ = {}; |
297 | 301 | |
298 | 302 | // 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); })); | |
300 | 305 | } |
301 | 306 | |
302 | 307 | |
304 | 309 | { |
305 | 310 | } |
306 | 311 | |
307 | ||
308 | 312 | void ControlSessionHttp::sendAsync(const std::string& message) |
309 | 313 | { |
310 | 314 | if (!ws_) |
311 | 315 | return; |
312 | 316 | |
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 | ||
313 | 333 | 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 | })); | |
324 | 348 | } |
325 | 349 | |
326 | 350 | |
350 | 374 | { |
351 | 375 | // Read a message into our buffer |
352 | 376 | 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); })); | |
354 | 379 | } |
355 | 380 | |
356 | 381 | |
371 | 396 | std::string line{boost::beast::buffers_to_string(buffer_.data())}; |
372 | 397 | if (!line.empty()) |
373 | 398 | { |
374 | LOG(DEBUG) << "received: " << line << "\n"; | |
399 | // LOG(DEBUG) << "received: " << line << "\n"; | |
375 | 400 | if ((message_receiver_ != nullptr) && !line.empty()) |
376 | 401 | { |
377 | 402 | string response = message_receiver_->onMessageReceived(this, line); |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
21 | 21 | #include "control_session.hpp" |
22 | 22 | #include <boost/beast/core.hpp> |
23 | 23 | #include <boost/beast/websocket.hpp> |
24 | #include <deque> | |
24 | 25 | |
25 | 26 | namespace beast = boost::beast; // from <boost/beast.hpp> |
26 | 27 | namespace http = beast::http; // from <boost/beast/http.hpp> |
38 | 39 | { |
39 | 40 | public: |
40 | 41 | /// 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); | |
42 | 43 | ~ControlSessionHttp() override; |
43 | 44 | void start() override; |
44 | 45 | void stop() override; |
57 | 58 | template <class Body, class Allocator, class Send> |
58 | 59 | void handle_request(http::request<Body, http::basic_fields<Allocator>>&& req, Send&& send); |
59 | 60 | |
61 | void send_next(); | |
62 | ||
60 | 63 | http::request<http::string_body> req_; |
61 | 64 | |
62 | 65 | protected: |
71 | 74 | tcp::socket socket_; |
72 | 75 | beast::flat_buffer buffer_; |
73 | 76 | ServerSettings::HttpSettings settings_; |
77 | boost::asio::io_context::strand strand_; | |
78 | std::deque<std::string> messages_; | |
74 | 79 | }; |
75 | 80 | |
76 | 81 |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
21 | 21 | |
22 | 22 | using namespace std; |
23 | 23 | |
24 | // https://stackoverflow.com/questions/7754695/boost-asio-async-write-how-to-not-interleaving-async-write-calls/7756894 | |
24 | 25 | |
25 | 26 | |
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) | |
27 | 29 | { |
28 | 30 | } |
29 | 31 | |
39 | 41 | { |
40 | 42 | const std::string delimiter = "\n"; |
41 | 43 | 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 | } | |
48 | 51 | |
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()) | |
57 | 55 | { |
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 | } | |
61 | 65 | } |
62 | } | |
63 | streambuf_.consume(bytes_transferred); | |
64 | do_read(); | |
65 | }); | |
66 | streambuf_.consume(bytes_transferred); | |
67 | do_read(); | |
68 | })); | |
66 | 69 | } |
67 | 70 | |
68 | 71 | |
88 | 91 | |
89 | 92 | void ControlSessionTcp::sendAsync(const std::string& message) |
90 | 93 | { |
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) | |
94 | 97 | { |
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; | |
96 | 100 | } |
97 | else | |
98 | { | |
99 | LOG(DEBUG) << "Wrote " << length << " bytes to control socket\n"; | |
100 | } | |
101 | send_next(); | |
101 | 102 | }); |
102 | 103 | } |
103 | 104 | |
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 | } | |
104 | 124 | |
105 | 125 | bool ControlSessionTcp::send(const std::string& message) |
106 | 126 | { |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
19 | 19 | #define CONTROL_SESSION_TCP_HPP |
20 | 20 | |
21 | 21 | #include "control_session.hpp" |
22 | #include <deque> | |
22 | 23 | |
23 | 24 | /// Endpoint for a connected control client. |
24 | 25 | /** |
30 | 31 | { |
31 | 32 | public: |
32 | 33 | /// 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); | |
34 | 35 | ~ControlSessionTcp() override; |
35 | 36 | void start() override; |
36 | 37 | void stop() override; |
43 | 44 | |
44 | 45 | protected: |
45 | 46 | void do_read(); |
47 | void send_next(); | |
48 | ||
46 | 49 | tcp::socket socket_; |
47 | 50 | boost::asio::streambuf streambuf_; |
51 | boost::asio::io_context::strand strand_; | |
52 | std::deque<std::string> messages_; | |
48 | 53 | }; |
49 | 54 | |
50 | 55 |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
25 | 25 | #include "message/codec_header.hpp" |
26 | 26 | #include "message/pcm_chunk.hpp" |
27 | 27 | |
28 | namespace encoder | |
29 | { | |
28 | 30 | |
29 | 31 | class Encoder; |
30 | 32 | |
95 | 97 | std::string codecOptions_; |
96 | 98 | }; |
97 | 99 | |
100 | } // namespace encoder | |
98 | 101 | |
99 | 102 | #endif |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
17 | 17 | |
18 | 18 | #include "encoder_factory.hpp" |
19 | 19 | #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) | |
21 | 21 | #include "ogg_encoder.hpp" |
22 | 22 | #endif |
23 | 23 | #if defined(HAS_FLAC) |
24 | 24 | #include "flac_encoder.hpp" |
25 | #endif | |
26 | #if defined(HAS_OPUS) | |
27 | #include "opus_encoder.hpp" | |
25 | 28 | #endif |
26 | 29 | #include "common/aixlog.hpp" |
27 | 30 | #include "common/snap_exception.hpp" |
30 | 33 | |
31 | 34 | using namespace std; |
32 | 35 | |
36 | namespace encoder | |
37 | { | |
33 | 38 | |
34 | Encoder* EncoderFactory::createEncoder(const std::string& codecSettings) const | |
39 | std::unique_ptr<Encoder> EncoderFactory::createEncoder(const std::string& codecSettings) const | |
35 | 40 | { |
36 | Encoder* encoder; | |
37 | 41 | std::string codec(codecSettings); |
38 | 42 | std::string codecOptions; |
39 | 43 | if (codec.find(":") != std::string::npos) |
42 | 46 | codec = utils::string::trim_copy(codec.substr(0, codec.find(":"))); |
43 | 47 | } |
44 | 48 | 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) | |
47 | 51 | else if (codec == "ogg") |
48 | encoder = new OggEncoder(codecOptions); | |
52 | return std::make_unique<OggEncoder>(codecOptions); | |
49 | 53 | #endif |
50 | 54 | #if defined(HAS_FLAC) |
51 | 55 | else if (codec == "flac") |
52 | encoder = new FlacEncoder(codecOptions); | |
56 | return std::make_unique<FlacEncoder>(codecOptions); | |
53 | 57 | #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 | |
58 | 62 | |
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); | |
70 | 64 | } |
65 | ||
66 | } // namespace encoder |
1 | 1 | #define ENCODER_FACTORY_H |
2 | 2 | |
3 | 3 | #include "encoder.hpp" |
4 | #include <memory> | |
4 | 5 | #include <string> |
6 | ||
7 | namespace encoder | |
8 | { | |
5 | 9 | |
6 | 10 | class EncoderFactory |
7 | 11 | { |
8 | 12 | public: |
9 | 13 | // EncoderFactory(const std::string& codecSettings); |
10 | Encoder* createEncoder(const std::string& codecSettings) const; | |
14 | std::unique_ptr<Encoder> createEncoder(const std::string& codecSettings) const; | |
11 | 15 | }; |
12 | 16 | |
17 | } // namespace encoder | |
13 | 18 | |
14 | 19 | #endif |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
24 | 24 | |
25 | 25 | using namespace std; |
26 | 26 | |
27 | namespace encoder | |
28 | { | |
27 | 29 | |
28 | 30 | FlacEncoder::FlacEncoder(const std::string& codecOptions) : Encoder(codecOptions), encoder_(nullptr), pcmBufferSize_(0), encodedSamples_(0) |
29 | 31 | { |
132 | 134 | return FLAC__STREAM_ENCODER_WRITE_STATUS_OK; |
133 | 135 | } |
134 | 136 | |
135 | ||
137 | namespace callback | |
138 | { | |
136 | 139 | FLAC__StreamEncoderWriteStatus write_callback(const FLAC__StreamEncoder* encoder, const FLAC__byte buffer[], size_t bytes, unsigned samples, |
137 | 140 | unsigned current_frame, void* client_data) |
138 | 141 | { |
139 | 142 | FlacEncoder* flacEncoder = (FlacEncoder*)client_data; |
140 | 143 | return flacEncoder->write_callback(encoder, buffer, bytes, samples, current_frame); |
141 | 144 | } |
142 | ||
145 | } // namespace callback | |
143 | 146 | |
144 | 147 | void FlacEncoder::initEncoder() |
145 | 148 | { |
195 | 198 | throw SnapException("error setting meta data"); |
196 | 199 | |
197 | 200 | // 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); | |
199 | 202 | if (init_status != FLAC__STREAM_ENCODER_INIT_STATUS_OK) |
200 | 203 | throw SnapException("ERROR: initializing encoder: " + string(FLAC__StreamEncoderInitStatusString[init_status])); |
201 | 204 | } |
205 | ||
206 | } // namespace encoder |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
25 | 25 | #include "FLAC/metadata.h" |
26 | 26 | #include "FLAC/stream_encoder.h" |
27 | 27 | |
28 | namespace encoder | |
29 | { | |
28 | 30 | |
29 | 31 | class FlacEncoder : public Encoder |
30 | 32 | { |
52 | 54 | size_t encodedSamples_; |
53 | 55 | }; |
54 | 56 | |
57 | } // namespace encoder | |
55 | 58 | |
56 | 59 | #endif |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
27 | 27 | |
28 | 28 | using namespace std; |
29 | 29 | |
30 | namespace encoder | |
31 | { | |
30 | 32 | |
31 | 33 | OggEncoder::OggEncoder(const std::string& codecOptions) : Encoder(codecOptions), lastGranulepos_(0) |
32 | 34 | { |
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_); | |
33 | 45 | } |
34 | 46 | |
35 | 47 | |
256 | 268 | pos += og_.body_len; |
257 | 269 | } |
258 | 270 | } |
271 | ||
272 | } // namespace encoder |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
21 | 21 | #include <ogg/ogg.h> |
22 | 22 | #include <vorbis/vorbisenc.h> |
23 | 23 | |
24 | namespace encoder | |
25 | { | |
26 | ||
24 | 27 | class OggEncoder : public Encoder |
25 | 28 | { |
26 | 29 | public: |
27 | 30 | OggEncoder(const std::string& codecOptions = ""); |
31 | ~OggEncoder() override; | |
32 | ||
28 | 33 | void encode(const msg::PcmChunk* chunk) override; |
29 | 34 | std::string getAvailableOptions() const override; |
30 | 35 | std::string getDefaultOptions() const override; |
47 | 52 | ogg_int64_t lastGranulepos_; |
48 | 53 | }; |
49 | 54 | |
55 | } // namespace encoder | |
50 | 56 | |
51 | 57 | #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 |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
20 | 20 | #include <memory> |
21 | 21 | |
22 | 22 | |
23 | namespace encoder | |
24 | { | |
25 | ||
23 | 26 | #define ID_RIFF 0x46464952 |
24 | 27 | #define ID_WAVE 0x45564157 |
25 | 28 | #define ID_FMT 0x20746d66 |
26 | 29 | #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 | |
27 | 41 | |
28 | 42 | |
29 | 43 | PcmEncoder::PcmEncoder(const std::string& codecOptions) : Encoder(codecOptions) |
64 | 78 | { |
65 | 79 | return "pcm"; |
66 | 80 | } |
81 | ||
82 | } // namespace encoder |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
19 | 19 | #define PCM_ENCODER_H |
20 | 20 | #include "encoder.hpp" |
21 | 21 | |
22 | namespace encoder | |
23 | { | |
22 | 24 | |
23 | 25 | class PcmEncoder : public Encoder |
24 | 26 | { |
29 | 31 | |
30 | 32 | protected: |
31 | 33 | 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 | } | |
39 | 34 | }; |
40 | 35 | |
36 | } // namespace encoder | |
41 | 37 | |
42 | 38 | #endif |
1 | 1 | |
2 | 2 | <head> |
3 | 3 | <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 | |
5 | 6 | <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 | ||
7 | 10 | var server; |
8 | 11 | |
9 | 12 | 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); | |
13 | 15 | 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 | ||
22 | 28 | show() |
23 | 29 | } |
24 | 30 | |
30 | 36 | alert("error"); |
31 | 37 | } |
32 | 38 | |
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 | ||
33 | 67 | 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) { | |
43 | 87 | 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++) { | |
58 | 97 | if (server.server.groups[i_group].clients[i_client].id == params.client.id) { |
59 | 98 | server.server.groups[i_group].clients[i_client] = params.client; |
60 | 99 | // console.log(server.server.groups[i_group].clients[i_client]); |
61 | 100 | } |
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++) { | |
72 | 108 | if (server.server.groups[i_group].id == params.id) { |
73 | 109 | 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++) { | |
84 | 117 | if (server.server.groups[i_group].id == params.id) { |
85 | 118 | 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;) { | |
96 | 126 | if (server.server.streams[i_stream].id == params.id) { |
97 | 127 | server.server.streams[i_stream] = params.stream; |
98 | 128 | // console.log(server.server.streams[i_stream]); |
99 | 129 | } |
100 | i_stream++ | |
130 | ||
101 | 131 | } |
102 | 132 | } |
103 | 133 | |
104 | 134 | function show() { |
105 | var i_group = 0; | |
135 | // Render the page | |
106 | 136 | var content = ""; |
107 | 137 | |
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; | |
110 | 141 | 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 | ||
126 | 142 | var mutetext; |
127 | ||
128 | 143 | if (server.server.groups[i_group].muted == true) { |
144 | classgroup = 'groupmuted'; | |
129 | 145 | unmuted = 'false'; |
130 | 146 | mutetext = '🔇'; |
131 | } | |
132 | if (server.server.groups[i_group].muted == false) { | |
147 | } else { | |
148 | classgroup = 'group'; | |
133 | 149 | unmuted = 'true'; |
134 | 150 | mutetext = '🔊'; |
135 | 151 | } |
136 | 152 | |
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++) { | |
143 | 176 | var sv = server.server.groups[i_group].clients[i_client]; |
144 | 177 | |
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 = '🔇'; | |
199 | } else { | |
200 | sliderclass = 'slider' | |
201 | unmuted = 'true'; | |
202 | mutetext = '🔊'; | |
203 | } | |
204 | ||
205 | // Client group selection vars | |
145 | 206 | 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++) { | |
149 | 208 | var groupselected = ""; |
150 | if (o_group == i_group) { groupselected = 'selected' } | |
151 | ||
209 | if (o_group == i_group) { | |
210 | groupselected = 'selected' | |
211 | } | |
152 | 212 | 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 | ||
155 | 215 | groupselect = groupselect + "<option value='new'>new</option>"; |
156 | 216 | groupselect = groupselect + "</select>" |
157 | 217 | |
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 = '🔇'; | |
172 | } | |
173 | if (sv.config.volume.muted == false) { | |
174 | unmuted = 'true'; | |
175 | mutetext = '🔊'; | |
176 | } | |
218 | // Populate client div | |
177 | 219 | 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 | ||
183 | 220 | 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>"; |
184 | 221 | 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>"; |
185 | 222 | content = content + " <a href=\"javascript:setName('" + sv.id + "');\" class='edit'>✎</a>"; |
186 | 223 | content = content + name; |
187 | // content=content+" Connected:"+sv.connected; | |
188 | 224 | content = content + groupselect; |
189 | 225 | content = content + "</div>"; |
190 | ||
191 | i_client++ | |
192 | } | |
226 | } | |
227 | ||
193 | 228 | content = content + "</div>" |
194 | i_group++ | |
195 | } | |
196 | ||
229 | } | |
230 | ||
231 | // Pad then update page | |
197 | 232 | content = content + "<br><br>"; |
198 | 233 | document.getElementById('show').innerHTML = content; |
199 | 234 | } |
202 | 237 | percent = document.getElementById('vol_' + id).value; |
203 | 238 | percent_fine = document.getElementById('vol_fine_' + id).value; |
204 | 239 | |
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 | |
206 | 242 | 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 | |
210 | 252 | send('{"id":8,"jsonrpc":"2.0","method":"Client.SetVolume","params":{"id":"' + id + '","volume":{"muted":' + mute + ',"percent":' + percent + '}}}') |
211 | 253 | |
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++) { | |
217 | 257 | var sv = server.server.groups[i_group].clients[i_client]; |
218 | ||
219 | 258 | 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 | } | |
222 | 265 | 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 | } | |
232 | 269 | show() |
233 | 270 | } |
234 | ||
235 | ||
236 | ||
237 | 271 | |
238 | 272 | function setMuteGroup(id, what) { |
239 | 273 | 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++) { | |
242 | 276 | 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 | } | |
245 | 283 | // console.log(server.server.groups[i_group]); |
246 | 284 | } |
247 | i_group++ | |
248 | 285 | } |
249 | 286 | show() |
250 | 287 | } |
251 | 288 | |
252 | 289 | function setStream(id) { |
253 | ||
254 | 290 | send('{"id":4,"jsonrpc":"2.0","method":"Group.SetStream","params":{"id":"' + id + '","stream_id":"' + document.getElementById('stream_' + id).value + '"}}') |
255 | 291 | |
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++) { | |
259 | 293 | if (server.server.groups[i_group].id == id) { |
260 | 294 | server.server.groups[i_group].stream_id = document.getElementById('stream_' + id).value; |
261 | 295 | // console.log(server.server.groups[i_group]); |
262 | 296 | } |
263 | i_group++ | |
264 | 297 | } |
265 | 298 | show() |
266 | 299 | } |
267 | 300 | |
268 | 301 | function setGroup(id) { |
269 | 302 | group = document.getElementById('group_' + id).value; |
303 | ||
304 | // Get client group id | |
270 | 305 | 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 | |
281 | 320 | 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++) { | |
284 | 322 | 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++) { | |
287 | 324 | 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 { | |
291 | 326 | send_clients[send_clients.length] = server.server.groups[i_group].clients[i_client].id; |
292 | 327 | } |
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 | } | |
299 | 338 | |
300 | 339 | 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 | ||
306 | 340 | 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"}}') | |
308 | 341 | } |
309 | 342 | |
310 | 343 | function setName(id) { |
344 | // Get current name and lacency | |
311 | 345 | 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++) { | |
318 | 350 | var sv = server.server.groups[i_group].clients[i_client]; |
319 | 351 | 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 | } | |
322 | 357 | current_latency = sv.config.latency; |
323 | } | |
324 | i_client++ | |
325 | } | |
326 | i_group++ | |
358 | break groups; | |
359 | } | |
360 | } | |
327 | 361 | } |
328 | 362 | |
329 | 363 | var newName = window.prompt("New Name", current_name); |
330 | 364 | var newLatency = window.prompt("New Latency", current_latency); |
331 | 365 | |
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++) { | |
339 | 380 | var sv = server.server.groups[i_group].clients[i_client]; |
340 | ||
341 | 381 | if (sv.id == id) { |
342 | 382 | sv.config.name = newName; |
343 | 383 | sv.config.latency = newLatency; |
344 | 384 | } |
345 | i_client++ | |
346 | } | |
347 | i_group++ | |
385 | } | |
348 | 386 | } |
349 | 387 | show() |
350 | 388 | } |
389 | ||
351 | 390 | </script> |
352 | ||
353 | 391 | <style> |
354 | 392 | body { |
355 | 393 | background: #1f1f1f; |
454 | 492 | opacity: 0.27; |
455 | 493 | } |
456 | 494 | |
457 | ||
458 | 495 | .sliders { |
459 | 496 | text-align: left; |
460 | 497 | vertical-align: middle; |
462 | 499 | padding-top: 10px; |
463 | 500 | clear: both; |
464 | 501 | float: none; |
465 | ||
466 | 502 | } |
467 | 503 | |
468 | 504 | .sliderdiv { |
469 | 505 | display: inline-block; |
470 | ||
471 | 506 | padding-left: 40px; |
472 | 507 | text-align: left; |
473 | 508 | width: 20px; |
475 | 510 | |
476 | 511 | .sliderdiv_fine { |
477 | 512 | display: inline-block; |
478 | ||
479 | 513 | text-align: left; |
480 | 514 | width: 20px; |
481 | ||
482 | 515 | position: relative; |
483 | 516 | top: 0px; |
484 | 517 | left: 13px; |
504 | 537 | border: 1px solid #555555; |
505 | 538 | -moz-appearance: none; |
506 | 539 | -webkit-appearance: none; |
507 | appearancce: none; | |
508 | ||
540 | appearance: none; | |
509 | 541 | } |
510 | 542 | |
511 | 543 | .stream { |
516 | 548 | |
517 | 549 | .refreshbutton { |
518 | 550 | background-color: rgb(61, 61, 61); |
519 | ||
520 | 551 | font-size: 20px; |
521 | 552 | color: #e3e3e3; |
522 | 553 | border: 1px solid #555555; |
12 | 12 | |
13 | 13 | # default values are commented |
14 | 14 | # 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 | ############################################################################### | |
15 | 29 | |
16 | 30 | |
17 | 31 | # HTTP RPC #################################################################### |
31 | 45 | #port = 1780 |
32 | 46 | |
33 | 47 | # serve a website from the doc_root location |
34 | # doc_root = | |
48 | #doc_root = | |
35 | 49 | # |
36 | 50 | ############################################################################### |
37 | 51 | |
69 | 83 | #port = 1704 |
70 | 84 | |
71 | 85 | # 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 | |
73 | 107 | stream = pipe:///tmp/snapfifo?name=default |
108 | #stream = tcp://127.0.0.1?name=mopidy_tcp | |
74 | 109 | |
75 | 110 | # Default sample format |
76 | 111 | #sampleformat = 48000:16:2 |
77 | 112 | |
78 | 113 | # Default transport codec |
79 | # (flac|ogg|pcm)[:options] | |
114 | # (flac|ogg|opus|pcm)[:options] | |
80 | 115 | # Type codec:? to get codec specific options |
81 | 116 | #codec = flac |
82 | 117 | |
83 | # Default stream read buffer [ms] | |
84 | #stream_buffer = 20 | |
118 | # Default stream read chunk size [ms] | |
119 | #chunk_ms = 20 | |
85 | 120 | |
86 | 121 | # Buffer [ms] |
87 | 122 | #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>⏎ |
2 | 2 | _( )/ ___) / \ ( ( \( _ \( _ \ / __)( ) ( ) |
3 | 3 | / \) \\___ \( O )/ / ) / ) __/( (__(_ _)(_ _) |
4 | 4 | \____/(____/ \__/ \_)__)(__\_)(__) \___)(_) (_) |
5 | version 1.2.2 | |
5 | version 1.3.1 | |
6 | 6 | https://github.com/badaix/jsonrpcpp |
7 | 7 | |
8 | 8 | This file is part of jsonrpc++ |
18 | 18 | /// run-clang-tidy-3.8.py -header-filter='jsonrpcpp.hpp' |
19 | 19 | /// -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' |
20 | 20 | |
21 | #ifndef JSON_RPC_H | |
22 | #define JSON_RPC_H | |
21 | #ifndef JSON_RPC_HPP | |
22 | #define JSON_RPC_HPP | |
23 | 23 | |
24 | 24 | #include "json.hpp" |
25 | 25 | #include <cstring> |
32 | 32 | |
33 | 33 | namespace jsonrpcpp |
34 | 34 | { |
35 | ||
36 | 35 | |
37 | 36 | class Entity; |
38 | 37 | class Request; |
42 | 41 | class Error; |
43 | 42 | class Batch; |
44 | 43 | |
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>; | |
54 | 51 | |
55 | 52 | |
56 | 53 | class Entity |
73 | 70 | Entity(const Entity&) = default; |
74 | 71 | Entity& operator=(const Entity&) = default; |
75 | 72 | |
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; | |
83 | 80 | |
84 | 81 | virtual std::string type_str() const; |
85 | 82 | |
92 | 89 | protected: |
93 | 90 | entity_t entity; |
94 | 91 | }; |
95 | ||
96 | 92 | |
97 | 93 | |
98 | 94 | class NullableEntity : public Entity |
114 | 110 | }; |
115 | 111 | |
116 | 112 | |
117 | ||
118 | 113 | class Id : public Entity |
119 | 114 | { |
120 | 115 | public: |
140 | 135 | return out; |
141 | 136 | } |
142 | 137 | |
143 | value_t type() const | |
138 | const value_t& type() const | |
144 | 139 | { |
145 | 140 | return type_; |
146 | 141 | } |
150 | 145 | return int_id_; |
151 | 146 | } |
152 | 147 | |
153 | std::string string_id() const | |
148 | const std::string& string_id() const | |
154 | 149 | { |
155 | 150 | return string_id_; |
156 | 151 | } |
162 | 157 | }; |
163 | 158 | |
164 | 159 | |
165 | ||
166 | 160 | class Parameter : public NullableEntity |
167 | 161 | { |
168 | 162 | public: |
224 | 218 | }; |
225 | 219 | |
226 | 220 | |
227 | ||
228 | 221 | class Error : public NullableEntity |
229 | 222 | { |
230 | 223 | public: |
240 | 233 | return code_; |
241 | 234 | } |
242 | 235 | |
243 | std::string message() const | |
236 | const std::string& message() const | |
244 | 237 | { |
245 | 238 | return message_; |
246 | 239 | } |
247 | 240 | |
248 | Json data() const | |
241 | const Json& data() const | |
249 | 242 | { |
250 | 243 | return data_; |
251 | 244 | } |
255 | 248 | std::string message_; |
256 | 249 | Json data_; |
257 | 250 | }; |
258 | ||
259 | 251 | |
260 | 252 | |
261 | 253 | /// JSON-RPC 2.0 request |
272 | 264 | Json to_json() const override; |
273 | 265 | void parse_json(const Json& json) override; |
274 | 266 | |
275 | std::string method() const | |
267 | const std::string& method() const | |
276 | 268 | { |
277 | 269 | return method_; |
278 | 270 | } |
279 | 271 | |
280 | Parameter params() const | |
272 | const Parameter& params() const | |
281 | 273 | { |
282 | 274 | return params_; |
283 | 275 | } |
284 | 276 | |
285 | Id id() const | |
277 | const Id& id() const | |
286 | 278 | { |
287 | 279 | return id_; |
288 | 280 | } |
294 | 286 | }; |
295 | 287 | |
296 | 288 | |
297 | ||
298 | 289 | class RpcException : public std::exception |
299 | 290 | { |
300 | 291 | public: |
306 | 297 | protected: |
307 | 298 | std::runtime_error m_; |
308 | 299 | }; |
309 | ||
310 | 300 | |
311 | 301 | |
312 | 302 | class RpcEntityException : public RpcException, public Entity |
316 | 306 | RpcEntityException(const std::string& text); |
317 | 307 | Json to_json() const override = 0; |
318 | 308 | |
319 | Error error() const | |
309 | const Error& error() const | |
320 | 310 | { |
321 | 311 | return error_; |
322 | 312 | } |
327 | 317 | }; |
328 | 318 | |
329 | 319 | |
330 | ||
331 | 320 | class ParseErrorException : public RpcEntityException |
332 | 321 | { |
333 | 322 | public: |
335 | 324 | ParseErrorException(const std::string& data); |
336 | 325 | Json to_json() const override; |
337 | 326 | }; |
338 | ||
339 | 327 | |
340 | 328 | |
341 | 329 | // -32600 Invalid Request The JSON sent is not a valid Request object. |
347 | 335 | { |
348 | 336 | public: |
349 | 337 | RequestException(const Error& error, const Id& requestId = Id()); |
350 | RequestException(const RequestException& e) = default; | |
351 | 338 | Json to_json() const override; |
352 | 339 | |
353 | Id id() const | |
340 | const Id& id() const | |
354 | 341 | { |
355 | 342 | return id_; |
356 | 343 | } |
358 | 345 | protected: |
359 | 346 | Id id_; |
360 | 347 | }; |
361 | ||
362 | 348 | |
363 | 349 | |
364 | 350 | class InvalidRequestException : public RequestException |
371 | 357 | }; |
372 | 358 | |
373 | 359 | |
374 | ||
375 | 360 | class MethodNotFoundException : public RequestException |
376 | 361 | { |
377 | 362 | public: |
382 | 367 | }; |
383 | 368 | |
384 | 369 | |
385 | ||
386 | 370 | class InvalidParamsException : public RequestException |
387 | 371 | { |
388 | 372 | public: |
393 | 377 | }; |
394 | 378 | |
395 | 379 | |
396 | ||
397 | 380 | class InternalErrorException : public RequestException |
398 | 381 | { |
399 | 382 | public: |
402 | 385 | InternalErrorException(const char* data, const Id& requestId = Id()); |
403 | 386 | InternalErrorException(const std::string& data, const Id& requestId = Id()); |
404 | 387 | }; |
405 | ||
406 | 388 | |
407 | 389 | |
408 | 390 | class Response : public Entity |
418 | 400 | Json to_json() const override; |
419 | 401 | void parse_json(const Json& json) override; |
420 | 402 | |
421 | Id id() const | |
403 | const Id& id() const | |
422 | 404 | { |
423 | 405 | return id_; |
424 | 406 | } |
425 | 407 | |
426 | Json result() const | |
408 | const Json& result() const | |
427 | 409 | { |
428 | 410 | return result_; |
429 | 411 | } |
430 | 412 | |
431 | Error error() const | |
413 | const Error& error() const | |
432 | 414 | { |
433 | 415 | return error_; |
434 | 416 | } |
440 | 422 | }; |
441 | 423 | |
442 | 424 | |
443 | ||
444 | 425 | class Notification : public Entity |
445 | 426 | { |
446 | 427 | public: |
451 | 432 | Json to_json() const override; |
452 | 433 | void parse_json(const Json& json) override; |
453 | 434 | |
454 | std::string method() const | |
435 | const std::string& method() const | |
455 | 436 | { |
456 | 437 | return method_; |
457 | 438 | } |
458 | 439 | |
459 | Parameter params() const | |
440 | const Parameter& params() const | |
460 | 441 | { |
461 | 442 | return params_; |
462 | 443 | } |
467 | 448 | }; |
468 | 449 | |
469 | 450 | |
470 | ||
471 | 451 | typedef std::function<void(const Parameter& params)> notification_callback; |
472 | 452 | typedef std::function<jsonrpcpp::response_ptr(const Id& id, const Parameter& params)> request_callback; |
473 | ||
474 | 453 | |
475 | 454 | class Parser |
476 | 455 | { |
501 | 480 | }; |
502 | 481 | |
503 | 482 | |
504 | ||
505 | 483 | class Batch : public Entity |
506 | 484 | { |
507 | 485 | public: |
532 | 510 | { |
533 | 511 | } |
534 | 512 | |
535 | ||
536 | inline bool Entity::is_exception() | |
513 | inline bool Entity::is_exception() const | |
537 | 514 | { |
538 | 515 | return (entity == entity_t::exception); |
539 | 516 | } |
540 | 517 | |
541 | ||
542 | inline bool Entity::is_id() | |
518 | inline bool Entity::is_id() const | |
543 | 519 | { |
544 | 520 | return (entity == entity_t::id); |
545 | 521 | } |
546 | 522 | |
547 | ||
548 | inline bool Entity::is_error() | |
523 | inline bool Entity::is_error() const | |
549 | 524 | { |
550 | 525 | return (entity == entity_t::error); |
551 | 526 | } |
552 | 527 | |
553 | ||
554 | inline bool Entity::is_response() | |
528 | inline bool Entity::is_response() const | |
555 | 529 | { |
556 | 530 | return (entity == entity_t::response); |
557 | 531 | } |
558 | 532 | |
559 | ||
560 | inline bool Entity::is_request() | |
533 | inline bool Entity::is_request() const | |
561 | 534 | { |
562 | 535 | return (entity == entity_t::request); |
563 | 536 | } |
564 | 537 | |
565 | ||
566 | inline bool Entity::is_notification() | |
538 | inline bool Entity::is_notification() const | |
567 | 539 | { |
568 | 540 | return (entity == entity_t::notification); |
569 | 541 | } |
570 | 542 | |
571 | ||
572 | inline bool Entity::is_batch() | |
543 | inline bool Entity::is_batch() const | |
573 | 544 | { |
574 | 545 | return (entity == entity_t::batch); |
575 | 546 | } |
576 | ||
577 | 547 | |
578 | 548 | inline void Entity::parse(const char* json_str) |
579 | 549 | { |
599 | 569 | } |
600 | 570 | } |
601 | 571 | |
602 | ||
603 | 572 | inline void Entity::parse(const std::string& json_str) |
604 | 573 | { |
605 | 574 | parse(json_str.c_str()); |
606 | 575 | } |
607 | ||
608 | 576 | |
609 | 577 | inline std::string Entity::type_str() const |
610 | 578 | { |
632 | 600 | } |
633 | 601 | |
634 | 602 | |
635 | ||
636 | 603 | /////////////////////////// NullableEntity implementation ///////////////////// |
637 | 604 | |
638 | 605 | inline NullableEntity::NullableEntity(entity_t type) : Entity(type), isNull(false) |
639 | 606 | { |
640 | 607 | } |
641 | 608 | |
642 | ||
643 | 609 | inline NullableEntity::NullableEntity(entity_t type, std::nullptr_t) : Entity(type), isNull(true) |
644 | 610 | { |
645 | 611 | } |
646 | 612 | |
647 | 613 | |
648 | ||
649 | 614 | /////////////////////////// Id implementation ///////////////////////////////// |
650 | 615 | |
651 | 616 | inline Id::Id() : Entity(entity_t::id), type_(value_t::null), int_id_(0), string_id_("") |
652 | 617 | { |
653 | 618 | } |
654 | 619 | |
655 | ||
656 | 620 | inline Id::Id(int id) : Entity(entity_t::id), type_(value_t::integer), int_id_(id), string_id_("") |
657 | 621 | { |
658 | 622 | } |
659 | 623 | |
660 | ||
661 | 624 | inline Id::Id(const char* id) : Entity(entity_t::id), type_(value_t::string), int_id_(0), string_id_(id) |
662 | 625 | { |
663 | 626 | } |
664 | 627 | |
665 | ||
666 | 628 | inline Id::Id(const std::string& id) : Id(id.c_str()) |
667 | 629 | { |
668 | 630 | } |
669 | 631 | |
670 | ||
671 | 632 | inline Id::Id(const Json& json_id) : Entity(entity_t::id), type_(value_t::null) |
672 | 633 | { |
673 | 634 | Id::parse_json(json_id); |
674 | 635 | } |
675 | ||
676 | 636 | |
677 | 637 | inline void Id::parse_json(const Json& json) |
678 | 638 | { |
693 | 653 | else |
694 | 654 | throw std::invalid_argument("id must be integer, string or null"); |
695 | 655 | } |
696 | ||
697 | 656 | |
698 | 657 | inline Json Id::to_json() const |
699 | 658 | { |
708 | 667 | } |
709 | 668 | |
710 | 669 | |
711 | ||
712 | 670 | //////////////////////// Error implementation ///////////////////////////////// |
713 | 671 | |
714 | 672 | inline Parameter::Parameter(std::nullptr_t) : NullableEntity(entity_t::id, nullptr), type(value_t::null) |
715 | 673 | { |
716 | 674 | } |
717 | ||
718 | 675 | |
719 | 676 | inline Parameter::Parameter(const Json& json) : NullableEntity(entity_t::id), type(value_t::null) |
720 | 677 | { |
721 | 678 | if (json != nullptr) |
722 | 679 | Parameter::parse_json(json); |
723 | 680 | } |
724 | ||
725 | 681 | |
726 | 682 | inline Parameter::Parameter(const std::string& key1, const Json& value1, const std::string& key2, const Json& value2, const std::string& key3, |
727 | 683 | const Json& value3, const std::string& key4, const Json& value4) |
736 | 692 | param_map[key4] = value4; |
737 | 693 | } |
738 | 694 | |
739 | ||
740 | 695 | inline void Parameter::parse_json(const Json& json) |
741 | 696 | { |
742 | 697 | if (json.is_array()) |
752 | 707 | type = value_t::map; |
753 | 708 | } |
754 | 709 | } |
755 | ||
756 | 710 | |
757 | 711 | inline Json Parameter::to_json() const |
758 | 712 | { |
764 | 718 | return nullptr; |
765 | 719 | } |
766 | 720 | |
767 | ||
768 | 721 | inline bool Parameter::is_array() const |
769 | 722 | { |
770 | 723 | return type == value_t::array; |
771 | 724 | } |
772 | 725 | |
773 | ||
774 | 726 | inline bool Parameter::is_map() const |
775 | 727 | { |
776 | 728 | return type == value_t::map; |
777 | 729 | } |
778 | 730 | |
779 | ||
780 | 731 | inline bool Parameter::is_null() const |
781 | 732 | { |
782 | 733 | return isNull; |
783 | 734 | } |
784 | ||
785 | 735 | |
786 | 736 | inline bool Parameter::has(const std::string& key) const |
787 | 737 | { |
790 | 740 | return (param_map.find(key) != param_map.end()); |
791 | 741 | } |
792 | 742 | |
793 | ||
794 | 743 | inline Json Parameter::get(const std::string& key) const |
795 | 744 | { |
796 | 745 | return param_map.at(key); |
797 | 746 | } |
798 | ||
799 | 747 | |
800 | 748 | inline bool Parameter::has(size_t idx) const |
801 | 749 | { |
804 | 752 | return (param_array.size() > idx); |
805 | 753 | } |
806 | 754 | |
807 | ||
808 | 755 | inline Json Parameter::get(size_t idx) const |
809 | 756 | { |
810 | 757 | return param_array.at(idx); |
811 | 758 | } |
812 | ||
813 | 759 | |
814 | 760 | |
815 | 761 | //////////////////////// Error implementation ///////////////////////////////// |
820 | 766 | Error::parse_json(json); |
821 | 767 | } |
822 | 768 | |
823 | ||
824 | 769 | inline Error::Error(std::nullptr_t) : NullableEntity(entity_t::error, nullptr), code_(0), message_(""), data_(nullptr) |
825 | 770 | { |
826 | 771 | } |
827 | 772 | |
828 | ||
829 | 773 | inline Error::Error(const std::string& message, int code, const Json& data) : NullableEntity(entity_t::error), code_(code), message_(message), data_(data) |
830 | 774 | { |
831 | 775 | } |
832 | ||
833 | 776 | |
834 | 777 | inline void Error::parse_json(const Json& json) |
835 | 778 | { |
856 | 799 | } |
857 | 800 | } |
858 | 801 | |
859 | ||
860 | 802 | inline Json Error::to_json() const |
861 | 803 | { |
862 | 804 | Json j = { |
869 | 811 | } |
870 | 812 | |
871 | 813 | |
872 | ||
873 | 814 | ////////////////////// Request implementation ///////////////////////////////// |
874 | 815 | |
875 | 816 | inline Request::Request(const Json& json) : Entity(entity_t::request), method_(""), id_() |
878 | 819 | Request::parse_json(json); |
879 | 820 | } |
880 | 821 | |
881 | ||
882 | 822 | inline Request::Request(const Id& id, const std::string& method, const Parameter& params) : Entity(entity_t::request), method_(method), params_(params), id_(id) |
883 | 823 | { |
884 | 824 | } |
885 | ||
886 | 825 | |
887 | 826 | inline void Request::parse_json(const Json& json) |
888 | 827 | { |
929 | 868 | } |
930 | 869 | } |
931 | 870 | |
932 | ||
933 | 871 | inline Json Request::to_json() const |
934 | 872 | { |
935 | 873 | Json json = {{"jsonrpc", "2.0"}, {"method", method_}, {"id", id_.to_json()}}; |
941 | 879 | } |
942 | 880 | |
943 | 881 | |
944 | ||
945 | 882 | inline RpcException::RpcException(const char* text) : m_(text) |
946 | 883 | { |
947 | 884 | } |
956 | 893 | } |
957 | 894 | |
958 | 895 | |
959 | ||
960 | 896 | inline RpcEntityException::RpcEntityException(const Error& error) : RpcException(error.message()), Entity(entity_t::exception), error_(error) |
961 | 897 | { |
962 | 898 | } |
966 | 902 | } |
967 | 903 | |
968 | 904 | |
969 | ||
970 | 905 | inline ParseErrorException::ParseErrorException(const Error& error) : RpcEntityException(error) |
971 | 906 | { |
972 | 907 | } |
983 | 918 | } |
984 | 919 | |
985 | 920 | |
986 | ||
987 | 921 | inline RequestException::RequestException(const Error& error, const Id& requestId) : RpcEntityException(error), id_(requestId) |
988 | 922 | { |
989 | 923 | } |
994 | 928 | |
995 | 929 | return response; |
996 | 930 | } |
997 | ||
998 | 931 | |
999 | 932 | |
1000 | 933 | inline InvalidRequestException::InvalidRequestException(const Id& requestId) : RequestException(Error("Invalid request", -32600), requestId) |
1015 | 948 | } |
1016 | 949 | |
1017 | 950 | |
1018 | ||
1019 | 951 | inline MethodNotFoundException::MethodNotFoundException(const Id& requestId) : RequestException(Error("Method not found", -32601), requestId) |
1020 | 952 | { |
1021 | 953 | } |
1034 | 966 | } |
1035 | 967 | |
1036 | 968 | |
1037 | ||
1038 | 969 | inline InvalidParamsException::InvalidParamsException(const Id& requestId) : RequestException(Error("Invalid params", -32602), requestId) |
1039 | 970 | { |
1040 | 971 | } |
1053 | 984 | } |
1054 | 985 | |
1055 | 986 | |
1056 | ||
1057 | 987 | inline InternalErrorException::InternalErrorException(const Id& requestId) : RequestException(Error("Internal error", -32603), requestId) |
1058 | 988 | { |
1059 | 989 | } |
1072 | 1002 | } |
1073 | 1003 | |
1074 | 1004 | |
1075 | ||
1076 | 1005 | ///////////////////// Response implementation ///////////////////////////////// |
1077 | 1006 | |
1078 | 1007 | inline Response::Response(const Json& json) : Entity(entity_t::response) |
1081 | 1010 | Response::parse_json(json); |
1082 | 1011 | } |
1083 | 1012 | |
1084 | ||
1085 | 1013 | inline Response::Response(const Id& id, const Json& result) : Entity(entity_t::response), id_(id), result_(result), error_(nullptr) |
1086 | 1014 | { |
1087 | 1015 | } |
1088 | 1016 | |
1089 | ||
1090 | 1017 | inline Response::Response(const Id& id, const Error& error) : Entity(entity_t::response), id_(id), result_(), error_(error) |
1091 | 1018 | { |
1092 | 1019 | } |
1093 | 1020 | |
1094 | ||
1095 | 1021 | inline Response::Response(const Request& request, const Json& result) : Response(request.id(), result) |
1096 | 1022 | { |
1097 | 1023 | } |
1098 | 1024 | |
1099 | ||
1100 | 1025 | inline Response::Response(const Request& request, const Error& error) : Response(request.id(), error) |
1101 | 1026 | { |
1102 | 1027 | } |
1103 | 1028 | |
1104 | ||
1105 | 1029 | inline Response::Response(const RequestException& exception) : Response(exception.id(), exception.error()) |
1106 | 1030 | { |
1107 | 1031 | } |
1108 | ||
1109 | 1032 | |
1110 | 1033 | inline void Response::parse_json(const Json& json) |
1111 | 1034 | { |
1124 | 1047 | if (json.count("result") != 0u) |
1125 | 1048 | result_ = json["result"]; |
1126 | 1049 | else if (json.count("error") != 0u) |
1127 | error_.parse_json(json["error"]); | |
1050 | error_ = json["error"]; | |
1128 | 1051 | else |
1129 | 1052 | throw RpcException("response must contain result or error"); |
1130 | 1053 | } |
1137 | 1060 | throw RpcException(e.what()); |
1138 | 1061 | } |
1139 | 1062 | } |
1140 | ||
1141 | 1063 | |
1142 | 1064 | inline Json Response::to_json() const |
1143 | 1065 | { |
1154 | 1076 | } |
1155 | 1077 | |
1156 | 1078 | |
1157 | ||
1158 | 1079 | ///////////////// Notification implementation ///////////////////////////////// |
1159 | 1080 | |
1160 | 1081 | inline Notification::Notification(const Json& json) : Entity(entity_t::notification) |
1163 | 1084 | Notification::parse_json(json); |
1164 | 1085 | } |
1165 | 1086 | |
1166 | ||
1167 | 1087 | inline Notification::Notification(const char* method, const Parameter& params) : Entity(entity_t::notification), method_(method), params_(params) |
1168 | 1088 | { |
1169 | 1089 | } |
1170 | 1090 | |
1171 | ||
1172 | 1091 | inline Notification::Notification(const std::string& method, const Parameter& params) : Notification(method.c_str(), params) |
1173 | 1092 | { |
1174 | 1093 | } |
1175 | ||
1176 | 1094 | |
1177 | 1095 | inline void Notification::parse_json(const Json& json) |
1178 | 1096 | { |
1207 | 1125 | } |
1208 | 1126 | } |
1209 | 1127 | |
1210 | ||
1211 | 1128 | inline Json Notification::to_json() const |
1212 | 1129 | { |
1213 | 1130 | Json json = { |
1221 | 1138 | } |
1222 | 1139 | |
1223 | 1140 | |
1224 | ||
1225 | 1141 | //////////////////////// Batch implementation ///////////////////////////////// |
1226 | 1142 | |
1227 | 1143 | inline Batch::Batch(const Json& json) : Entity(entity_t::batch) |
1229 | 1145 | if (json != nullptr) |
1230 | 1146 | Batch::parse_json(json); |
1231 | 1147 | } |
1232 | ||
1233 | 1148 | |
1234 | 1149 | inline void Batch::parse_json(const Json& json) |
1235 | 1150 | { |
1259 | 1174 | throw InvalidRequestException(); |
1260 | 1175 | } |
1261 | 1176 | |
1262 | ||
1263 | 1177 | inline Json Batch::to_json() const |
1264 | 1178 | { |
1265 | 1179 | Json result; |
1269 | 1183 | } |
1270 | 1184 | |
1271 | 1185 | |
1272 | /*void Batch::add(const entity_ptr entity) | |
1273 | { | |
1274 | entities.push_back(entity); | |
1275 | } | |
1276 | */ | |
1277 | ||
1278 | ||
1279 | ||
1280 | 1186 | //////////////////////// Parser implementation //////////////////////////////// |
1281 | 1187 | |
1282 | 1188 | inline void Parser::register_notification_callback(const std::string& notification, notification_callback callback) |
1285 | 1191 | notification_callbacks_[notification] = callback; |
1286 | 1192 | } |
1287 | 1193 | |
1288 | ||
1289 | 1194 | inline void Parser::register_request_callback(const std::string& request, request_callback callback) |
1290 | 1195 | { |
1291 | 1196 | if (callback) |
1292 | 1197 | request_callbacks_[request] = callback; |
1293 | 1198 | } |
1294 | ||
1295 | 1199 | |
1296 | 1200 | inline entity_ptr Parser::parse(const std::string& json_str) |
1297 | 1201 | { |
1324 | 1228 | return entity; |
1325 | 1229 | } |
1326 | 1230 | |
1327 | ||
1328 | 1231 | inline entity_ptr Parser::parse_json(const Json& json) |
1329 | 1232 | { |
1330 | 1233 | return do_parse_json(json); |
1331 | 1234 | } |
1332 | 1235 | |
1333 | ||
1334 | 1236 | inline entity_ptr Parser::do_parse(const std::string& json_str) |
1335 | 1237 | { |
1336 | 1238 | try |
1348 | 1250 | |
1349 | 1251 | return nullptr; |
1350 | 1252 | } |
1351 | ||
1352 | 1253 | |
1353 | 1254 | inline entity_ptr Parser::do_parse_json(const Json& json) |
1354 | 1255 | { |
1375 | 1276 | return nullptr; |
1376 | 1277 | } |
1377 | 1278 | |
1378 | ||
1379 | 1279 | inline bool Parser::is_request(const std::string& json_str) |
1380 | 1280 | { |
1381 | 1281 | try |
1388 | 1288 | } |
1389 | 1289 | } |
1390 | 1290 | |
1391 | ||
1392 | 1291 | inline bool Parser::is_request(const Json& json) |
1393 | 1292 | { |
1394 | 1293 | return ((json.count("method") != 0u) && (json.count("id") != 0u)); |
1395 | 1294 | } |
1396 | 1295 | |
1397 | ||
1398 | 1296 | inline bool Parser::is_notification(const std::string& json_str) |
1399 | 1297 | { |
1400 | 1298 | try |
1407 | 1305 | } |
1408 | 1306 | } |
1409 | 1307 | |
1410 | ||
1411 | 1308 | inline bool Parser::is_notification(const Json& json) |
1412 | 1309 | { |
1413 | 1310 | return ((json.count("method") != 0u) && (json.count("id") == 0)); |
1414 | 1311 | } |
1415 | 1312 | |
1416 | ||
1417 | 1313 | inline bool Parser::is_response(const std::string& json_str) |
1418 | 1314 | { |
1419 | 1315 | try |
1426 | 1322 | } |
1427 | 1323 | } |
1428 | 1324 | |
1429 | ||
1430 | 1325 | inline bool Parser::is_response(const Json& json) |
1431 | 1326 | { |
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 | } | |
1435 | 1329 | |
1436 | 1330 | inline bool Parser::is_batch(const std::string& json_str) |
1437 | 1331 | { |
1445 | 1339 | } |
1446 | 1340 | } |
1447 | 1341 | |
1448 | ||
1449 | 1342 | inline bool Parser::is_batch(const Json& json) |
1450 | 1343 | { |
1451 | 1344 | return (json.is_array()); |
1452 | 1345 | } |
1453 | 1346 | |
1454 | ||
1455 | 1347 | } // namespace jsonrpcpp |
1456 | 1348 | |
1457 | ||
1458 | ||
1459 | 1349 | #endif |
26 | 26 | static AvahiSimplePoll* simple_poll; |
27 | 27 | static char* name; |
28 | 28 | |
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) | |
30 | 30 | { |
31 | 31 | group = nullptr; |
32 | 32 | simple_poll = nullptr; |
55 | 55 | LOG(ERROR) << "Failed to create client: " << avahi_strerror(error) << "\n"; |
56 | 56 | } |
57 | 57 | |
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 | }); | |
67 | 69 | } |
68 | 70 | |
69 | 71 | |
70 | 72 | PublishAvahi::~PublishAvahi() |
71 | 73 | { |
72 | active_ = false; | |
73 | pollThread_.join(); | |
74 | timer_.cancel(); | |
74 | 75 | |
75 | 76 | if (client_) |
76 | 77 | avahi_client_free(client_); |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
29 | 29 | #include <avahi-common/simple-watch.h> |
30 | 30 | #include <avahi-common/timeval.h> |
31 | 31 | #include <string> |
32 | #include <thread> | |
33 | 32 | #include <vector> |
34 | 33 | |
35 | 34 | class PublishAvahi; |
39 | 38 | class PublishAvahi : public PublishmDNS |
40 | 39 | { |
41 | 40 | public: |
42 | PublishAvahi(const std::string& serviceName); | |
41 | PublishAvahi(const std::string& serviceName, boost::asio::io_context& ioc); | |
43 | 42 | ~PublishAvahi() override; |
44 | 43 | void publish(const std::vector<mDNSService>& services) override; |
45 | 44 | |
47 | 46 | static void entry_group_callback(AvahiEntryGroup* g, AvahiEntryGroupState state, AVAHI_GCC_UNUSED void* userdata); |
48 | 47 | static void client_callback(AvahiClient* c, AvahiClientState state, AVAHI_GCC_UNUSED void* userdata); |
49 | 48 | void create_services(AvahiClient* c); |
50 | void worker(); | |
49 | void poll(); | |
51 | 50 | AvahiClient* client_; |
52 | std::thread pollThread_; | |
53 | std::atomic<bool> active_; | |
54 | 51 | std::vector<mDNSService> services_; |
52 | boost::asio::steady_timer timer_; | |
55 | 53 | }; |
56 | 54 | |
57 | 55 |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
27 | 27 | } Opaque16; |
28 | 28 | |
29 | 29 | |
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) | |
31 | 31 | { |
32 | 32 | /// dns-sd -R Snapcast _snapcast._tcp local 1704 |
33 | 33 | /// dns-sd -R Snapcast _snapcast-jsonrpc._tcp local 1705 |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
29 | 29 | class PublishBonjour : public PublishmDNS |
30 | 30 | { |
31 | 31 | public: |
32 | PublishBonjour(const std::string& serviceName); | |
32 | PublishBonjour(const std::string& serviceName, boost::asio::io_context& ioc); | |
33 | 33 | virtual ~PublishBonjour(); |
34 | 34 | virtual void publish(const std::vector<mDNSService>& services); |
35 | 35 |
0 | 0 | #ifndef PUBLISH_MDNS_H |
1 | 1 | #define PUBLISH_MDNS_H |
2 | 2 | |
3 | #include <boost/asio.hpp> | |
3 | 4 | #include <string> |
4 | 5 | #include <vector> |
5 | 6 | |
18 | 19 | class PublishmDNS |
19 | 20 | { |
20 | 21 | public: |
21 | PublishmDNS(const std::string& serviceName) : serviceName_(serviceName) | |
22 | PublishmDNS(const std::string& serviceName, boost::asio::io_context& ioc) : serviceName_(serviceName), ioc_(ioc) | |
22 | 23 | { |
23 | 24 | } |
24 | 25 | |
28 | 29 | |
29 | 30 | protected: |
30 | 31 | std::string serviceName_; |
32 | boost::asio::io_context& ioc_; | |
31 | 33 | }; |
32 | 34 | |
33 | 35 | #if defined(HAS_AVAHI) |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
45 | 45 | std::string codec{"flac"}; |
46 | 46 | int32_t bufferMs{1000}; |
47 | 47 | std::string sampleFormat{"48000:16:2"}; |
48 | size_t streamReadMs{20}; | |
48 | size_t streamChunkMs{20}; | |
49 | 49 | bool sendAudioToMutedClients{false}; |
50 | 50 | std::vector<std::string> bind_to_address{{"0.0.0.0"}}; |
51 | 51 | }; |
0 | .TH SNAPSERVER 1 "October 2019" | |
0 | .TH SNAPSERVER 1 "January 2020" | |
1 | 1 | .SH NAME |
2 | 2 | snapserver - Snapcast server |
3 | 3 | .SH SYNOPSIS |
43 | 43 | \fI~/.config/snapcast/server.json\fR or (if $HOME is not set) \fI/var/lib/snapcast/server.json\fR |
44 | 44 | persistent server data file |
45 | 45 | .SH "COPYRIGHT" |
46 | Copyright (C) 2014-2019 Johannes Pohl (snapcast@badaix.de). | |
46 | Copyright (C) 2014-2020 Johannes Pohl (snapcast@badaix.de). | |
47 | 47 | License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>. |
48 | 48 | This is free software: you are free to change and redistribute it. |
49 | 49 | There is NO WARRANTY, to the extent permitted by law. |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
24 | 24 | #include "common/daemon.hpp" |
25 | 25 | #endif |
26 | 26 | #include "common/sample_format.hpp" |
27 | #include "common/signal_handler.hpp" | |
28 | 27 | #include "common/snap_exception.hpp" |
29 | 28 | #include "common/time_defs.hpp" |
30 | 29 | #include "common/utils/string_utils.hpp" |
36 | 35 | #include "publishZeroConf/publish_mdns.hpp" |
37 | 36 | #endif |
38 | 37 | #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 | ||
44 | 40 | |
45 | 41 | using namespace std; |
46 | 42 | using namespace popl; |
82 | 78 | auto streamValue = conf.add<Value<string>>( |
83 | 79 | "s", "stream.stream", "URI of the PCM input stream.\nFormat: TYPE://host/path?name=NAME\n[&codec=CODEC]\n[&sampleformat=SAMPLEFORMAT]", pcmStream, |
84 | 80 | &pcmStream); |
81 | int num_threads = -1; | |
82 | conf.add<Value<int>>("", "server.threads", "number of server threads", num_threads, &num_threads); | |
85 | 83 | |
86 | 84 | 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", | |
88 | 86 | 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); | |
90 | 91 | conf.add<Value<int>>("b", "stream.buffer", "Buffer [ms]", settings.stream.bufferMs, &settings.stream.bufferMs); |
91 | 92 | conf.add<Value<bool>>("", "stream.send_to_muted", "Send audio to muted clients", settings.stream.sendAudioToMutedClients, |
92 | 93 | &settings.stream.sendAudioToMutedClients); |
141 | 142 | if (versionSwitch->is_set()) |
142 | 143 | { |
143 | 144 | 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" | |
145 | 146 | << "License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.\n" |
146 | 147 | << "This is free software: you are free to change and redistribute it.\n" |
147 | 148 | << "There is NO WARRANTY, to the extent permitted by law.\n\n" |
164 | 165 | |
165 | 166 | if (settings.stream.codec.find(":?") != string::npos) |
166 | 167 | { |
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)); | |
169 | 170 | if (encoder) |
170 | 171 | { |
171 | 172 | cout << "Options for codec \"" << encoder->name() << "\":\n" |
185 | 186 | } |
186 | 187 | else |
187 | 188 | { |
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"; | |
190 | 194 | |
191 | 195 | if (!streamValue->is_set()) |
192 | 196 | settings.stream.pcmStreams.push_back(streamValue->value()); |
196 | 200 | LOG(INFO) << "Adding stream: " << streamValue->value(n) << "\n"; |
197 | 201 | settings.stream.pcmStreams.push_back(streamValue->value(n)); |
198 | 202 | } |
199 | ||
200 | signal(SIGHUP, signal_handler); | |
201 | signal(SIGTERM, signal_handler); | |
202 | signal(SIGINT, signal_handler); | |
203 | 203 | |
204 | 204 | #ifdef HAS_DAEMON |
205 | 205 | std::unique_ptr<Daemon> daemon; |
236 | 236 | Config::instance().init(); |
237 | 237 | #endif |
238 | 238 | |
239 | ||
239 | boost::asio::io_context io_context; | |
240 | 240 | #if defined(HAS_AVAHI) || defined(HAS_BONJOUR) |
241 | PublishZeroConf publishZeroConfg("Snapcast"); | |
241 | auto publishZeroConfg = std::make_unique<PublishZeroConf>("Snapcast", io_context); | |
242 | 242 | vector<mDNSService> dns_services; |
243 | 243 | dns_services.emplace_back("_snapcast._tcp", settings.stream.port); |
244 | 244 | dns_services.emplace_back("_snapcast-stream._tcp", settings.stream.port); |
251 | 251 | { |
252 | 252 | dns_services.emplace_back("_snapcast-http._tcp", settings.http.port); |
253 | 253 | } |
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 | } | |
256 | 261 | |
257 | 262 | if (settings.stream.bufferMs < 400) |
258 | 263 | { |
260 | 265 | settings.stream.bufferMs = 400; |
261 | 266 | } |
262 | 267 | |
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); | |
265 | 269 | streamServer->start(); |
266 | 270 | |
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(); | |
275 | 293 | |
276 | 294 | LOG(INFO) << "Stopping streamServer" << endl; |
277 | 295 | streamServer->stop(); |
282 | 300 | SLOG(ERROR) << "Exception: " << e.what() << std::endl; |
283 | 301 | exitcode = EXIT_FAILURE; |
284 | 302 | } |
285 | ||
303 | Config::instance().save(); | |
286 | 304 | SLOG(NOTICE) << "daemon terminated." << endl; |
287 | 305 | exit(exitcode); |
288 | 306 | } |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
17 | 17 | |
18 | 18 | #include "stream_server.hpp" |
19 | 19 | #include "common/aixlog.hpp" |
20 | #include "config.h" | |
20 | #include "config.hpp" | |
21 | 21 | #include "message/hello.hpp" |
22 | 22 | #include "message/stream_tags.hpp" |
23 | 23 | #include "message/time.hpp" |
24 | 24 | #include <iostream> |
25 | 25 | |
26 | 26 | using namespace std; |
27 | using namespace streamreader; | |
27 | 28 | |
28 | 29 | using json = nlohmann::json; |
29 | 30 | |
30 | 31 | |
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) | |
32 | 33 | { |
33 | 34 | } |
34 | 35 | |
36 | 37 | StreamServer::~StreamServer() = default; |
37 | 38 | |
38 | 39 | |
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 | ||
39 | 52 | void StreamServer::onMetaChanged(const PcmStream* pcmStream) |
40 | 53 | { |
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 | |
43 | 57 | |
44 | 58 | // Send meta to all connected clients |
45 | 59 | 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_); | |
48 | 63 | for (auto s : sessions_) |
49 | 64 | { |
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 | } | |
52 | 70 | } |
53 | 71 | |
54 | 72 | LOG(INFO) << "onMetaChanged (" << pcmStream->getName() << ")\n"; |
55 | 73 | json notification = jsonrpcpp::Notification("Stream.OnMetadata", jsonrpcpp::Parameter("id", pcmStream->getId(), "meta", meta->msg)).to_json(); |
56 | 74 | controlServer_->send(notification.dump(), nullptr); |
57 | ////cout << "Notification: " << notification.dump() << "\n"; | |
75 | // cout << "Notification: " << notification.dump() << "\n"; | |
58 | 76 | } |
59 | 77 | |
60 | 78 | void StreamServer::onStateChanged(const PcmStream* pcmStream, const ReaderState& state) |
61 | 79 | { |
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"; | |
66 | 84 | // LOG(INFO) << pcmStream->toJson().dump(4); |
67 | 85 | json notification = jsonrpcpp::Notification("Stream.OnUpdate", jsonrpcpp::Parameter("id", pcmStream->getId(), "stream", pcmStream->toJson())).to_json(); |
68 | 86 | controlServer_->send(notification.dump(), nullptr); |
69 | ////cout << "Notification: " << notification.dump() << "\n"; | |
87 | // cout << "Notification: " << notification.dump() << "\n"; | |
70 | 88 | } |
71 | 89 | |
72 | 90 | |
74 | 92 | { |
75 | 93 | // LOG(INFO) << "onChunkRead (" << pcmStream->getName() << "): " << duration << "ms\n"; |
76 | 94 | 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) | |
81 | 112 | { |
82 | 113 | if (!settings_.stream.sendAudioToMutedClients) |
83 | 114 | { |
84 | GroupPtr group = Config::instance().getGroupFromClient(s->clientId); | |
115 | GroupPtr group = Config::instance().getGroupFromClient(session->clientId); | |
85 | 116 | if (group) |
86 | 117 | { |
87 | 118 | if (group->muted) |
119 | { | |
88 | 120 | 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); | |
100 | 136 | } |
101 | 137 | } |
102 | 138 | |
117 | 153 | |
118 | 154 | LOG(INFO) << "onDisconnect: " << session->clientId << "\n"; |
119 | 155 | 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()); | |
126 | 162 | LOG(DEBUG) << "sessions: " << sessions_.size() << "\n"; |
127 | 163 | |
128 | 164 | // notify controllers if not yet done |
135 | 171 | Config::instance().save(); |
136 | 172 | if (controlServer_ != nullptr) |
137 | 173 | { |
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 | |
141 | 177 | if (getStreamSession(clientInfo->id) == nullptr) |
142 | 178 | { |
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 | |
147 | 183 | json notification = |
148 | 184 | jsonrpcpp::Notification("Client.OnDisconnect", jsonrpcpp::Parameter("id", clientInfo->id, "client", clientInfo->toJson())).to_json(); |
149 | 185 | controlServer_->send(notification.dump()); |
150 | ////cout << "Notification: " << notification.dump() << "\n"; | |
151 | } | |
152 | } | |
186 | // cout << "Notification: " << notification.dump() << "\n"; | |
187 | } | |
188 | } | |
189 | cleanup(); | |
153 | 190 | } |
154 | 191 | |
155 | 192 | |
157 | 194 | { |
158 | 195 | try |
159 | 196 | { |
160 | ////LOG(INFO) << "StreamServer::ProcessRequest method: " << request->method << ", " << "id: " << request->id() << "\n"; | |
197 | // LOG(INFO) << "StreamServer::ProcessRequest method: " << request->method << ", " << "id: " << request->id() << "\n"; | |
161 | 198 | Json result; |
162 | 199 | |
163 | 200 | if (request->method().find("Client.") == 0) |
168 | 205 | |
169 | 206 | if (request->method() == "Client.GetStatus") |
170 | 207 | { |
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 | |
176 | 212 | result["client"] = clientInfo->toJson(); |
177 | 213 | } |
178 | 214 | else if (request->method() == "Client.SetVolume") |
179 | 215 | { |
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_); | |
183 | 223 | clientInfo->config.volume.fromJson(request->params().get("volume")); |
184 | 224 | result["volume"] = clientInfo->config.volume.toJson(); |
185 | 225 | notification.reset(new jsonrpcpp::Notification("Client.OnVolumeChanged", |
187 | 227 | } |
188 | 228 | else if (request->method() == "Client.SetLatency") |
189 | 229 | { |
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 | |
193 | 235 | int latency = request->params().get("latency"); |
194 | 236 | if (latency < -10000) |
195 | 237 | latency = -10000; |
202 | 244 | } |
203 | 245 | else if (request->method() == "Client.SetName") |
204 | 246 | { |
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 | |
208 | 252 | clientInfo->config.name = request->params().get<std::string>("name"); |
209 | 253 | result["name"] = clientInfo->config.name; |
210 | 254 | notification.reset( |
238 | 282 | |
239 | 283 | if (request->method() == "Group.GetStatus") |
240 | 284 | { |
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 | |
249 | 289 | result["group"] = group->toJson(); |
250 | 290 | } |
251 | 291 | else if (request->method() == "Group.SetName") |
252 | 292 | { |
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 | |
256 | 298 | group->name = request->params().get<std::string>("name"); |
257 | 299 | result["name"] = group->name; |
258 | 300 | notification.reset(new jsonrpcpp::Notification("Group.OnNameChanged", jsonrpcpp::Parameter("id", group->id, "name", group->name))); |
259 | 301 | } |
260 | 302 | else if (request->method() == "Group.SetMute") |
261 | 303 | { |
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 | |
265 | 309 | bool muted = request->params().get<bool>("mute"); |
266 | 310 | group->muted = muted; |
267 | 311 | |
286 | 330 | } |
287 | 331 | else if (request->method() == "Group.SetStream") |
288 | 332 | { |
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 | |
294 | 338 | string streamId = request->params().get<std::string>("stream_id"); |
295 | 339 | PcmStreamPtr stream = streamManager_->getStream(streamId); |
296 | 340 | if (stream == nullptr) |
298 | 342 | |
299 | 343 | group->streamId = streamId; |
300 | 344 | |
301 | /// Update clients | |
345 | // Update clients | |
302 | 346 | for (auto client : group->clients) |
303 | 347 | { |
304 | 348 | session_ptr session = getStreamSession(client->id); |
310 | 354 | } |
311 | 355 | } |
312 | 356 | |
313 | /// Notify others | |
357 | // Notify others | |
314 | 358 | result["stream_id"] = group->streamId; |
315 | 359 | notification.reset(new jsonrpcpp::Notification("Group.OnStreamChanged", jsonrpcpp::Parameter("id", group->id, "stream_id", group->streamId))); |
316 | 360 | } |
317 | 361 | else if (request->method() == "Group.SetClients") |
318 | 362 | { |
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 | |
346 | 368 | vector<string> clients = request->params().get("clients"); |
347 | /// Remove clients from group | |
369 | // Remove clients from group | |
348 | 370 | for (auto iter = group->clients.begin(); iter != group->clients.end();) |
349 | 371 | { |
350 | 372 | auto client = *iter; |
358 | 380 | newGroup->streamId = group->streamId; |
359 | 381 | } |
360 | 382 | |
361 | /// Add clients to group | |
383 | // Add clients to group | |
362 | 384 | PcmStreamPtr stream = streamManager_->getStream(group->streamId); |
363 | 385 | for (const auto& clientId : clients) |
364 | 386 | { |
377 | 399 | |
378 | 400 | group->addClient(client); |
379 | 401 | |
380 | /// assign new stream | |
402 | // assign new stream | |
381 | 403 | session_ptr session = getStreamSession(client->id); |
382 | 404 | if (session && stream && (session->pcmStream() != stream)) |
383 | 405 | { |
393 | 415 | json server = Config::instance().getServerStatus(streamManager_->toJson()); |
394 | 416 | result["server"] = server; |
395 | 417 | |
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 | |
397 | 419 | notification.reset(new jsonrpcpp::Notification("Server.OnUpdate", jsonrpcpp::Parameter("server", server))); |
398 | 420 | } |
399 | 421 | else |
403 | 425 | { |
404 | 426 | if (request->method().find("Server.GetRPCVersion") == 0) |
405 | 427 | { |
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}} | |
408 | 430 | // <major>: backwards incompatible change |
409 | 431 | result["major"] = 2; |
410 | 432 | // <minor>: feature addition to the API |
414 | 436 | } |
415 | 437 | else if (request->method() == "Server.GetStatus") |
416 | 438 | { |
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 | |
430 | 443 | result["server"] = Config::instance().getServerStatus(streamManager_->toJson()); |
431 | 444 | } |
432 | 445 | else if (request->method() == "Server.DeleteClient") |
433 | 446 | { |
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 | |
456 | 452 | ClientInfoPtr clientInfo = Config::instance().getClientInfo(request->params().get<std::string>("id")); |
457 | 453 | if (clientInfo == nullptr) |
458 | 454 | throw jsonrpcpp::InternalErrorException("Client not found", request->id()); |
494 | 490 | } |
495 | 491 | else if (request->method() == "Stream.AddStream") |
496 | 492 | { |
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 | |
501 | 498 | |
502 | 499 | LOG(INFO) << "Stream.AddStream(" << request->params().get("streamUri") << ")" |
503 | 500 | << "\n"; |
513 | 510 | } |
514 | 511 | else if (request->method() == "Stream.RemoveStream") |
515 | 512 | { |
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 | |
520 | 518 | |
521 | 519 | LOG(INFO) << "Stream.RemoveStream(" << request->params().get("id") << ")" |
522 | 520 | << "\n"; |
533 | 531 | else |
534 | 532 | throw jsonrpcpp::MethodNotFoundException(request->id()); |
535 | 533 | |
536 | Config::instance().save(); | |
537 | 534 | response.reset(new jsonrpcpp::Response(*request, result)); |
538 | 535 | } |
539 | 536 | catch (const jsonrpcpp::RequestException& e) |
551 | 548 | |
552 | 549 | std::string StreamServer::onMessageReceived(ControlSession* controlSession, const std::string& message) |
553 | 550 | { |
554 | LOG(DEBUG) << "onMessageReceived: " << message << "\n"; | |
551 | // LOG(DEBUG) << "onMessageReceived: " << message << "\n"; | |
555 | 552 | jsonrpcpp::entity_ptr entity(nullptr); |
556 | 553 | try |
557 | 554 | { |
574 | 571 | { |
575 | 572 | jsonrpcpp::request_ptr request = dynamic_pointer_cast<jsonrpcpp::Request>(entity); |
576 | 573 | ProcessRequest(request, response, notification); |
574 | Config::instance().save(); | |
577 | 575 | ////cout << "Request: " << request->to_json().dump() << "\n"; |
578 | 576 | if (notification) |
579 | 577 | { |
605 | 603 | notificationBatch.add_ptr(notification); |
606 | 604 | } |
607 | 605 | } |
606 | Config::instance().save(); | |
608 | 607 | if (!notificationBatch.entities.empty()) |
609 | 608 | controlServer_->send(notificationBatch.to_json().dump(), controlSession); |
610 | 609 | if (!responseBatch.entities.empty()) |
699 | 698 | |
700 | 699 | if (newGroup) |
701 | 700 | { |
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 | |
716 | 704 | json server = Config::instance().getServerStatus(streamManager_->toJson()); |
717 | 705 | json notification = jsonrpcpp::Notification("Server.OnUpdate", jsonrpcpp::Parameter("server", server)).to_json(); |
718 | 706 | controlServer_->send(notification.dump()); |
719 | ////cout << "Notification: " << notification.dump() << "\n"; | |
720 | 707 | } |
721 | 708 | else |
722 | 709 | { |
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 | |
727 | 713 | json notification = jsonrpcpp::Notification("Client.OnConnect", jsonrpcpp::Parameter("id", client->id, "client", client->toJson())).to_json(); |
728 | 714 | controlServer_->send(notification.dump()); |
729 | ////cout << "Notification: " << notification.dump() << "\n"; | |
715 | // cout << "Notification: " << notification.dump() << "\n"; | |
730 | 716 | } |
731 | 717 | // cout << Config::instance().getServerStatus(streamManager_->toJson()).dump(4) << "\n"; |
732 | 718 | // cout << group->toJson().dump(4) << "\n"; |
738 | 724 | session_ptr StreamServer::getStreamSession(StreamSession* streamSession) const |
739 | 725 | { |
740 | 726 | std::lock_guard<std::recursive_mutex> mlock(sessionsMutex_); |
727 | ||
741 | 728 | for (auto session : sessions_) |
742 | 729 | { |
743 | if (session.get() == streamSession) | |
744 | return session; | |
730 | if (auto s = session.lock()) | |
731 | if (s.get() == streamSession) | |
732 | return s; | |
745 | 733 | } |
746 | 734 | return nullptr; |
747 | 735 | } |
753 | 741 | std::lock_guard<std::recursive_mutex> mlock(sessionsMutex_); |
754 | 742 | for (auto session : sessions_) |
755 | 743 | { |
756 | if (session->clientId == clientId) | |
757 | return session; | |
744 | if (auto s = session.lock()) | |
745 | if (s->clientId == clientId) | |
746 | return s; | |
758 | 747 | } |
759 | 748 | return nullptr; |
760 | 749 | } |
788 | 777 | socket.set_option(tcp::no_delay(true)); |
789 | 778 | |
790 | 779 | 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)); | |
792 | 781 | |
793 | 782 | session->setBufferMs(settings_.stream.bufferMs); |
794 | 783 | session->start(); |
795 | 784 | |
796 | 785 | std::lock_guard<std::recursive_mutex> mlock(sessionsMutex_); |
797 | sessions_.insert(session); | |
786 | sessions_.emplace_back(session); | |
787 | cleanup(); | |
798 | 788 | } |
799 | 789 | catch (const std::exception& e) |
800 | 790 | { |
808 | 798 | { |
809 | 799 | try |
810 | 800 | { |
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); | |
812 | 802 | controlServer_->start(); |
813 | 803 | |
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); | |
815 | 806 | // throw SnapException("xxx"); |
816 | 807 | for (const auto& streamUri : settings_.stream.pcmStreams) |
817 | 808 | { |
827 | 818 | { |
828 | 819 | LOG(INFO) << "Creating stream acceptor for address: " << address << ", port: " << settings_.stream.port << "\n"; |
829 | 820 | 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))); | |
831 | 822 | } |
832 | 823 | catch (const boost::system::system_error& e) |
833 | 824 | { |
848 | 839 | |
849 | 840 | void StreamServer::stop() |
850 | 841 | { |
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 | ||
873 | 842 | for (auto& acceptor : acceptor_) |
874 | 843 | acceptor->cancel(); |
875 | 844 | 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 | } |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
15 | 15 | along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | ***/ |
17 | 17 | |
18 | #ifndef STREAM_SERVER_H | |
19 | #define STREAM_SERVER_H | |
18 | #ifndef STREAM_SERVER_HPP | |
19 | #define STREAM_SERVER_HPP | |
20 | 20 | |
21 | 21 | #include <boost/asio.hpp> |
22 | 22 | #include <memory> |
23 | 23 | #include <mutex> |
24 | 24 | #include <set> |
25 | 25 | #include <sstream> |
26 | #include <thread> | |
27 | 26 | #include <vector> |
28 | 27 | |
29 | 28 | #include "common/queue.h" |
37 | 36 | #include "stream_session.hpp" |
38 | 37 | #include "streamreader/stream_manager.hpp" |
39 | 38 | |
39 | using namespace streamreader; | |
40 | 40 | |
41 | 41 | using boost::asio::ip::tcp; |
42 | 42 | using acceptor_ptr = std::unique_ptr<tcp::acceptor>; |
43 | using socket_ptr = std::shared_ptr<tcp::socket>; | |
44 | 43 | using session_ptr = std::shared_ptr<StreamSession>; |
45 | 44 | |
46 | 45 | |
51 | 50 | * Receives (via the MessageReceiver interface) and answers messages from the clients |
52 | 51 | * Forwards PCM data to the clients |
53 | 52 | */ |
54 | class StreamServer : public MessageReceiver, ControlMessageReceiver, PcmListener | |
53 | class StreamServer : public MessageReceiver, public ControlMessageReceiver, public PcmListener | |
55 | 54 | { |
56 | 55 | public: |
57 | StreamServer(boost::asio::io_context* io_context, const ServerSettings& serverSettings); | |
56 | StreamServer(boost::asio::io_context& io_context, const ServerSettings& serverSettings); | |
58 | 57 | virtual ~StreamServer(); |
59 | 58 | |
60 | 59 | void start(); |
82 | 81 | session_ptr getStreamSession(const std::string& mac) const; |
83 | 82 | session_ptr getStreamSession(StreamSession* session) const; |
84 | 83 | void ProcessRequest(const jsonrpcpp::request_ptr request, jsonrpcpp::entity_ptr& response, jsonrpcpp::notification_ptr& notification) const; |
84 | void cleanup(); | |
85 | ||
85 | 86 | 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_; | |
88 | 90 | std::vector<acceptor_ptr> acceptor_; |
89 | 91 | |
90 | 92 | ServerSettings settings_; |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
20 | 20 | #include "common/aixlog.hpp" |
21 | 21 | #include "message/pcm_chunk.hpp" |
22 | 22 | #include <iostream> |
23 | #include <mutex> | |
24 | 23 | |
25 | 24 | 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_); | |
32 | 36 | } |
33 | 37 | |
34 | 38 | |
38 | 42 | } |
39 | 43 | |
40 | 44 | |
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 | ||
41 | 106 | void StreamSession::setPcmStream(PcmStreamPtr pcmStream) |
42 | 107 | { |
43 | 108 | pcmStream_ = pcmStream; |
52 | 117 | |
53 | 118 | void StreamSession::start() |
54 | 119 | { |
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(); }); | |
61 | 122 | } |
62 | 123 | |
63 | 124 | |
64 | 125 | void StreamSession::stop() |
65 | 126 | { |
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"; | |
69 | 178 | 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"; | |
85 | 179 | } |
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) | |
119 | 186 | { |
120 | 187 | if (!message) |
121 | 188 | return; |
122 | 189 | |
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); | |
158 | 191 | tv t; |
159 | 192 | 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 | } |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
15 | 15 | along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | ***/ |
17 | 17 | |
18 | #ifndef STREAM_SESSION_H | |
19 | #define STREAM_SESSION_H | |
18 | #ifndef STREAM_SESSION_HPP | |
19 | #define STREAM_SESSION_HPP | |
20 | 20 | |
21 | 21 | #include "common/queue.h" |
22 | 22 | #include "message/message.hpp" |
24 | 24 | #include <atomic> |
25 | 25 | #include <boost/asio.hpp> |
26 | 26 | #include <condition_variable> |
27 | #include <deque> | |
27 | 28 | #include <memory> |
28 | 29 | #include <mutex> |
29 | 30 | #include <set> |
31 | #include <sstream> | |
30 | 32 | #include <string> |
31 | #include <thread> | |
33 | #include <vector> | |
32 | 34 | |
33 | 35 | |
34 | 36 | using boost::asio::ip::tcp; |
46 | 48 | }; |
47 | 49 | |
48 | 50 | |
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 | ||
49 | 91 | /// Endpoint for a connected client. |
50 | 92 | /** |
51 | 93 | * Endpoint for a connected client. |
52 | 94 | * Messages are sent to the client with the "send" method. |
53 | 95 | * Received messages from the client are passed to the MessageReceiver callback |
54 | 96 | */ |
55 | class StreamSession | |
97 | class StreamSession : public std::enable_shared_from_this<StreamSession> | |
56 | 98 | { |
57 | 99 | public: |
58 | 100 | /// 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); | |
60 | 102 | ~StreamSession(); |
61 | 103 | void start(); |
62 | 104 | void stop(); |
63 | 105 | |
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); | |
66 | 108 | |
67 | 109 | /// 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); | |
71 | 111 | |
72 | 112 | /// Max playout latency. No need to send PCM data that is older than bufferMs |
73 | 113 | void setBufferMs(size_t bufferMs); |
79 | 119 | return socket_.remote_endpoint().address().to_string(); |
80 | 120 | } |
81 | 121 | |
82 | void setPcmStream(PcmStreamPtr pcmStream); | |
83 | const PcmStreamPtr pcmStream() const; | |
122 | void setPcmStream(streamreader::PcmStreamPtr pcmStream); | |
123 | const streamreader::PcmStreamPtr pcmStream() const; | |
84 | 124 | |
85 | 125 | 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(); | |
90 | 128 | |
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_; | |
97 | 132 | tcp::socket socket_; |
98 | 133 | MessageReceiver* messageReceiver_; |
99 | Queue<std::shared_ptr<msg::BaseMessage>> messages_; | |
100 | 134 | size_t bufferMs_; |
101 | PcmStreamPtr pcmStream_; | |
135 | streamreader::PcmStreamPtr pcmStream_; | |
136 | boost::asio::io_context::strand strand_; | |
137 | std::deque<shared_const_buffer> messages_; | |
102 | 138 | }; |
103 | 139 | |
104 | 140 |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
24 | 24 | |
25 | 25 | using namespace std; |
26 | 26 | |
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) | |
28 | 35 | { |
29 | 36 | typedef unsigned char byte; |
30 | 37 | unsigned long x = strtoul(input.c_str(), nullptr, 16); |
31 | 38 | byte a[] = {byte(x >> 24), byte(x >> 16), byte(x >> 8), byte(x), 0}; |
32 | 39 | return string((char*)a); |
33 | 40 | } |
41 | } // namespace | |
34 | 42 | |
35 | 43 | /* |
36 | 44 | * Expat is used in metadata parsing from Shairport-sync. |
40 | 48 | * move to Makefile? |
41 | 49 | */ |
42 | 50 | |
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) | |
44 | 53 | { |
45 | 54 | logStderr_ = true; |
46 | 55 | |
59 | 68 | params_wo_port_ += " --metadata-pipename " + pipePath_; |
60 | 69 | params_ = params_wo_port_ + " --port=" + cpt::to_string(port_); |
61 | 70 | |
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_ | |
62 | 79 | pipeReaderThread_ = thread(&AirplayStream::pipeReader, this); |
63 | 80 | pipeReaderThread_.detach(); |
81 | #endif | |
64 | 82 | } |
65 | 83 | |
66 | 84 | |
114 | 132 | |
115 | 133 | if (entry_->type == "ssnc" && entry_->code == "mden") |
116 | 134 | { |
117 | // LOG(INFO) << "metadata=" << jtag_.dump(4) << "\n"; | |
135 | // LOG(INFO, LOG_TAG) << "metadata=" << jtag_.dump(4) << "\n"; | |
118 | 136 | setMeta(jtag_); |
119 | 137 | } |
120 | 138 | } |
121 | 139 | #endif |
122 | 140 | |
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_ | |
123 | 194 | void AirplayStream::pipeReader() |
124 | 195 | { |
125 | 196 | #ifdef HAS_EXPAT |
146 | 217 | this_thread::sleep_for(chrono::milliseconds(500)); |
147 | 218 | } |
148 | 219 | } |
220 | #endif | |
149 | 221 | |
150 | 222 | void AirplayStream::initExeAndPath(const string& filename) |
151 | 223 | { |
166 | 238 | } |
167 | 239 | |
168 | 240 | |
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()) | |
173 | 244 | 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"; | |
178 | 249 | ++port_; |
179 | 250 | params_ = params_wo_port_ + " --port=" + cpt::to_string(port_); |
180 | 251 | } |
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"; | |
185 | 256 | } |
186 | 257 | } |
187 | 258 | |
236 | 307 | string value(content, (size_t)length); |
237 | 308 | self->buf_.append(value); |
238 | 309 | } |
239 | #endif | |
310 | ||
311 | #endif | |
312 | ||
313 | } // namespace streamreader |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
15 | 15 | along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | ***/ |
17 | 17 | |
18 | #ifndef AIRPLAY_STREAM_H | |
19 | #define AIRPLAY_STREAM_H | |
18 | #ifndef AIRPLAY_STREAM_HPP | |
19 | #define AIRPLAY_STREAM_HPP | |
20 | 20 | |
21 | 21 | #include "process_stream.hpp" |
22 | 22 | |
27 | 27 | #ifdef HAS_EXPAT |
28 | 28 | #include <expat.h> |
29 | 29 | #endif |
30 | ||
31 | namespace streamreader | |
32 | { | |
30 | 33 | |
31 | 34 | class TageEntry |
32 | 35 | { |
55 | 58 | { |
56 | 59 | public: |
57 | 60 | /// 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); | |
59 | 62 | ~AirplayStream() override; |
60 | 63 | |
61 | 64 | protected: |
66 | 69 | std::string buf_; |
67 | 70 | json jtag_; |
68 | 71 | |
69 | void pipeReader(); | |
72 | void pipeReadLine(); | |
70 | 73 | #ifdef HAS_EXPAT |
71 | 74 | int parse(std::string line); |
72 | 75 | void createParser(); |
73 | 76 | void push(); |
74 | 77 | #endif |
75 | 78 | |
76 | void onStderrMsg(const char* buffer, size_t n) override; | |
79 | void do_connect() override; | |
80 | void onStderrMsg(const std::string& line) override; | |
77 | 81 | void initExeAndPath(const std::string& filename) override; |
82 | ||
78 | 83 | size_t port_; |
79 | 84 | std::string pipePath_; |
80 | 85 | 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_; | |
82 | 89 | |
83 | 90 | #ifdef HAS_EXPAT |
84 | 91 | static void XMLCALL element_start(void* userdata, const char* element_name, const char** attr); |
87 | 94 | #endif |
88 | 95 | }; |
89 | 96 | |
97 | } // namespace streamreader | |
90 | 98 | |
91 | 99 | #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 |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
27 | 27 | |
28 | 28 | using namespace std; |
29 | 29 | |
30 | namespace streamreader | |
31 | { | |
32 | ||
33 | static constexpr auto LOG_TAG = "FileStream"; | |
30 | 34 | |
31 | 35 | |
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) | |
33 | 37 | { |
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) | |
36 | 40 | { |
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 + "\""); | |
39 | 46 | } |
40 | 47 | } |
41 | 48 | |
42 | 49 | |
43 | FileStream::~FileStream() | |
50 | void FileStream::do_connect() | |
44 | 51 | { |
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(); | |
46 | 56 | } |
47 | 57 | |
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 |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
15 | 15 | along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | ***/ |
17 | 17 | |
18 | #ifndef FILE_STREAM_H | |
19 | #define FILE_STREAM_H | |
18 | #ifndef FILE_STREAM_HPP | |
19 | #define FILE_STREAM_HPP | |
20 | 20 | |
21 | #include "pcm_stream.hpp" | |
22 | #include <fstream> | |
21 | #include "posix_stream.hpp" | |
23 | 22 | |
23 | namespace streamreader | |
24 | { | |
24 | 25 | |
25 | 26 | /// Reads and decodes PCM data from a file |
26 | 27 | /** |
28 | 29 | * Implements EncoderListener to get the encoded data. |
29 | 30 | * Data is passed to the PcmListener |
30 | 31 | */ |
31 | class FileStream : public PcmStream | |
32 | class FileStream : public PosixStream | |
32 | 33 | { |
33 | 34 | public: |
34 | 35 | /// 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); | |
37 | 37 | |
38 | 38 | protected: |
39 | void worker() override; | |
40 | std::ifstream ifs; | |
39 | void do_connect() override; | |
41 | 40 | }; |
42 | 41 | |
42 | } // namespace streamreader | |
43 | 43 | |
44 | 44 | #endif |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
25 | 25 | |
26 | 26 | using namespace std; |
27 | 27 | |
28 | namespace streamreader | |
29 | { | |
30 | ||
31 | static constexpr auto LOG_TAG = "LibrespotStream"; | |
28 | 32 | |
29 | 33 | |
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) | |
31 | 35 | { |
32 | 36 | sampleFormat_ = SampleFormat("44100:16:2"); |
33 | 37 | 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 | |
34 | 43 | |
35 | 44 | string username = uri_.getQuery("username", ""); |
36 | 45 | string password = uri_.getQuery("password", ""); |
37 | 46 | string cache = uri_.getQuery("cache", ""); |
38 | string volume = uri_.getQuery("volume", ""); | |
47 | string volume = uri_.getQuery("volume", "100"); | |
39 | 48 | string bitrate = uri_.getQuery("bitrate", "320"); |
40 | 49 | string devicename = uri_.getQuery("devicename", "Snapcast"); |
41 | 50 | string onevent = uri_.getQuery("onevent", ""); |
51 | bool normalize = (uri_.getQuery("normalize", "false") == "true"); | |
42 | 52 | |
43 | 53 | if (username.empty() != password.empty()) |
44 | 54 | throw SnapException("missing parameter \"username\" or \"password\" (must provide both, or neither)"); |
50 | 60 | if (!cache.empty()) |
51 | 61 | params_ += " --cache \"" + cache + "\""; |
52 | 62 | if (!volume.empty()) |
53 | params_ += " --initial-volume \"" + volume + "\""; | |
63 | params_ += " --initial-volume " + volume; | |
54 | 64 | if (!onevent.empty()) |
55 | 65 | params_ += " --onevent \"" + onevent + "\""; |
66 | if (normalize) | |
67 | params_ += " --enable-volume-normalisation"; | |
68 | params_ += " --verbose"; | |
56 | 69 | |
57 | 70 | if (uri_.query.find("username") != uri_.query.end()) |
58 | 71 | uri_.query["username"] = "xxx"; |
59 | 72 | if (uri_.query.find("password") != uri_.query.end()) |
60 | 73 | uri_.query["password"] = "xxx"; |
61 | // LOG(INFO) << "params: " << params << "\n"; | |
74 | // LOG(INFO, LOG_TAG) << "params: " << params << "\n"; | |
62 | 75 | } |
63 | ||
64 | ||
65 | LibrespotStream::~LibrespotStream() = default; | |
66 | 76 | |
67 | 77 | |
68 | 78 | void LibrespotStream::initExeAndPath(const std::string& filename) |
87 | 97 | } |
88 | 98 | |
89 | 99 | |
90 | void LibrespotStream::onStderrMsg(const char* buffer, size_t n) | |
100 | void LibrespotStream::onStderrMsg(const std::string& line) | |
91 | 101 | { |
92 | 102 | static bool libreelec_patched = false; |
93 | 103 | smatch m; |
112 | 122 | // 2016-11-03 09-00-18 [out] INFO:librespot::main_helper: librespot 6fa4e4d (2016-09-21). Built on 2016-10-27. |
113 | 123 | // 2016-11-03 09-00-18 [out] INFO:librespot::session: Connecting to AP lon3-accesspoint-a34.ap.spotify.com:443 |
114 | 124 | // 2016-11-03 09-00-18 [out] INFO:librespot::session: Authenticated ! |
115 | watchdog_->trigger(); | |
116 | string logmsg = utils::string::trim_copy(string(buffer, n)); | |
117 | 125 | |
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)) | |
120 | 127 | { |
121 | LOG(INFO) << "(" << getName() << ") " << logmsg << "\n"; | |
128 | LOG(INFO, LOG_TAG) << "(" << getName() << ") " << line << "\n"; | |
122 | 129 | } |
123 | 130 | |
124 | 131 | // Librespot patch: |
131 | 138 | if (!libreelec_patched) |
132 | 139 | { |
133 | 140 | static regex re_nonpatched("Track \"(.*)\" loaded"); |
134 | if (regex_search(logmsg, m, re_nonpatched)) | |
141 | if (regex_search(line, m, re_nonpatched)) | |
135 | 142 | { |
136 | LOG(INFO) << "metadata: <" << m[1] << ">\n"; | |
143 | LOG(INFO, LOG_TAG) << "metadata: <" << m[1] << ">\n"; | |
137 | 144 | |
138 | 145 | json jtag = {{"TITLE", (string)m[1]}}; |
139 | 146 | setMeta(jtag); |
142 | 149 | |
143 | 150 | // Parse the patched version |
144 | 151 | static regex re_patched("metadata:(.*)"); |
145 | if (regex_search(logmsg, m, re_patched)) | |
152 | if (regex_search(line, m, re_patched)) | |
146 | 153 | { |
147 | LOG(INFO) << "metadata: <" << m[1] << ">\n"; | |
154 | LOG(INFO, LOG_TAG) << "metadata: <" << m[1] << ">\n"; | |
148 | 155 | |
149 | 156 | setMeta(json::parse(m[1].str())); |
150 | 157 | libreelec_patched = true; |
151 | 158 | } |
152 | 159 | } |
153 | 160 | |
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 |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
15 | 15 | along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | ***/ |
17 | 17 | |
18 | #ifndef SPOTIFY_STREAM_H | |
19 | #define SPOTIFY_STREAM_H | |
18 | #ifndef SPOTIFY_STREAM_HPP | |
19 | #define SPOTIFY_STREAM_HPP | |
20 | 20 | |
21 | 21 | #include "process_stream.hpp" |
22 | #include "watchdog.h" | |
22 | ||
23 | namespace streamreader | |
24 | { | |
23 | 25 | |
24 | 26 | /// Starts librespot and reads PCM data from stdout |
25 | 27 | /** |
30 | 32 | * snapserver -s "spotify:///librespot?name=Spotify&username=<my username>&password=<my password>[&devicename=Snapcast][&bitrate=320][&volume=<volume in |
31 | 33 | * percent>][&cache=<cache dir>]" |
32 | 34 | */ |
33 | class LibrespotStream : public ProcessStream, WatchdogListener | |
35 | class LibrespotStream : public ProcessStream | |
34 | 36 | { |
35 | 37 | public: |
36 | 38 | /// 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); | |
39 | 40 | |
40 | 41 | 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; | |
45 | 43 | void initExeAndPath(const std::string& filename) override; |
46 | ||
47 | void onTimeout(const Watchdog* watchdog, size_t ms) override; | |
48 | 44 | }; |
49 | 45 | |
46 | } // namespace streamreader | |
50 | 47 | |
51 | 48 | #endif |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
28 | 28 | |
29 | 29 | using namespace std; |
30 | 30 | |
31 | namespace streamreader | |
32 | { | |
31 | 33 | |
32 | 34 | |
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) | |
34 | 37 | { |
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()) | |
37 | 40 | throw SnapException("Stream URI must have a codec"); |
38 | encoder_.reset(encoderFactory.createEncoder(uri_.query["codec"])); | |
41 | encoder_ = encoderFactory.createEncoder(uri_.query[kUriCodec]); | |
39 | 42 | |
40 | if (uri_.query.find("name") == uri_.query.end()) | |
43 | if (uri_.query.find(kUriName) == uri_.query.end()) | |
41 | 44 | throw SnapException("Stream URI must have a name"); |
42 | name_ = uri_.query["name"]; | |
45 | name_ = uri_.query[kUriName]; | |
43 | 46 | |
44 | if (uri_.query.find("sampleformat") == uri_.query.end()) | |
47 | if (uri_.query.find(kUriSampleFormat) == uri_.query.end()) | |
45 | 48 | throw SnapException("Stream URI must have a sampleformat"); |
46 | sampleFormat_ = SampleFormat(uri_.query["sampleformat"]); | |
49 | sampleFormat_ = SampleFormat(uri_.query[kUriSampleFormat]); | |
47 | 50 | LOG(INFO) << "PcmStream sampleFormat: " << sampleFormat_.getFormat() << "\n"; |
48 | 51 | |
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]); | |
51 | 54 | |
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_; | |
59 | 55 | setMeta(json()); |
60 | 56 | } |
61 | 57 | |
101 | 97 | LOG(DEBUG) << "PcmStream start: " << sampleFormat_.getFormat() << "\n"; |
102 | 98 | encoder_->init(this, sampleFormat_); |
103 | 99 | active_ = true; |
104 | thread_ = thread(&PcmStream::worker, this); | |
105 | 100 | } |
106 | 101 | |
107 | 102 | |
108 | 103 | void PcmStream::stop() |
109 | 104 | { |
110 | if (!active_ && !thread_.joinable()) | |
111 | return; | |
112 | ||
113 | 105 | 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_; })); | |
126 | 106 | } |
127 | 107 | |
128 | 108 | |
136 | 116 | { |
137 | 117 | if (newState != state_) |
138 | 118 | { |
119 | LOG(DEBUG) << "State changed: " << static_cast<int>(state_) << " => " << static_cast<int>(newState) << "\n"; | |
139 | 120 | state_ = newState; |
140 | 121 | if (pcmListener_) |
141 | 122 | pcmListener_->onStateChanged(this, newState); |
143 | 124 | } |
144 | 125 | |
145 | 126 | |
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) | |
147 | 128 | { |
148 | 129 | // LOG(INFO) << "onChunkEncoded: " << duration << " us\n"; |
149 | 130 | if (duration <= 0) |
160 | 141 | json PcmStream::toJson() const |
161 | 142 | { |
162 | 143 | string state("unknown"); |
163 | if (state_ == kIdle) | |
144 | if (state_ == ReaderState::kIdle) | |
164 | 145 | state = "idle"; |
165 | else if (state_ == kPlaying) | |
146 | else if (state_ == ReaderState::kPlaying) | |
166 | 147 | state = "playing"; |
167 | else if (state_ == kDisabled) | |
148 | else if (state_ == ReaderState::kDisabled) | |
168 | 149 | state = "disabled"; |
169 | 150 | |
170 | 151 | json j = { |
182 | 163 | return meta_; |
183 | 164 | } |
184 | 165 | |
185 | void PcmStream::setMeta(json jtag) | |
166 | void PcmStream::setMeta(const json& jtag) | |
186 | 167 | { |
187 | 168 | meta_.reset(new msg::StreamTags(jtag)); |
188 | 169 | meta_->msg["STREAM"] = name_; |
192 | 173 | if (pcmListener_) |
193 | 174 | pcmListener_->onMetaChanged(this); |
194 | 175 | } |
176 | ||
177 | } // namespace streamreader |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
15 | 15 | along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | ***/ |
17 | 17 | |
18 | #ifndef PCM_STREAM_H | |
19 | #define PCM_STREAM_H | |
18 | #ifndef PCM_STREAM_HPP | |
19 | #define PCM_STREAM_HPP | |
20 | 20 | |
21 | 21 | #include "common/json.hpp" |
22 | 22 | #include "common/sample_format.hpp" |
25 | 25 | #include "message/stream_tags.hpp" |
26 | 26 | #include "stream_uri.hpp" |
27 | 27 | #include <atomic> |
28 | #include <boost/asio/io_context.hpp> | |
28 | 29 | #include <condition_variable> |
29 | 30 | #include <map> |
30 | 31 | #include <mutex> |
31 | 32 | #include <string> |
32 | #include <thread> | |
33 | 33 | |
34 | ||
35 | namespace streamreader | |
36 | { | |
34 | 37 | |
35 | 38 | class PcmStream; |
36 | 39 | |
37 | enum ReaderState | |
40 | enum class ReaderState | |
38 | 41 | { |
39 | 42 | kUnknown = 0, |
40 | 43 | kIdle = 1, |
41 | 44 | kPlaying = 2, |
42 | 45 | kDisabled = 3 |
43 | 46 | }; |
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"; | |
44 | 53 | |
45 | 54 | |
46 | 55 | /// Callback interface for users of PcmStream |
63 | 72 | * Implements EncoderListener to get the encoded data. |
64 | 73 | * Data is passed to the PcmListener |
65 | 74 | */ |
66 | class PcmStream : public EncoderListener | |
75 | class PcmStream : public encoder::EncoderListener | |
67 | 76 | { |
68 | 77 | public: |
69 | 78 | /// 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); | |
71 | 80 | virtual ~PcmStream(); |
72 | 81 | |
73 | 82 | virtual void start(); |
74 | 83 | virtual void stop(); |
75 | 84 | |
76 | 85 | /// 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; | |
78 | 87 | virtual std::shared_ptr<msg::CodecHeader> getHeader(); |
79 | 88 | |
80 | 89 | virtual const StreamUri& getUri() const; |
83 | 92 | virtual const SampleFormat& getSampleFormat() const; |
84 | 93 | |
85 | 94 | std::shared_ptr<msg::StreamTags> getMeta() const; |
86 | void setMeta(json j); | |
95 | void setMeta(const json& j); | |
87 | 96 | |
88 | 97 | virtual ReaderState getState() const; |
89 | 98 | virtual json toJson() const; |
90 | 99 | |
91 | 100 | |
92 | 101 | protected: |
93 | std::condition_variable cv_; | |
94 | std::mutex mtx_; | |
95 | std::thread thread_; | |
96 | 102 | std::atomic<bool> active_; |
97 | 103 | |
98 | virtual void worker() = 0; | |
99 | virtual bool sleep(int32_t ms); | |
100 | 104 | void setState(const ReaderState& newState); |
101 | 105 | |
102 | 106 | timeval tvEncodedChunk_; |
103 | 107 | PcmListener* pcmListener_; |
104 | 108 | StreamUri uri_; |
105 | 109 | 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_; | |
109 | 112 | std::string name_; |
110 | 113 | ReaderState state_; |
111 | 114 | std::shared_ptr<msg::StreamTags> meta_; |
115 | boost::asio::io_context& ioc_; | |
112 | 116 | }; |
113 | 117 | |
118 | } // namespace streamreader | |
114 | 119 | |
115 | 120 | #endif |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
24 | 24 | #include "common/aixlog.hpp" |
25 | 25 | #include "common/snap_exception.hpp" |
26 | 26 | #include "common/str_compat.hpp" |
27 | #include "encoder/encoder_factory.hpp" | |
28 | 27 | #include "pipe_stream.hpp" |
29 | 28 | |
30 | 29 | |
31 | 30 | using namespace std; |
32 | 31 | |
32 | namespace streamreader | |
33 | { | |
34 | ||
35 | static constexpr auto LOG_TAG = "PipeStream"; | |
33 | 36 | |
34 | 37 | |
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) | |
36 | 39 | { |
37 | 40 | umask(0); |
38 | 41 | string mode = uri_.getQuery("mode", "create"); |
39 | 42 | |
40 | LOG(INFO) << "PipeStream mode: " << mode << "\n"; | |
43 | LOG(INFO, LOG_TAG) << "PipeStream mode: " << mode << "\n"; | |
41 | 44 | if ((mode != "read") && (mode != "create")) |
42 | 45 | throw SnapException("create mode for fifo must be \"read\" or \"create\""); |
43 | 46 | |
49 | 52 | } |
50 | 53 | |
51 | 54 | |
52 | PipeStream::~PipeStream() | |
55 | void PipeStream::do_connect() | |
53 | 56 | { |
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(); | |
56 | 61 | } |
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 | } | |
152 | 62 | } |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
15 | 15 | along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | ***/ |
17 | 17 | |
18 | #ifndef PIPE_STREAM_H | |
19 | #define PIPE_STREAM_H | |
18 | #ifndef PIPE_STREAM_HPP | |
19 | #define PIPE_STREAM_HPP | |
20 | 20 | |
21 | #include "pcm_stream.hpp" | |
21 | #include "posix_stream.hpp" | |
22 | 22 | |
23 | namespace streamreader | |
24 | { | |
25 | ||
26 | using boost::asio::posix::stream_descriptor; | |
23 | 27 | |
24 | 28 | |
25 | 29 | /// Reads and decodes PCM data from a named pipe |
28 | 32 | * Implements EncoderListener to get the encoded data. |
29 | 33 | * Data is passed to the PcmListener |
30 | 34 | */ |
31 | class PipeStream : public PcmStream | |
35 | class PipeStream : public PosixStream | |
32 | 36 | { |
33 | 37 | public: |
34 | 38 | /// 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); | |
37 | 40 | |
38 | 41 | protected: |
39 | void worker() override; | |
40 | int fd_; | |
42 | void do_connect() override; | |
41 | 43 | }; |
42 | 44 | |
45 | } // namespace streamreader | |
43 | 46 | |
44 | 47 | #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 | /*** | |
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_ |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
28 | 28 | |
29 | 29 | using namespace std; |
30 | 30 | |
31 | namespace streamreader | |
32 | { | |
33 | ||
34 | static constexpr auto LOG_TAG = "ProcessStream"; | |
31 | 35 | |
32 | 36 | |
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) | |
34 | 38 | { |
35 | 39 | 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"); | |
44 | 43 | } |
45 | 44 | |
46 | 45 | |
96 | 95 | } |
97 | 96 | |
98 | 97 | |
99 | void ProcessStream::start() | |
98 | void ProcessStream::do_connect() | |
100 | 99 | { |
100 | if (!active_) | |
101 | return; | |
101 | 102 | 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(); | |
103 | 127 | } |
104 | 128 | |
105 | 129 | |
106 | void ProcessStream::stop() | |
130 | void ProcessStream::do_disconnect() | |
107 | 131 | { |
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); | |
115 | 134 | } |
116 | 135 | |
117 | 136 | |
118 | void ProcessStream::onStderrMsg(const char* buffer, size_t n) | |
137 | void ProcessStream::onStderrMsg(const std::string& line) | |
119 | 138 | { |
120 | 139 | if (logStderr_) |
121 | 140 | { |
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"; | |
125 | 142 | } |
126 | 143 | } |
127 | 144 | |
128 | 145 | |
129 | void ProcessStream::stderrReader() | |
146 | void ProcessStream::stderrReadLine() | |
130 | 147 | { |
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 | }); | |
137 | 170 | } |
138 | 171 | |
139 | 172 | |
140 | void ProcessStream::worker() | |
173 | void ProcessStream::onTimeout(const Watchdog& /*watchdog*/, std::chrono::milliseconds ms) | |
141 | 174 | { |
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 | } | |
146 | 179 | |
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 |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
15 | 15 | along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | ***/ |
17 | 17 | |
18 | #ifndef PROCESS_STREAM_H | |
19 | #define PROCESS_STREAM_H | |
18 | #ifndef PROCESS_STREAM_HPP | |
19 | #define PROCESS_STREAM_HPP | |
20 | 20 | |
21 | #include <boost/process.hpp> | |
21 | 22 | #include <memory> |
22 | 23 | #include <string> |
23 | 24 | |
24 | #include "pcm_stream.hpp" | |
25 | #include "process.hpp" | |
25 | #include "posix_stream.hpp" | |
26 | #include "watchdog.hpp" | |
26 | 27 | |
28 | ||
29 | namespace bp = boost::process; | |
30 | ||
31 | ||
32 | namespace streamreader | |
33 | { | |
27 | 34 | |
28 | 35 | /// Starts an external process and reads and PCM data from stdout |
29 | 36 | /** |
31 | 38 | * Implements EncoderListener to get the encoded data. |
32 | 39 | * Data is passed to the PcmListener |
33 | 40 | */ |
34 | class ProcessStream : public PcmStream | |
41 | class ProcessStream : public PosixStream, public WatchdogListener | |
35 | 42 | { |
36 | 43 | public: |
37 | 44 | /// 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; | |
43 | 47 | |
44 | 48 | protected: |
49 | void do_connect() override; | |
50 | void do_disconnect() override; | |
51 | ||
45 | 52 | std::string exe_; |
46 | 53 | std::string path_; |
47 | 54 | 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 | ||
50 | 59 | bool logStderr_; |
60 | boost::asio::streambuf streambuf_stderr_; | |
61 | std::unique_ptr<stream_descriptor> stream_stderr_; | |
51 | 62 | |
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); | |
55 | 66 | virtual void initExeAndPath(const std::string& filename); |
56 | 67 | |
57 | 68 | bool fileExists(const std::string& filename); |
58 | 69 | 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; | |
59 | 74 | }; |
60 | 75 | |
76 | } // namespace streamreader | |
61 | 77 | |
62 | 78 | #endif |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
25 | 25 | #include "librespot_stream.hpp" |
26 | 26 | #include "pipe_stream.hpp" |
27 | 27 | #include "process_stream.hpp" |
28 | #include "tcp_stream.hpp" | |
28 | 29 | |
29 | 30 | |
30 | 31 | using namespace std; |
31 | 32 | |
33 | namespace streamreader | |
34 | { | |
32 | 35 | |
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) | |
35 | 39 | { |
36 | 40 | } |
37 | 41 | |
40 | 44 | { |
41 | 45 | StreamUri streamUri(uri); |
42 | 46 | |
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_; | |
45 | 49 | |
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_; | |
48 | 52 | |
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_); | |
51 | 55 | |
52 | 56 | // LOG(DEBUG) << "\nURI: " << streamUri.uri << "\nscheme: " << streamUri.scheme << "\nhost: " |
53 | 57 | // << streamUri.host << "\npath: " << streamUri.path << "\nfragment: " << streamUri.fragment << "\n"; |
58 | 62 | |
59 | 63 | if (streamUri.scheme == "pipe") |
60 | 64 | { |
61 | stream = make_shared<PipeStream>(pcmListener_, streamUri); | |
65 | stream = make_shared<PipeStream>(pcmListener_, ioc_, streamUri); | |
62 | 66 | } |
63 | 67 | else if (streamUri.scheme == "file") |
64 | 68 | { |
65 | stream = make_shared<FileStream>(pcmListener_, streamUri); | |
69 | stream = make_shared<FileStream>(pcmListener_, ioc_, streamUri); | |
66 | 70 | } |
67 | 71 | else if (streamUri.scheme == "process") |
68 | 72 | { |
69 | stream = make_shared<ProcessStream>(pcmListener_, streamUri); | |
73 | stream = make_shared<ProcessStream>(pcmListener_, ioc_, streamUri); | |
70 | 74 | } |
71 | 75 | else if ((streamUri.scheme == "spotify") || (streamUri.scheme == "librespot")) |
72 | 76 | { |
73 | stream = make_shared<LibrespotStream>(pcmListener_, streamUri); | |
77 | stream = make_shared<LibrespotStream>(pcmListener_, ioc_, streamUri); | |
74 | 78 | } |
75 | 79 | else if (streamUri.scheme == "airplay") |
76 | 80 | { |
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); | |
78 | 86 | } |
79 | 87 | else |
80 | 88 | { |
83 | 91 | |
84 | 92 | if (stream) |
85 | 93 | { |
86 | for (auto s : streams_) | |
94 | for (const auto& s : streams_) | |
87 | 95 | { |
88 | 96 | if (s->getName() == stream->getName()) |
89 | 97 | throw SnapException("Stream with name \"" + stream->getName() + "\" already exists"); |
97 | 105 | |
98 | 106 | void StreamManager::removeStream(const std::string& name) |
99 | 107 | { |
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()) | |
103 | 110 | { |
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); | |
111 | 113 | } |
112 | 114 | } |
115 | ||
113 | 116 | |
114 | 117 | const std::vector<PcmStreamPtr>& StreamManager::getStreams() |
115 | 118 | { |
139 | 142 | |
140 | 143 | void StreamManager::start() |
141 | 144 | { |
142 | for (auto stream : streams_) | |
145 | for (const auto& stream : streams_) | |
143 | 146 | stream->start(); |
144 | 147 | } |
145 | 148 | |
146 | 149 | |
147 | 150 | void StreamManager::stop() |
148 | 151 | { |
149 | for (auto stream : streams_) | |
150 | stream->stop(); | |
152 | for (const auto& stream : streams_) | |
153 | if (stream) | |
154 | stream->stop(); | |
151 | 155 | } |
152 | 156 | |
153 | 157 | |
158 | 162 | result.push_back(stream->toJson()); |
159 | 163 | return result; |
160 | 164 | } |
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 | |
2 | 20 | |
3 | 21 | #include "pcm_stream.hpp" |
22 | #include <boost/asio/io_context.hpp> | |
4 | 23 | #include <memory> |
5 | 24 | #include <string> |
6 | 25 | #include <vector> |
26 | ||
27 | namespace streamreader | |
28 | { | |
7 | 29 | |
8 | 30 | typedef std::shared_ptr<PcmStream> PcmStreamPtr; |
9 | 31 | |
10 | 32 | class StreamManager |
11 | 33 | { |
12 | 34 | 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); | |
14 | 37 | |
15 | 38 | PcmStreamPtr addStream(const std::string& uri); |
16 | 39 | void removeStream(const std::string& name); |
26 | 49 | PcmListener* pcmListener_; |
27 | 50 | std::string sampleFormat_; |
28 | 51 | std::string codec_; |
29 | size_t readBufferMs_; | |
52 | size_t chunkBufferMs_; | |
53 | boost::asio::io_context& ioc_; | |
30 | 54 | }; |
31 | 55 | |
56 | } // namespace streamreader | |
32 | 57 | |
33 | 58 | #endif |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
24 | 24 | using namespace std; |
25 | 25 | namespace strutils = utils::string; |
26 | 26 | |
27 | namespace streamreader | |
28 | { | |
27 | 29 | |
28 | 30 | StreamUri::StreamUri(const std::string& uri) |
29 | 31 | { |
30 | 32 | parse(uri); |
31 | 33 | } |
32 | ||
33 | 34 | |
34 | 35 | |
35 | 36 | void StreamUri::parse(const std::string& streamUri) |
63 | 64 | |
64 | 65 | pos = tmp.find('/'); |
65 | 66 | 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 | ||
67 | 73 | host = strutils::trim_copy(tmp.substr(0, pos)); |
68 | 74 | tmp = tmp.substr(pos); |
69 | 75 | path = tmp; |
140 | 146 | return iter->second; |
141 | 147 | return def; |
142 | 148 | } |
149 | } |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
15 | 15 | along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | ***/ |
17 | 17 | |
18 | #ifndef READER_URI_H | |
19 | #define READER_URI_H | |
18 | #ifndef STREAM_URI_HPP | |
19 | #define STREAM_URI_HPP | |
20 | 20 | |
21 | 21 | #include <map> |
22 | 22 | #include <string> |
26 | 26 | |
27 | 27 | using json = nlohmann::json; |
28 | 28 | |
29 | namespace streamreader | |
30 | { | |
29 | 31 | |
30 | 32 | // scheme:[//[user:password@]host[:port]][/]path[?query][#fragment] |
31 | 33 | struct StreamUri |
55 | 57 | std::string toString() const; |
56 | 58 | }; |
57 | 59 | |
60 | } // namespace streamreader | |
58 | 61 | |
59 | 62 | #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 |
0 | 0 | /*** |
1 | 1 | This file is part of snapcast |
2 | Copyright (C) 2014-2019 Johannes Pohl | |
2 | Copyright (C) 2014-2020 Johannes Pohl | |
3 | 3 | |
4 | 4 | This program is free software: you can redistribute it and/or modify |
5 | 5 | it under the terms of the GNU General Public License as published by |
15 | 15 | along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | ***/ |
17 | 17 | |
18 | #include "watchdog.h" | |
18 | #include "watchdog.hpp" | |
19 | #include "common/aixlog.hpp" | |
19 | 20 | #include <chrono> |
21 | ||
22 | ||
23 | static constexpr auto LOG_TAG = "Watchdog"; | |
20 | 24 | |
21 | 25 | |
22 | 26 | using namespace std; |
23 | 27 | |
28 | namespace streamreader | |
29 | { | |
24 | 30 | |
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) | |
26 | 32 | { |
27 | 33 | } |
28 | 34 | |
33 | 39 | } |
34 | 40 | |
35 | 41 | |
36 | void Watchdog::start(size_t timeoutMs) | |
42 | void Watchdog::start(const std::chrono::milliseconds& timeout) | |
37 | 43 | { |
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(); | |
46 | 47 | } |
47 | 48 | |
48 | 49 | |
49 | 50 | void Watchdog::stop() |
50 | 51 | { |
51 | active_ = false; | |
52 | trigger(); | |
53 | if (thread_ && thread_->joinable()) | |
54 | thread_->join(); | |
55 | thread_ = nullptr; | |
52 | timer_.cancel(); | |
56 | 53 | } |
57 | 54 | |
58 | 55 | |
59 | 56 | void Watchdog::trigger() |
60 | 57 | { |
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 | }); | |
63 | 67 | } |
64 | 68 | |
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 | /*** | |
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 |