Codebase list snapcast / bb98b64
New upstream version 0.22.0+dfsg1 Felix Geyer 3 years ago
167 changed file(s) with 11710 addition(s) and 3235 deletion(s). Raw diff Collapse all Expand all
0 ---
1 name: Bug report
2 about: 'Report a bug'
3 title: ''
4 labels: bug
5 assignees: ''
6
7 ---
8
9 **Describe the bug**
10 A clear and concise description of what the bug is.
11
12 **Steps to Reproduce**
13 1.
14 2.
15 3.
16
17 **Environment details**
18 - OS: [e.g. Raspbian, debian, Windows]
19 - Snapcast version [e.g. 0.21.0]
20 - Installed from a package, self compiled, ...
21
22 **Attach logfile if applicable**
23 Generate logs with `snapclient --logfilter debug` or `snapserver --logging.filter debug` if possible and paste them in the following codeblock
24
25 ```log
26 # Replace this with your logs
27 ```
0 ---
1 name: Feature request
2 about: Suggest an idea or enhancement for snapcast
3 title: ''
4 labels: feature request
5 assignees: ''
6
7 ---
8
9 **Is your feature request related to a problem? Please describe.**
10 A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
11
12 **Describe the solution you'd like**
13 A clear and concise description of what you want to happen.
14
15 **Describe alternatives you've considered**
16 A clear and concise description of any alternative solutions or features you've considered.
17
18 **Additional context**
19 Add any other context or screenshots about the feature request here.
0 [Describe your pull request here. Please read the text below the line, and make sure you follow the checklist.]
1
2 * * *
3
4 ## Pull Request Checklist
5
6 * Contributions must be licensed under the [GPL-3.0 License](LICENSE)
7
8 * This project loosely follows the [Google C++ Style Guide](https://google.github.io/styleguide/cppguide.html)
9
10 * For better compatibility with embedded toolchains, the used C++ standard should be limited to C++14
11
12 * Code should be formatted by running `make reformat`
13
14 * Branch from the `develop` branch and ensure it is up to date with the current `develop` branch before submitting your pull request. If it doesn't merge cleanly with `develop`, you may be asked to resolve the conflicts. Pull requests to master will be closed.
15
16 * Commits should be as small as possible while ensuring that each commit is correct independently (i.e., each commit should compile and pass tests).
17
18 * Pull requests must not contain compiled sources (already set by the default .gitignore) or binary files
19
20 * Test your changes as thoroughly as possible before you commit them. Preferably, automate your test by unit/integration tests. If tested manually, provide information about the test scope in the PR description (e.g. “Test passed: Upgrade version from 0.42 to 0.42.23.”).
21
22 * Create _Work In Progress [WIP]_ pull requests only if you need clarification or an explicit review before you can continue your work item.
23
24 * If your patch is not getting reviewed or you need a specific person to review it, you can @-reply a reviewer asking for a review in the pull request or a comment, or you can ask for a review by contacting us via [email](mailto:snapcast@badaix.de).
25
26 * Post review:
27 * If a review requires you to change your commit(s), please test the changes again.
28 * Amend the affected commit(s) and force push onto your branch.
29 * Set respective comments in your GitHub review to resolved.
30 * Create a general PR comment to notify the reviewers that your amendments are ready for another round of review.
0 name: macOS
1
2 on: [push, pull_request]
3
4 jobs:
5 build:
6
7 runs-on: macos-latest
8
9 steps:
10 - uses: actions/checkout@v2
11 - name: dependencies
12 run: brew install pkgconfig libsoxr ccache expat
13 - name: cache boost
14 id: cache-boost
15 uses: actions/cache@v2
16 with:
17 path: boost_1_74_0
18 key: ${{ runner.os }}-boost
19 - name: get boost
20 if: steps.cache-boost.outputs.cache-hit != 'true'
21 run: wget https://dl.bintray.com/boostorg/release/1.74.0/source/boost_1_74_0.tar.bz2 && tar xjf boost_1_74_0.tar.bz2
22 - name: cache ccache
23 id: cache-ccache
24 uses: actions/cache@v2
25 with:
26 path: /Users/runner/.ccache
27 key: ${{ runner.os }}-ccache-${{ github.sha }}
28 restore-keys: ${{ runner.os }}-ccache-
29 #- name: ccache dump config
30 # run: ccache -p
31 - name: cmake build
32 run: cmake -S . -B build -DBOOST_ROOT=boost_1_74_0 -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_FLAGS="$CXXFLAGS -Werror -Wall -Wextra -pedantic -Wno-unused-function -I/usr/local/include"
33 - name: cmake make
34 run: cmake --build build --parallel 3
0 name: self-hosted
1
2 on: [push, pull_request]
3
4 jobs:
5 build:
6
7 runs-on: self-hosted-rpi3
8
9 steps:
10 - name: clean-up
11 run: rm -rf /home/pi/actions-runner/_work/snapcast/snap*_armhf.deb
12 - uses: actions/checkout@v2
13 - name: cmake build
14 run: mkdir build && cd build && cmake -DBOOST_ROOT=/home/pi/Develop/boost_1_74_0 -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_FLAGS="$CXXFLAGS -Wall -Wextra -pedantic -Wno-unused-function" .. && cd ..
15 - name: cmake make
16 run: cmake --build build
17 - name: debian package
18 run: fakeroot make -f debian/rules CMAKEFLAGS="-DBOOST_ROOT=/home/pi/Develop/boost_1_74_0 -DCMAKE_CXX_COMPILER_LAUNCHER=ccache" binary
19 - name: Archive artifacts
20 uses: actions/upload-artifact@v2
21 with:
22 name: develop_snapshot_armhf-${{github.sha}}
23 path: /home/pi/actions-runner/_work/snapcast/snap??????_*_armhf.deb
0 name: Ubuntu
1
2 on: [push, pull_request]
3
4 jobs:
5 build:
6
7 runs-on: ubuntu-16.04
8 #ubuntu-latest
9
10 steps:
11 - uses: actions/checkout@v2
12 - name: dependencies
13 run: sudo apt-get update && sudo apt-get install -yq libasound2-dev libsoxr-dev libvorbisidec-dev libvorbis-dev libflac-dev libopus-dev alsa-utils libavahi-client-dev avahi-daemon debhelper ccache expat
14 - name: cache boost
15 id: cache-boost
16 uses: actions/cache@v2
17 with:
18 path: boost_1_74_0
19 key: ${{ runner.os }}-boost
20 - name: get boost
21 if: steps.cache-boost.outputs.cache-hit != 'true'
22 run: wget https://dl.bintray.com/boostorg/release/1.74.0/source/boost_1_74_0.tar.bz2 && tar xjf boost_1_74_0.tar.bz2
23 - name: cache ccache
24 id: cache-ccache
25 uses: actions/cache@v2
26 with:
27 path: /home/runner/.ccache
28 key: ${{ runner.os }}-ccache-${{ github.sha }}
29 restore-keys: ${{ runner.os }}-ccache-
30 #- name: ccache dump config
31 # run: ccache -p
32 - name: cmake build
33 run: cmake -S . -B build -DBOOST_ROOT=boost_1_74_0 -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_FLAGS="$CXXFLAGS -Werror -Wall -Wextra -pedantic -Wno-unused-function"
34 - name: cmake make
35 run: cmake --build build --parallel 3
36 - name: debian package
37 run: fakeroot make -f debian/rules CMAKEFLAGS="-DBOOST_ROOT=boost_1_74_0 -DCMAKE_CXX_COMPILER_LAUNCHER=ccache" binary
38 - name: Archive artifacts
39 uses: actions/upload-artifact@v2
40 with:
41 name: develop_snapshot_amd64-${{github.sha}}
42 path: /home/runner/work/snapcast/snap*_amd64.deb
0 name: Windows
1
2 on: [push, pull_request]
3
4 jobs:
5 build:
6
7 runs-on: windows-latest
8
9 steps:
10 - uses: actions/checkout@v2
11 - name: cache dependencies
12 id: cache-dependencies
13 uses: actions/cache@v2
14 with:
15 #path: ${VCPKG_INSTALLATION_ROOT}\installed
16 path: c:\vcpkg\installed
17 key: ${{ runner.os }}-dependencies
18 - name: dependencies
19 if: steps.cache-dependencies.outputs.cache-hit != 'true'
20 run: vcpkg.exe install libflac libvorbis soxr opus boost-asio --triplet x64-windows
21 - name: cmake build
22 run: |
23 echo vcpkg installation root: ${env:VCPKG_INSTALLATION_ROOT}
24 cmake -S . -B build -G "Visual Studio 16 2019" -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" -DVCPKG_TARGET_TRIPLET="x64-windows" -DCMAKE_BUILD_TYPE="Release"
25 - name: cmake make
26 run: cmake --build build --config Release --parallel 3 --verbose
27 - name: Archive artifacts
28 uses: actions/upload-artifact@v2
29 with:
30 name: develop_snapshot_win64-${{github.sha}}
31 path: bin\Release\snapclient.exe
32
5454 debian/snapserver.postrm.debhelper
5555 debian/snapserver.prerm.debhelper
5656 debian/snapserver.substvars
57 debian/tmp
58 obj-x86_64-linux-gnu
00 [submodule "externals/flac"]
11 path = externals/flac
2 url = https://git.xiph.org/flac.git
2 url = https://gitlab.xiph.org/xiph/flac.git
33 [submodule "externals/ogg"]
44 path = externals/ogg
5 url = https://git.xiph.org/ogg.git
5 url = https://gitlab.xiph.org/xiph/ogg.git
66 [submodule "externals/tremor"]
77 path = externals/tremor
8 url = https://git.xiph.org/tremor.git
8 url = https://gitlab.xiph.org/xiph/tremor.git
99 [submodule "externals/opus"]
1010 path = externals/opus
11 url = https://git.xiph.org/opus.git
11 url = https://gitlab.xiph.org/xiph/opus.git
1212 [submodule "externals/oboe"]
1313 path = externals/oboe
1414 url = https://github.com/google/oboe.git
15 [submodule "externals/soxr"]
16 path = externals/soxr
17 url = git://git.code.sf.net/p/soxr/code
22 sudo: required
33 group: edge
44
5 git:
6 submodules: false
7
58 matrix:
69 include:
7 - os: linux
8 compiler: gcc
9 env:
10 - COMPILER=g++-4.9
11 addons:
12 apt:
13 sources:
14 - sourceline: 'ppa:mhier/libboost-latest'
15 - ubuntu-toolchain-r-test
16 packages:
17 - g++-4.9 boost1.70 libasound2-dev libsoxr-dev libvorbisidec-dev libvorbis-dev libflac-dev libopus-dev alsa-utils libavahi-client-dev avahi-daemon
10 # - os: linux
11 # compiler: gcc
12 # env:
13 # - COMPILER=g++-4.9
14 # addons:
15 # apt:
16 # sources:
17 # - sourceline: 'ppa:mhier/libboost-latest'
18 # - ubuntu-toolchain-r-test
19 # packages:
20 # - g++-4.9 boost1.74 libasound2-dev libsoxr-dev libvorbisidec-dev libvorbis-dev libflac-dev libopus-dev alsa-utils libavahi-client-dev avahi-daemon expat
1821
1922 - os: linux
2023 compiler: gcc
2629 - sourceline: 'ppa:mhier/libboost-latest'
2730 - ubuntu-toolchain-r-test
2831 packages:
29 - g++-5 boost1.70 libasound2-dev libsoxr-dev libvorbisidec-dev libvorbis-dev libflac-dev libopus-dev alsa-utils libavahi-client-dev avahi-daemon
32 - g++-5 boost1.74 libasound2-dev libsoxr-dev libvorbisidec-dev libvorbis-dev libflac-dev libopus-dev alsa-utils libavahi-client-dev avahi-daemon expat
3033
3134 - os: linux
3235 compiler: gcc
3841 - sourceline: 'ppa:mhier/libboost-latest'
3942 - ubuntu-toolchain-r-test
4043 packages:
41 - g++-6 boost1.70 libasound2-dev libsoxr-dev libvorbisidec-dev libvorbis-dev libflac-dev libopus-dev alsa-utils libavahi-client-dev avahi-daemon
44 - g++-6 boost1.74 libasound2-dev libsoxr-dev libvorbisidec-dev libvorbis-dev libflac-dev libopus-dev alsa-utils libavahi-client-dev avahi-daemon expat
4245
4346 - os: linux
4447 compiler: gcc
5053 - sourceline: 'ppa:mhier/libboost-latest'
5154 - ubuntu-toolchain-r-test
5255 packages:
53 - g++-7 boost1.70 libasound2-dev libsoxr-dev libvorbisidec-dev libvorbis-dev libflac-dev libopus-dev alsa-utils libavahi-client-dev avahi-daemon
56 - g++-7 boost1.74 libasound2-dev libsoxr-dev libvorbisidec-dev libvorbis-dev libflac-dev libopus-dev alsa-utils libavahi-client-dev avahi-daemon expat
5457
5558 - os: linux
5659 compiler: gcc
6265 - sourceline: 'ppa:mhier/libboost-latest'
6366 - ubuntu-toolchain-r-test
6467 packages:
65 - g++-8 boost1.70 libasound2-dev libsoxr-dev libvorbisidec-dev libvorbis-dev libflac-dev libopus-dev alsa-utils libavahi-client-dev avahi-daemon
68 - g++-8 boost1.74 libasound2-dev libsoxr-dev libvorbisidec-dev libvorbis-dev libflac-dev libopus-dev alsa-utils libavahi-client-dev avahi-daemon expat
6669
6770 - os: linux
6871 compiler: gcc
7477 - sourceline: 'ppa:mhier/libboost-latest'
7578 - ubuntu-toolchain-r-test
7679 packages:
77 - g++-9 boost1.70 libasound2-dev libsoxr-dev libvorbisidec-dev libvorbis-dev libflac-dev libopus-dev alsa-utils libavahi-client-dev avahi-daemon
80 - g++-9 boost1.74 libasound2-dev libsoxr-dev libvorbisidec-dev libvorbis-dev libflac-dev libopus-dev alsa-utils libavahi-client-dev avahi-daemon expat
7881
7982
8083 - os: linux
8992 - sourceline: 'ppa:mhier/libboost-latest'
9093 - llvm-toolchain-trusty-3.9
9194 packages:
92 - clang-3.9 boost1.70 libasound2-dev libsoxr-dev libvorbisidec-dev libvorbis-dev libflac-dev libopus-dev alsa-utils libavahi-client-dev avahi-daemon
95 - clang-3.9 boost1.74 libasound2-dev libsoxr-dev libvorbisidec-dev libvorbis-dev libflac-dev libopus-dev alsa-utils libavahi-client-dev avahi-daemon expat
9396
9497 - os: linux
9598 compiler: clang
103106 - sourceline: 'ppa:mhier/libboost-latest'
104107 - llvm-toolchain-trusty-4.0
105108 packages:
106 - clang-4.0 boost1.70 libasound2-dev libsoxr-dev libvorbisidec-dev libvorbis-dev libflac-dev libopus-dev alsa-utils libavahi-client-dev avahi-daemon
109 - clang-4.0 boost1.74 libasound2-dev libsoxr-dev libvorbisidec-dev libvorbis-dev libflac-dev libopus-dev alsa-utils libavahi-client-dev avahi-daemon expat
107110
108111 - os: linux
109112 compiler: clang
117120 - sourceline: 'ppa:mhier/libboost-latest'
118121 - llvm-toolchain-trusty-5.0
119122 packages:
120 - clang-5.0 boost1.70 libasound2-dev libsoxr-dev libvorbisidec-dev libvorbis-dev libflac-dev libopus-dev alsa-utils libavahi-client-dev avahi-daemon
123 - clang-5.0 boost1.74 libasound2-dev libsoxr-dev libvorbisidec-dev libvorbis-dev libflac-dev libopus-dev alsa-utils libavahi-client-dev avahi-daemon expat
121124
122125 - os: linux
123126 compiler: clang
131134 - llvm-toolchain-trusty-6.0
132135 - ubuntu-toolchain-r-test
133136 packages:
134 - clang-6.0 boost1.70 libasound2-dev libsoxr-dev libvorbisidec-dev libvorbis-dev libflac-dev libopus-dev alsa-utils libavahi-client-dev avahi-daemon
137 - clang-6.0 boost1.74 libasound2-dev libsoxr-dev libvorbisidec-dev libvorbis-dev libflac-dev libopus-dev alsa-utils libavahi-client-dev avahi-daemon expat
135138
136139 - os: linux
137140 compiler: clang
145148 - llvm-toolchain-trusty-7
146149 - ubuntu-toolchain-r-test
147150 packages:
148 - clang-7 boost1.70 libasound2-dev libsoxr-dev libvorbisidec-dev libvorbis-dev libflac-dev libopus-dev alsa-utils libavahi-client-dev avahi-daemon
151 - clang-7 boost1.74 libasound2-dev libsoxr-dev libvorbisidec-dev libvorbis-dev libflac-dev libopus-dev alsa-utils libavahi-client-dev avahi-daemon expat
149152
150153 # build on osx
151154 - os: osx
152155 osx_image: xcode9.4
153156 env:
154 - MATRIX_EVAL="brew update && brew unlink python@2 && brew upgrade boost && brew install flac opus libvorbis libsoxr"
157 - MATRIX_EVAL="brew update && brew unlink python@2 && brew upgrade boost && brew install flac opus libvorbis libsoxr expat"
155158
156159 - os: osx
157160 osx_image: xcode10.3
158161 env:
159 - MATRIX_EVAL="brew update && brew install flac opus libvorbis libsoxr"
162 - MATRIX_EVAL="brew update && brew install flac opus libvorbis libsoxr expat"
160163
161164 - os: osx
162165 osx_image: xcode11
163166 env:
164 - MATRIX_EVAL="brew update && brew install flac opus libvorbis libsoxr"
167 - MATRIX_EVAL="brew update && brew install flac opus libvorbis libsoxr expat"
168
169 # build on windows
170 - os: windows
165171
166172 before_install:
167173 - eval "${MATRIX_EVAL}"
174 - |
175 (
176 if [ "$TRAVIS_OS_NAME" = 'windows' ]; then
177 curl -LfsS -o /tmp/travis-wait-enhanced.zip "https://github.com/crazy-max/travis-wait-enhanced/releases/download/v1.1.0/travis-wait-enhanced_1.1.0_windows_x86_64.zip"
178 7z x /tmp/travis-wait-enhanced.zip -y -o/usr/bin/ travis-wait-enhanced.exe -r
179 travis-wait-enhanced --version # we use this tool so travis doesn't timeout when there's no output for > 10 minutes (vcpkg has no verbose option). travis_wait also fixes this issue, but it swallows the output
180 if [ ! -f "vcpkg/vcpkg.exe" ]; then # if not in cache from a previous run
181 git clone https://github.com/Microsoft/vcpkg.git
182 ./vcpkg/bootstrap-vcpkg.bat
183 travis-wait-enhanced --interval=1m --timeout=30m -- ./vcpkg/vcpkg.exe install libflac libvorbis soxr opus boost-asio --triplet x64-windows
184 else
185 ./vcpkg/vcpkg.exe update # make sure dependencies are up to date
186 fi
187 fi
188 )
168189
169190 script:
170191 # make sure CXX is correctly set
172193
173194 - mkdir build
174195 - cd build
175 - cmake -DCMAKE_CXX_FLAGS="$CXXFLAGS -Werror -Wall -Wextra -pedantic -Wno-unused-parameter -Wno-unused-function -O2" .. && make && sudo make install
196 - |
197 (
198 if [ "$TRAVIS_OS_NAME" != 'windows' ]; then
199 cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="$CXXFLAGS -Werror -Wall -Wextra -pedantic -Wno-unused-function" .. && make && sudo make install
200 else
201 cmake -G "Visual Studio 15 2017 Win64" -DCMAKE_TOOLCHAIN_FILE=../vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows ..
202 cmake --build . --config Release
203 fi
204 )
205 cache:
206 directories:
207 - vcpkg
00 cmake_minimum_required(VERSION 3.2)
11
2 project(snapcast LANGUAGES CXX VERSION 0.19.0)
2 project(snapcast LANGUAGES CXX VERSION 0.22.0)
33 set(PROJECT_DESCRIPTION "Multiroom client-server audio player")
44 set(PROJECT_URL "https://github.com/badaix/snapcast")
55
77 option(BUILD_STATIC_LIBS "Build snapcast in a static context" ON)
88 option(BUILD_TESTS "Build tests (run tests with make test)" ON)
99
10 option(BUILD_SERVER "Build Snapserver" ON)
10 include(GNUInstallDirs)
11
12 if(NOT WIN32)
13 option(BUILD_SERVER "Build Snapserver" ON) # no Windows server for now
14 endif()
15
1116 option(BUILD_CLIENT "Build Snapclient" ON)
1217
1318 option(BUILD_WITH_FLAC "Build with FLAC support" ON)
8792 ENDIF(${BIGENDIAN})
8893
8994 # Check dependencies
90 find_package(PkgConfig REQUIRED)
95
96 if(NOT WIN32) # no PkgConfig on Windows...
97 find_package(PkgConfig REQUIRED)
98 endif()
99
91100 find_package(Threads REQUIRED)
92101
93102 include(CMakePushCheckState)
94103 include(CheckIncludeFileCXX)
95
96 if(MACOSX)
97 set(BONJOUR_FOUND true)
98 if (BONJOUR_FOUND)
99 add_definitions(-DHAS_BONJOUR)
100 endif (BONJOUR_FOUND)
101
102 add_definitions(-DFREEBSD -DHAS_DAEMON)
103 link_directories("/usr/local/lib")
104 list(APPEND INCLUDE_DIRS "/usr/local/include")
105 list(APPEND CMAKE_REQUIRED_INCLUDES "${INCLUDE_DIRS}")
106 elseif(ANDROID)
107 # add_definitions("-DNO_CPP11_STRING")
108 else()
109 if (BUILD_CLIENT)
104 include_directories(${INCLUDE_DIRS})
105
106 include(${CMAKE_SOURCE_DIR}/cmake/CheckCXX11StringSupport.cmake)
107
108 CHECK_CXX11_STRING_SUPPORT(HAS_CXX11_STRING_SUPPORT)
109 if(NOT HAS_CXX11_STRING_SUPPORT)
110 add_definitions("-DNO_CPP11_STRING")
111 endif()
112
113
114 if(NOT WIN32)
115
116 if(MACOSX)
117 set(BONJOUR_FOUND true)
118 if (BONJOUR_FOUND)
119 add_definitions(-DHAS_BONJOUR)
120 endif (BONJOUR_FOUND)
121
122 add_definitions(-DFREEBSD -DMACOS -DHAS_DAEMON)
123 link_directories("/usr/local/lib")
124 list(APPEND INCLUDE_DIRS "/usr/local/include")
125 elseif(ANDROID)
126 # add_definitions("-DNO_CPP11_STRING")
127 else()
128
110129 pkg_search_module(ALSA REQUIRED alsa)
111130 if (ALSA_FOUND)
112131 add_definitions(-DHAS_ALSA)
113132 endif (ALSA_FOUND)
114 endif()
115
116 if(BUILD_WITH_AVAHI)
117 pkg_search_module(AVAHI avahi-client)
118 if (AVAHI_FOUND)
119 add_definitions(-DHAS_AVAHI)
120 endif (AVAHI_FOUND)
121 endif(BUILD_WITH_AVAHI)
122
123 add_definitions(-DHAS_DAEMON)
124
125 if(FREEBSD)
126 add_definitions(-DFREEBSD)
127 link_directories("/usr/local/lib")
128 list(APPEND INCLUDE_DIRS "/usr/local/include")
129 list(APPEND CMAKE_REQUIRED_INCLUDES "${INCLUDE_DIRS}")
130 endif()
131 endif()
132
133 include_directories(${INCLUDE_DIRS})
134
135
136 include(${CMAKE_SOURCE_DIR}/cmake/CheckCXX11StringSupport.cmake)
137 CHECK_CXX11_STRING_SUPPORT(HAS_CXX11_STRING_SUPPORT)
138 if(NOT HAS_CXX11_STRING_SUPPORT)
139 add_definitions("-DNO_CPP11_STRING")
140 endif()
141
142 if(BUILD_WITH_FLAC)
143 pkg_search_module(FLAC flac)
144 if (FLAC_FOUND)
145 add_definitions("-DHAS_FLAC")
146 endif (FLAC_FOUND)
147 endif()
148
149 if(BUILD_WITH_VORBIS OR BUILD_WITH_TREMOR)
150 pkg_search_module(OGG ogg)
151 if (OGG_FOUND)
152 add_definitions("-DHAS_OGG")
153 endif (OGG_FOUND)
154 endif()
155
156 if(BUILD_WITH_VORBIS)
157 pkg_search_module(VORBIS vorbis)
158 if (VORBIS_FOUND)
159 add_definitions("-DHAS_VORBIS")
160 endif (VORBIS_FOUND)
161 endif()
162
163 if(BUILD_WITH_TREMOR)
164 pkg_search_module(TREMOR vorbisidec)
165 if (TREMOR_FOUND)
166 add_definitions("-DHAS_TREMOR")
167 endif (TREMOR_FOUND)
168 endif()
169
170 if(BUILD_WITH_VORBIS)
171 pkg_search_module(VORBISENC vorbisenc)
172 if (VORBISENC_FOUND)
173 add_definitions("-DHAS_VORBIS_ENC")
174 endif(VORBISENC_FOUND)
175 endif()
176
177 if(BUILD_WITH_OPUS)
178 pkg_search_module(OPUS opus)
179 if (OPUS_FOUND)
180 add_definitions("-DHAS_OPUS")
181 endif (OPUS_FOUND)
182 endif()
183
184 if(BUILD_WITH_EXPAT)
185 pkg_search_module(EXPAT expat)
186 if (EXPAT_FOUND)
187 add_definitions("-DHAS_EXPAT")
188 endif (EXPAT_FOUND)
133
134 if(BUILD_WITH_AVAHI)
135 pkg_search_module(AVAHI avahi-client)
136 if (AVAHI_FOUND)
137 add_definitions(-DHAS_AVAHI)
138 else()
139 message(STATUS "avahi-client not found")
140 endif (AVAHI_FOUND)
141 endif(BUILD_WITH_AVAHI)
142
143 add_definitions(-DHAS_DAEMON)
144
145 if(FREEBSD)
146 add_definitions(-DFREEBSD)
147 link_directories("/usr/local/lib")
148 list(APPEND INCLUDE_DIRS "/usr/local/include")
149 endif()
150 endif()
151
152 pkg_search_module(SOXR soxr)
153 if (SOXR_FOUND)
154 add_definitions("-DHAS_SOXR")
155 else()
156 message(STATUS "soxr not found")
157 endif (SOXR_FOUND)
158
159 if(BUILD_WITH_FLAC)
160 pkg_search_module(FLAC flac)
161 if (FLAC_FOUND)
162 add_definitions("-DHAS_FLAC")
163 else()
164 message(STATUS "flac not found")
165 endif (FLAC_FOUND)
166 endif()
167
168 if(BUILD_WITH_VORBIS OR BUILD_WITH_TREMOR)
169 pkg_search_module(OGG ogg)
170 if (OGG_FOUND)
171 add_definitions("-DHAS_OGG")
172 else()
173 message(STATUS "ogg not found")
174 endif (OGG_FOUND)
175 endif()
176
177 if(BUILD_WITH_VORBIS)
178 pkg_search_module(VORBIS vorbis)
179 if (VORBIS_FOUND)
180 add_definitions("-DHAS_VORBIS")
181 endif (VORBIS_FOUND)
182 endif()
183
184 if(BUILD_WITH_TREMOR)
185 pkg_search_module(TREMOR vorbisidec)
186 if (TREMOR_FOUND)
187 add_definitions("-DHAS_TREMOR")
188 endif (TREMOR_FOUND)
189 endif()
190
191 if ((BUILD_WITH_VORBIS OR BUILD_WITH_TREMOR) AND NOT VORBIS_FOUND AND NOT TREMOR_FOUND)
192 message(STATUS "tremor and vorbis not found")
193 endif()
194
195 if(BUILD_WITH_VORBIS)
196 pkg_search_module(VORBISENC vorbisenc)
197 if (VORBISENC_FOUND)
198 add_definitions("-DHAS_VORBIS_ENC")
199 else()
200 message(STATUS "vorbisenc not found")
201 endif(VORBISENC_FOUND)
202 endif()
203
204 if(BUILD_WITH_OPUS)
205 pkg_search_module(OPUS opus)
206 if (OPUS_FOUND)
207 add_definitions("-DHAS_OPUS")
208 else()
209 message(STATUS "opus not found")
210 endif (OPUS_FOUND)
211 endif()
212
213 if(BUILD_WITH_EXPAT)
214 pkg_search_module(EXPAT expat)
215 if (EXPAT_FOUND)
216 add_definitions("-DHAS_EXPAT")
217 else()
218 message(STATUS "expat not found")
219 endif (EXPAT_FOUND)
220 endif()
189221 endif()
190222
191223 find_package(Boost 1.70 REQUIRED)
192224 add_definitions("-DBOOST_ERROR_CODE_HEADER_ONLY")
193225
226 if(WIN32)
227 include(FindPackageHandleStandardArgs)
228 SET(CMAKE_FIND_LIBRARY_SUFFIXES .lib .a ${CMAKE_FIND_LIBRARY_SUFFIXES})
229
230 find_path(FLAC_INCLUDE_DIRS FLAC/all.h)
231 find_library(FLAC_LIBRARIES FLAC)
232 find_package_handle_standard_args(FLAC REQUIRED FLAC_INCLUDE_DIRS FLAC_LIBRARIES)
233
234 find_path(OGG_INCLUDE_DIRS ogg/ogg.h)
235 find_library(OGG_LIBRARIES ogg)
236 find_package_handle_standard_args(Ogg REQUIRED OGG_INCLUDE_DIRS OGG_LIBRARIES)
237
238 find_path(VORBIS_INCLUDE_DIRS vorbis/vorbisenc.h)
239 find_library(VORBIS_LIBRARIES vorbis)
240 find_package_handle_standard_args(Vorbis REQUIRED VORBIS_INCLUDE_DIRS VORBIS_LIBRARIES)
241
242 find_path(OPUS_INCLUDE_DIRS opus/opus.h)
243 find_library(OPUS_LIBRARIES opus)
244 find_package_handle_standard_args(Opus REQUIRED OPUS_INCLUDE_DIRS OPUS_LIBRARIES)
245
246 find_path(SOXR_INCLUDE_DIRS soxr.h)
247 find_library(SOXR_LIBRARIES soxr)
248 find_package_handle_standard_args(Soxr REQUIRED SOXR_INCLUDE_DIRS SOXR_LIBRARIES)
249
250 add_definitions(-DNTDDI_VERSION=0x06020000 -D_WIN32_WINNT=0x0602 -DWINVER=0x0602 -DWINDOWS -DWIN32_LEAN_AND_MEAN -DUNICODE -D_UNICODE -D_CRT_SECURE_NO_WARNINGS )
251 add_definitions(-DHAS_OGG -DHAS_VORBIS -DHAS_FLAC -DHAS_VORBIS_ENC -DHAS_OPUS -DHAS_WASAPI -DHAS_SOXR)
252 endif()
253
254 list(APPEND CMAKE_REQUIRED_INCLUDES "${INCLUDE_DIRS}")
255
256 #include(${CMAKE_SOURCE_DIR}/cmake/SystemdService.cmake)
257
194258 add_subdirectory(common)
195259
196260 if (BUILD_SERVER)
200264 if (BUILD_CLIENT)
201265 add_subdirectory(client)
202266 endif()
267
268
269 FIND_PROGRAM(CLANG_FORMAT "clang-format")
270 IF(CLANG_FORMAT)
271 FILE(GLOB_RECURSE
272 CHECK_CXX_SOURCE_FILES
273 common/*.[ch]pp
274 client/*.[ch]pp
275 server/*.[ch]pp
276 )
277
278 ADD_CUSTOM_TARGET(
279 reformat
280 COMMAND
281 ${CLANG_FORMAT}
282 -i
283 -style=file
284 ${CHECK_CXX_SOURCE_FILES}
285 COMMENT "Auto formatting of all source files"
286 )
287 ENDIF()
0 # Contributing
1
2 ## Engaging in Our Project
3
4 We use GitHub to manage reviews of pull requests.
5
6 * If you are a new contributor, see: [Steps to Contribute](#steps-to-contribute)
7
8 * Before implementing your change, create an issue that describes the problem you would like to solve or the code that should be enhanced. Please note that you are willing to work on that issue.
9
10 * The team will review the issue and decide whether it should be implemented as a Pull Request. In that case, they will assign the issue to you. If the team decides against picking up the issue, it will be closed with a proper explanation.
11
12 ## Steps to Contribute
13
14 Should you wish to work on an issue, please claim it first by commenting on the GitHub issue that you want to work on. This is to prevent duplicated efforts from other contributors on the same issue.
15
16 Only start working on the Pull Request after the team assigned the issue to you to avoid unnecessary efforts.
17
18 If you have questions about one of the issues, please comment on them, and one of the maintainers will clarify.
19
20 We kindly ask you to follow the [Pull Request Checklist](#Pull-Request-Checklist) to ensure reviews can happen accordingly.
21
22 ## Contributing Code
23
24 You are welcome to contribute code in order to fix a bug or to implement a new feature that is logged as an issue.
25
26 Only start working on the Pull Request after the team assigned the issue to you to avoid unnecessary efforts.
27
28 The following rule governs code contributions:
29
30 * Contributions must be licensed under the [GPL-3.0 License](LICENSE)
31
32 * This project loosely follows the [Google C++ Style Guide](https://google.github.io/styleguide/cppguide.html)
33
34 * For better compatibility with embedded toolchains, the used C++ standard should be limited to C++14
35
36 * Code should be formatted by running `make reformat`
37
38 ## Contributing Documentation
39
40 You are welcome to contribute documentation to the project.
41
42 The following rule governs documentation contributions:
43
44 * Contributions must be licensed under the same license as code, the [GPL-3.0 License](LICENSE)
45
46 ## Pull Request Checklist
47
48 * Branch from the `develop` branch and ensure it is up to date with the current `develop` branch before submitting your pull request. If it doesn't merge cleanly with `develop`, you may be asked to resolve the conflicts. Pull requests to master will be closed.
49
50 * Commits should be as small as possible while ensuring that each commit is correct independently (i.e., each commit should compile and pass tests).
51
52 * Pull requests must not contain compiled sources (already set by the default .gitignore) or binary files
53
54 * Test your changes as thoroughly as possible before you commit them. Preferably, automate your test by unit/integration tests. If tested manually, provide information about the test scope in the PR description (e.g. “Test passed: Upgrade version from 0.42 to 0.42.23.”).
55
56 * Create _Work In Progress [WIP]_ pull requests only if you need clarification or an explicit review before you can continue your work item.
57
58 * If your patch is not getting reviewed or you need a specific person to review it, you can @-reply a reviewer asking for a review in the pull request or a comment, or you can ask for a review by contacting us via [email](mailto:snapcast@badaix.de).
59
60 * Post review:
61 * If a review requires you to change your commit(s), please test the changes again.
62 * Amend the affected commit(s) and force push onto your branch.
63 * Set respective comments in your GitHub review to resolved.
64 * Create a general PR comment to notify the reviewers that your amendments are ready for another round of review.
65
66 ## Issues and Planning
67
68 * We use GitHub issues to track bugs and enhancement requests.
69
70 * Please provide as much context as possible when you open an issue. The information you provide must be comprehensive enough to reproduce that issue for the assignee.
71
72 * Attach a log file (preferably inline as code block) if neccessary. Use `debug` log level (`snapclient --logfilter debug`, `snapserver --logging.filter debug`).
73
74 * Please apply one or more applicable [labels](https://github.com/badaix/snapcast/labels) to your issue so that all community members are able to cluster the issues better.
0 Snapcast
1 ========
0 # Snapcast
21
32 ![Snapcast](https://raw.githubusercontent.com/badaix/snapcast/master/doc/Snapcast_800.png)
43
98 [![Github Releases](https://img.shields.io/github/release/badaix/snapcast.svg)](https://github.com/badaix/snapcast/releases)
109 [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.me/badaix)
1110
12 Snapcast is a multiroom 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 multiroom solution.
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.
14
15 How does it work
16 ----------------
17 The Snapserver reads PCM chunks from the pipe `/tmp/snapfifo`. The chunk is encoded and tagged with the local time. Supported codecs are:
18 * **PCM** lossless uncompressed
19 * **FLAC** lossless compressed [default]
20 * **Vorbis** lossy compression
21 * **Opus** lossy low-latency compression
22
23 The encoded chunk is sent via a TCP connection to the Snapclients.
11 Snapcast is a multiroom 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 multiroom solution.
12 Audio is captured by the server and routed to the connected clients. Several players can feed audio to the server in parallel and clients can be grouped to play the same audio stream.
13 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/).
14
15 ![Overview](https://raw.githubusercontent.com/badaix/snapcast/master/doc/Overview.png)
16
17 ## How does it work
18
19 The Snapserver reads PCM chunks from configurable stream sources:
20
21 - **Named pipe**, e.g. `/tmp/snapfifo`
22 - **ALSA** to capture line-in, microphone, alsa-loop (to capture audio from other players)
23 - **TCP**
24 - **stdout** of a process
25 - Many more
26
27 The chunks are encoded and tagged with the local time. Supported codecs are:
28
29 - **PCM** lossless uncompressed
30 - **FLAC** lossless compressed [default]
31 - **Vorbis** lossy compression
32 - **Opus** lossy low-latency compression
33
34 The encoded chunks are sent via a TCP connection to the Snapclients.
2435 Each client does continuous time synchronization with the server, so that the client is always aware of the local server time.
2536 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 a system dependend low level audio API (e.g. ALSA) at the appropriate time. Time deviations are corrected by playing faster/slower, which is done by removing/duplicating single samples (a sample at 48kHz has a duration of ~0.02ms).
2637
2839
2940 For more information on the binary protocol, please see the [documentation](doc/binary_protocol.md).
3041
31 Installation
32 ------------
42 ## Installation
43
3344 You can either install Snapcast from a prebuilt package (recommended for new users), or build and install snapcast from source.
3445
3546 ### Install Linux packages (recommended for beginners)
36 Snapcast packages are available for several Linux distributions.
37
38 #### Debian
39 For Debian (and Debian-based systems, such as Ubuntu, Linux Mint, ElementaryOS) download the package for your CPU architecture from the [latest release page](https://github.com/badaix/snapcast/releases/latest).
40
41 e.g. for Raspberry Pi `snapclient_0.x.x_armhf.deb`, for laptops `snapclient_0.x.x_amd64.deb`
42
43 Install the package:
44
45 $ sudo dpkg -i snapclient_0.x.x_armhf.deb
46 or
47 $ sudo dpkg -i snapclient_0.x.x_amd64.deb
48
49 Install missing dependencies:
50
51 $ sudo apt-get -f install
52
53 #### OpenWrt
54 On OpenWrt do:
55
56 $ opkg install snapclient_0.x.x_ar71xx.ipk
57
58 #### Alpine Linux
59 On Alpine Linux do:
60
61 $ apk add snapcast
62
63 Or, for just the client:
64
65 $ apk add snapcast-client
66
67 Or, for just the server:
68
69 $ apk add snapcast-server
70
71 #### Gentoo Linux
72 On Gentoo Linux do:
73
74 $ emerge --ask media-sound/snapcast
75
76 #### Archlinux
77 On Archlinux, Snapcast is available through the AUR. To install, use your favorite AUR helper, or do:
78
79 $ git clone https://aur.archlinux.org/snapcast
80 $ cd snapcast
81 $ makepkg -si
82
83 #### Void Linux
84 To install the client:
85
86 # xbps-install snapclient
87
88 To install the server:
89
90 # xbps-install snapserver
91
47
48 Snapcast packages are available for several Linux distributions:
49
50 - [Debian](doc/install.md#debian)
51 - [OpenWrt](doc/install.md#openwrt)
52 - [Alpine Linux](doc/install.md#alpine-linux)
53 - [Archlinux](doc/install.md#archlinux)
54 - [Void Linux](doc/install.md#void-linux)
55
56 ### Nightly builds
57
58 There are debian packages of automated builds for [armhf](https://github.com/badaix/snapcast/actions?query=workflow%3Aself-hosted) and [amd64](https://github.com/badaix/snapcast/actions?query=workflow%3AUbuntu) available in [Actions](https://github.com/badaix/snapcast/actions).
59 Download and extract the archive for your architecture and follow the [debian installation instructions](doc/install.md#debian).
60
9261 ### Installation from source
9362
9463 Please follow this [guide](doc/build.md) to build Snapcast for
9564
96 * [Linux](doc/build.md#linux-native)
97
98 * [FreeBSD](doc/build.md#freebsd-native)
99
100 * [macOS](doc/build.md#macos-native)
101
102 * [Android](doc/build.md#android-cross-compile)
103
104 * [OpenWrt](doc/build.md#openwrtlede-cross-compile)
105
106 * [Buildroot](doc/build.md#buildroot-cross-compile)
107
108 * [Raspberry Pi](doc/build.md#raspberry-pi-cross-compile)
109
110 SnapOS
111 ------
65 - [Linux](doc/build.md#linux-native)
66 - [FreeBSD](doc/build.md#freebsd-native)
67 - [macOS](doc/build.md#macos-native)
68 - [Android](doc/build.md#android-cross-compile)
69 - [OpenWrt](doc/build.md#openwrtlede-cross-compile)
70 - [Buildroot](doc/build.md#buildroot-cross-compile)
71 - [Raspberry Pi](doc/build.md#raspberry-pi-cross-compile)
72 - [Windows](doc/build.md#windows-vcpkg)
73
74 ## SnapOS
75
11276 The bravest among you may be interested in [SnapOS](https://github.com/badaix/snapos), a small and fast-booting "just enough" OS to run Snapcast as an appliance.
11377
11478 There is a guide (with the necessary buildfiles) available to build SnapOS, which comes in two flavors:
79
11580 - [Buildroot](https://github.com/badaix/snapos/blob/master/buildroot-external/README.md) based, or
11681 - [OpenWrt](https://github.com/badaix/snapos/tree/master/openwrt) based.
11782
11883 Please note that there are no pre-built firmware packages available.
11984
120 Configuration
121 -------------
85 ## Configuration
86
12287 After installation, Snapserver and Snapclient are started with the command line arguments that are configured in `/etc/default/snapserver` and `/etc/default/snapclient`.
12388 Allowed options are listed in the man pages (`man snapserver`, `man snapclient`) or by invoking the snapserver or snapclient with the `-h` option.
12489
125 The server configuration is done in `/etc/snapserver.conf`. Different streams can by configured in the `[stream]` section with a list of `stream` options, e.g.:
126
127 ```
128 [stream]
129 stream = pipe:///tmp/snapfifo?name=Radio&sampleformat=48000:16:2&codec=flac
130 stream = file:///home/user/Musik/Some%20wave%20file.wav?name=File
131 ```
132
133 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`:
134
135 stream = pipe:///tmp/snapfifo?name=Radio&mode=read"
136
137 Test
138 ----
90 The server configuration is done in `/etc/snapserver.conf`. Different audio sources can by configured in the `[stream]` section with a list of `source` options, e.g.:
91
92 [stream]
93 source = pipe:///tmp/snapfifo?name=Radio&sampleformat=48000:16:2&codec=flac
94 source = file:///home/user/Musik/Some%20wave%20file.wav?name=File
95
96 Available stream sources are:
97
98 - [pipe](doc/configuration.md#pipe): read audio from a named pipe
99 - [alsa](doc/configuration.md#alsa): read audio from an alsa device
100 - [librespot](doc/configuration.md#librespot): launches librespot and reads audio from stdout
101 - [airplay](doc/configuration.md#airplay): launches airplay and read audio from stdout
102 - [file](doc/configuration.md#file): read PCM audio from a file
103 - [process](doc/configuration.md#process): launches a process and reads audio from stdout
104 - [tcp](doc/configuration.md#tcp-server): receives audio from a TCP socket, can act as client or server
105 - [meta](doc/configuration.md#meta): read and mix audio from other stream sources
106
107 The client will use as audio backend the system's low level audio API to have the best possible control and most precise timing to achieve perfectly synced playback. On Linux `alsa` is used, on Android `oboe` or `opensl`, on macOS `coreaudio` and on Windows `wasapi`.
108 There is also a `file` backend available that will write the raw PCM data to a file (or stdout, stderr). The backend can be configured using the `--player` command line parameter.
109
110 ## Test
111
139112 You can test your installation by copying random data into the server's fifo file
140113
141 $ sudo cat /dev/urandom > /tmp/snapfifo
114 sudo cat /dev/urandom > /tmp/snapfifo
142115
143116 All connected clients should play random noise now. You might raise the client's volume with "alsamixer".
144117 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:
145118
146 ```
147 [stream]
148 stream = file:///home/user/Musik/Some%20wave%20file.wav?name=test
149 ```
119 [stream]
120 stream = file:///home/user/Musik/Some%20wave%20file.wav?name=test
150121
151122 When you are using a Raspberry Pi, you might have to change your audio output to the 3.5mm jack:
152123
153124 #The last number is the audio output with 1 being the 3.5 jack, 2 being HDMI and 0 being auto.
154 $ amixer cset numid=3 1
155
156 To setup WiFi on a Raspberry Pi, you can follow this guide:
157 https://www.raspberrypi.org/documentation/configuration/wireless/wireless-cli.md
158
159 Control
160 -------
161 Snapcast can be controlled using a [JSON-RPC API](doc/json_rpc_api/v2_0_0.md):
162 * Set client's volume
163 * Mute clients
164 * Rename clients
165 * Assign a client to a stream
166 * ...
125 amixer cset numid=3 1
126
127 To setup WiFi on a Raspberry Pi, you can follow this [guide](https://www.raspberrypi.org/documentation/configuration/wireless/wireless-cli.md)
128
129 ## Control
130
131 Snapcast can be controlled using a [JSON-RPC API](doc/json_rpc_api/v2_0_0.md) over plain TCP, HTTP, or Websockets:
132
133 - Set client's volume
134 - Mute clients
135 - Rename clients
136 - Assign a client to a stream
137 - Manage groups
138 - ...
139
140 ### WebApp
141
142 The server is shipped with [Snapweb](https://github.com/badaix/snapweb), this WebApp can be reached under `http://<snapserver host>:1780`.
143
144 ![Snapweb](https://raw.githubusercontent.com/badaix/snapweb/master/snapweb.png)
145
146 ### Android client
167147
168148 There is an Android client [snapdroid](https://github.com/badaix/snapdroid) available in [Releases](https://github.com/badaix/snapdroid/releases/latest) and on [Google Play](https://play.google.com/store/apps/details?id=de.badaix.snapcast)
169149
170150 ![Snapcast for Android](https://raw.githubusercontent.com/badaix/snapcast/master/doc/snapcast_android_scaled.png)
151
152 ### Contributions
171153
172154 There is also an unofficial WebApp from @atoomic [atoomic/snapcast-volume-ui](https://github.com/atoomic/snapcast-volume-ui).
173155 This app lists all clients connected to a server and allows you to control individually the volume of each client.
183165
184166 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.
185167
186 Setup of audio players/server
187 -----------------------------
168 For Windows, there's [Snap.Net](https://github.com/stijnvdb88/snap.net), a control client and player. It runs in the tray and lets you adjust client volumes with just a few clicks. The player simplifies setting up snapclient to play your music through multiple Windows sound devices simultaneously: pc speakers, hdmi audio, any usb audio devices you may have, etc. Snap.Net also runs on Android, and has limited support for iOS.
169
170 ## Setup of audio players/server
171
188172 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.
189173 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).
190174
193177 audio player software -> snapfifo -> snapserver -> network -> snapclient -> alsa
194178
195179 This [guide](doc/player_setup.md) shows how to configure different players/audio sources to redirect their audio signal into the Snapserver's fifo:
196 * [MPD](doc/player_setup.md#mpd)
197 * [Mopidy](doc/player_setup.md#mopidy)
198 * [FFmpeg](doc/player_setup.md#ffmpeg)
199 * [mpv](doc/player_setup.md#mpv)
200 * [MPlayer](doc/player_setup.md#mplayer)
201 * [Alsa](doc/player_setup.md#alsa)
202 * [PulseAudio](doc/player_setup.md#pulseaudio)
203 * [AirPlay](doc/player_setup.md#airplay)
204 * [Spotify](doc/player_setup.md#spotify)
205 * [Process](doc/player_setup.md#process)
206 * [Line-in](doc/player_setup.md#line-in)
207
208 Roadmap
209 -------
180
181 - [MPD](doc/player_setup.md#mpd)
182 - [Mopidy](doc/player_setup.md#mopidy)
183 - [FFmpeg](doc/player_setup.md#ffmpeg)
184 - [mpv](doc/player_setup.md#mpv)
185 - [MPlayer](doc/player_setup.md#mplayer)
186 - [Alsa](doc/player_setup.md#alsa)
187 - [PulseAudio](doc/player_setup.md#pulseaudio)
188 - [AirPlay](doc/player_setup.md#airplay)
189 - [Spotify](doc/player_setup.md#spotify)
190 - [Process](doc/player_setup.md#process)
191 - [Line-in](doc/player_setup.md#line-in)
192 - [VLC](doc/player_setup.md#vlc)
193
194 ## Roadmap
195
210196 Unordered list of features that should make it into the v1.0
197
211198 - [X] **Remote control** JSON-RPC API to change client latency, volume, zone,...
212199 - [X] **Android client** JSON-RPC client and Snapclient
213200 - [X] **Streams** Support multiple streams
214201 - [X] **Debian packages** prebuild deb packages
215202 - [X] **Endian** independent code
216203 - [X] **OpenWrt** port Snapclient to OpenWrt
217 - [X] **Hi-Res audio** support (like 192kHz 24bit)
204 - [X] **Hi-Res audio** support (like 96kHz 24bit)
218205 - [X] **Groups** support multiple Groups of clients ("Zones")
206 - [X] **Ports** Snapclient for Windows, Mac OS X,...
219207 - [ ] **JSON-RPC** Possibility to add, remove, rename streams
220208 - [ ] **Protocol specification** Snapcast binary streaming protocol, JSON-RPC protocol
221 - [ ] **Ports** Snapclient for Windows, ~~Mac OS X~~,...
00 # Snapcast changelog
1
2 ## Version 0.22.0
3
4 ### Features
5
6 - Server: Add Meta stream source (Issue #402, #569, #666)
7 - Client: Add file audio backend (Issue #681)
8
9 ### Bugfixes
10
11 - Add missing define for alsa stream to makefile (Issue #692)
12 - Fix playback when plugging the headset on Android (Issue #699)
13 - Server discards old chunks if not consumed (Issue #708)
14
15 ### General
16
17 - Less verbose logging during pipe reconnects (Issue #696)
18 - Add null encoder for streams used only as input for meta streams
19 - Snapweb: Change latency range to [-10s, 10s] (Issue #695)
20 - Update Snapweb, including PR #11, #12, #13, Issues #16, #17
21
22 _Johannes Pohl <snapcast@badaix.de> Thu, 15 Oct 2020 00:13:37 +0200_
23
24 ## Version 0.21.0
25
26 ### Features
27
28 - Server: Support for WebSocket streaming clients
29 - Server: Install Snapweb web client (Issue #579)
30 - Server: Resample input to 48000:16:2 when using opus codec
31 - Server: Add Alsa stream source
32
33 ### Bugfixes
34
35 - make install will setup the snapserver home dir (Issue #643)
36 - Client retries to open a blocked alsa device (Issue #652)
37
38 ### General
39
40 - debian packag generation switched from make to CMake buildsystem
41 - Reintroduce MACOS define, hopefully not breaking anything on macOS
42 - Snapcast uses GitHub actions for automated CI/CD
43 - CMake installs man files (Issue #507)
44 - Update documentation (Issue #615, #617)
45
46 _Johannes Pohl <snapcast@badaix.de> Sun, 13 Sep 2020 00:13:37 +0200_
47
48 ## Version 0.20.0
49
50 ### Features
51
52 - Client: Windows support (Issue #24)
53 - Client: add hardware mixer (Issue #318)
54 - Client: add "script" and "none" mixer (Issue #302)
55 - Client: add sharingmode for audio device (if supported)
56 - Logging: configurable sink and filters (Issue #30, #561, #122, #559)
57 - Librespot: add option "disable-audio-cache=[false|true]"
58
59 ### Bugfixes
60
61 - Fix build failure on FreeBSD (Issue #565)
62 - Fix calling lsb_release multiple times (Issue #470)
63 - Client: high CPU load and crash during playback (Issue #609, #628)
64 - Client: improved handling of USB audio disconnects (Issue #64)
65 - Client: latency is forgotten (Issue #476, #588, Snapdroid #11)
66 - Client: fix segfault on mac when playback is paused (Issue #560)
67 - Client: fix bonjour on mac reports empty IP (Issue #632)
68 - Client: fix buzzing tone on Android (Issue #23, #24)
69 - Server: fix crash if client disconnects during connect (Issue #639)
70 - Server: fix reading metadata from shairport-sync (Issue #624)
71 - Server: fix crash on FreeBSD if settings.json is empty (Issue #620)
72 - Server: fix warning about unknown command line options (Issue #635)
73 - Readme: openWrt documentation (Issue #633)
74 - Fix setting the daemon's process priority (PR #448)
75
76 ### General
77
78 - Client: use less threads and thus less ressources
79 - Update links to xiph externals (Issue #637, PR #616)
80
81 _Johannes Pohl <snapcast@badaix.de> Sat, 13 Jun 2020 00:13:37 +0200_
182
283 ## Version 0.19.0
384
44 stream.cpp
55 time_provider.cpp
66 decoder/pcm_decoder.cpp
7 player/player.cpp)
7 player/player.cpp
8 player/file_player.cpp)
89
910 set(CLIENT_LIBRARIES ${CMAKE_THREAD_LIBS_INIT} ${ATOMIC_LIBRARY} common)
1011
1112 set(CLIENT_INCLUDE
1213 ${Boost_INCLUDE_DIR}
1314 ${CMAKE_SOURCE_DIR}/client
14 ${CMAKE_SOURCE_DIR}/common
15 ${ASIO_INCLUDE_DIRS}
16 ${POPL_INCLUDE_DIRS})
15 ${CMAKE_SOURCE_DIR}/common)
1716
1817
1918 if(MACOSX)
2726 list(APPEND CLIENT_SOURCES player/coreaudio_player.cpp)
2827 find_library(COREAUDIO_LIB CoreAudio)
2928 find_library(COREFOUNDATION_LIB CoreFoundation)
29 find_library(IOKIT_LIB IOKit)
3030 find_library(AUDIOTOOLBOX_LIB AudioToolbox)
31 list(APPEND CLIENT_LIBRARIES ${COREAUDIO_LIB} ${COREFOUNDATION_LIB} ${AUDIOTOOLBOX_LIB})
31 list(APPEND CLIENT_LIBRARIES ${COREAUDIO_LIB} ${COREFOUNDATION_LIB} ${IOKIT_LIB} ${AUDIOTOOLBOX_LIB})
32 elseif (WIN32)
33 list(APPEND CLIENT_SOURCES player/wasapi_player.cpp)
34 list(APPEND CLIENT_LIBRARIES wsock32 ws2_32 avrt ksuser iphlpapi)
3235 else()
3336 # Avahi
3437 if (AVAHI_FOUND)
4447 list(APPEND CLIENT_INCLUDE ${ALSA_INCLUDE_DIRS})
4548 endif (ALSA_FOUND)
4649 endif (MACOSX)
47
48 #pkg_search_module(SOXR soxr)
49 find_package(soxr)
50 if (SOXR_FOUND)
51 add_definitions("-DHAS_SOXR")
52 list(APPEND CLIENT_LIBRARIES ${SOXR_LIBRARIES})
53 list(APPEND CLIENT_INCLUDE ${SOXR_INCLUDE_DIRS})
54 endif (SOXR_FOUND)
5550
5651 # if OGG then tremor or vorbis
5752 if (OGG_FOUND)
8681 target_link_libraries(snapclient ${CLIENT_LIBRARIES})
8782
8883 install(TARGETS snapclient COMPONENT client DESTINATION "${CMAKE_INSTALL_BINDIR}")
84 install(FILES snapclient.1 COMPONENT client DESTINATION "${CMAKE_INSTALL_MANDIR}/man1")
85
1313 # You should have received a copy of the GNU General Public License
1414 # along with this program. If not, see <http://www.gnu.org/licenses/>.
1515
16 VERSION = 0.19.0
16 VERSION = 0.22.0
1717 BIN = snapclient
1818
1919 ifeq ($(TARGET), FREEBSD)
4141 LDFLAGS += -fsanitize=$(SANITIZE)
4242 endif
4343
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
44 CXXFLAGS += $(ADD_CFLAGS) -std=c++14 -Wall -Wextra -Wpedantic -Wno-unused-function -DBOOST_ERROR_CODE_HEADER_ONLY -DHAS_FLAC -DHAS_OGG -DHAS_OPUS -DHAS_SOXR -DVERSION=\"$(VERSION)\" -I. -I.. -I../common
4545 LDFLAGS += $(ADD_LDFLAGS) -logg -lFLAC -lopus -lsoxr
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
46 OBJ = snapclient.o stream.o client_connection.o time_provider.o player/player.o player/file_player.o decoder/pcm_decoder.o decoder/ogg_decoder.o decoder/flac_decoder.o decoder/opus_decoder.o controller.o ../common/sample_format.o ../common/resampler.o
4747
4848
4949 ifneq (,$(TARGET))
5757 ifeq ($(TARGET), ANDROID)
5858
5959 CXX = $(PROGRAM_PREFIX)clang++
60 CXXFLAGS += -pthread -fPIC -DHAS_TREMOR -DHAS_OPENSL -DHAS_OBOE -I$(NDK_DIR)/usr/local/include
61 LDFLAGS = -L$(NDK_DIR)/usr/local/lib -pie -lvorbisidec -logg -lopus -lFLAC -lOpenSLES -loboe -latomic -llog -lsoxr -static-libstdc++
60 CXXFLAGS += -pthread -fPIC -DHAS_TREMOR -DHAS_OPENSL -DHAS_OBOE -I$(TOOLCHAIN)/sysroot/usr/include -I$(TOOLCHAIN)/sysroot/$(NDK_TARGET)/usr/local/include
61 LDFLAGS = -L$(TOOLCHAIN)/sysroot/$(NDK_TARGET)/usr/local/lib -pie -lvorbisidec -logg -lFLAC -lopus -lsoxr -lOpenSLES -loboe -latomic -llog -static-libstdc++
6262 OBJ += player/opensl_player.o player/oboe_player.o
6363
6464 else ifeq ($(TARGET), OPENWRT)
6565
66 CXXFLAGS += -pthread -DNO_CPP11_STRING -DHAS_TREMOR -DHAS_ALSA -DHAS_AVAHI -DHAS_DAEMON -DHAS_SOXR
66 CXXFLAGS += -pthread -DNO_CPP11_STRING -DHAS_TREMOR -DHAS_ALSA -DHAS_AVAHI -DHAS_DAEMON
6767 LDFLAGS += -lasound -lvorbisidec -lavahi-client -lavahi-common -latomic
6868 OBJ += ../common/daemon.o player/alsa_player.o browseZeroConf/browse_avahi.o
6969
7070 else ifeq ($(TARGET), BUILDROOT)
7171
72 CXXFLAGS += -pthread -DNO_CPP11_STRING -DHAS_TREMOR -DHAS_ALSA -DHAS_AVAHI -DHAS_DAEMON -DHAS_SOXR
72 CXXFLAGS += -pthread -DNO_CPP11_STRING -DHAS_TREMOR -DHAS_ALSA -DHAS_AVAHI -DHAS_DAEMON
7373 LDFLAGS += -lasound -lvorbisidec -lavahi-client -lavahi-common -latomic
7474 OBJ += ../common/daemon.o player/alsa_player.o browseZeroConf/browse_avahi.o
7575
7676 else ifeq ($(TARGET), MACOS)
7777
7878 CXX = g++
79 CXXFLAGS += -DHAS_COREAUDIO -DHAS_VORBIS -DFREEBSD -DHAS_BONJOUR -DHAS_DAEMON -DHAS_SOXR -I/usr/local/include -Wno-unused-local-typedef -Wno-deprecated
79 CXXFLAGS += -DHAS_COREAUDIO -DHAS_VORBIS -DFREEBSD -DMACOS -DHAS_BONJOUR -DHAS_DAEMON -I/usr/local/include -Wno-unused-local-typedef -Wno-deprecated
8080 LDFLAGS += -lvorbis -lFLAC -L/usr/local/lib -framework AudioToolbox -framework CoreAudio -framework CoreFoundation -framework IOKit
8181 OBJ += ../common/daemon.o player/coreaudio_player.o browseZeroConf/browse_bonjour.o
8282
8383 else
8484
8585 CXX = g++
86 CXXFLAGS += -pthread -DHAS_VORBIS -DHAS_ALSA -DHAS_AVAHI -DHAS_DAEMON -DHAS_SOXR
86 CXXFLAGS += -pthread -DHAS_VORBIS -DHAS_ALSA -DHAS_AVAHI -DHAS_DAEMON
8787 LDFLAGS += -lrt -lasound -lvorbis -lavahi-client -lavahi-common -latomic
8888 OBJ += ../common/daemon.o player/alsa_player.o browseZeroConf/browse_avahi.o
8989
9494
9595 check-env:
9696 ifeq ($(TARGET), ANDROID)
97 $(eval TOOLCHAIN:=$(NDK_DIR)/toolchains/llvm/prebuilt/linux-x86_64)
9798 ifndef NDK_DIR
9899 $(error android NDK_DIR is not set)
99100 endif
100101 ifndef ARCH
101 $(error ARCH is not set (arm, mips, x86))
102 $(error ARCH is not set (arm, aarch64, x86))
102103 endif
103104 ifeq ($(ARCH), x86)
104 $(eval PROGRAM_PREFIX:=$(NDK_DIR)/bin/i686-linux-android-)
105 else ifeq ($(ARCH), mips)
106 $(eval CXXFLAGS:=$(CXXFLAGS) -DIS_BIG_ENDIAN)
107 $(eval PROGRAM_PREFIX:=$(NDK_DIR)/bin/mipsel-linux-android-)
105 $(eval NDK_TARGET:=x86_64-linux-android)
106 $(eval API:=21)
108107 else ifeq ($(ARCH), arm)
108 $(eval NDK_TARGET:=armv7a-linux-androideabi)
109 $(eval API:=16)
109110 $(eval CXXFLAGS:=$(CXXFLAGS) -march=armv7)
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-)
113 endif
111 else ifeq ($(ARCH), aarch64)
112 $(eval NDK_TARGET:=aarch64-linux-android)
113 $(eval API:=21)
114 endif
115 $(eval PROGRAM_PREFIX:=$(TOOLCHAIN)/bin/$(NDK_TARGET)$(API)-)
114116 endif
115117
116118 $(BIN): $(OBJ)
2828
2929 static AvahiSimplePoll* simple_poll = nullptr;
3030
31 static constexpr auto LOG_TAG = "Avahi";
32
3133
3234 BrowseAvahi::BrowseAvahi() : client_(nullptr), sb_(nullptr)
3335 {
6971 switch (event)
7072 {
7173 case AVAHI_RESOLVER_FAILURE:
72 LOG(ERROR) << "(Resolver) Failed to resolve service '" << name << "' of type '" << type << "' in domain '" << domain
73 << "': " << avahi_strerror(avahi_client_errno(avahi_service_resolver_get_client(r))) << "\n";
74 LOG(ERROR, LOG_TAG) << "(Resolver) Failed to resolve service '" << name << "' of type '" << type << "' in domain '" << domain
75 << "': " << avahi_strerror(avahi_client_errno(avahi_service_resolver_get_client(r))) << "\n";
7476 break;
7577
7678 case AVAHI_RESOLVER_FOUND:
7779 {
7880 char a[AVAHI_ADDRESS_STR_MAX], *t;
7981
80 LOG(INFO) << "Service '" << name << "' of type '" << type << "' in domain '" << domain << "':\n";
82 LOG(INFO, LOG_TAG) << "Service '" << name << "' of type '" << type << "' in domain '" << domain << "':\n";
8183
8284 avahi_address_snprint(a, sizeof(a), address);
8385 browseAvahi->result_.host = host_name;
8991 browseAvahi->result_.iface_idx = interface;
9092
9193 t = avahi_string_list_to_string(txt);
92 LOG(INFO) << "\t" << host_name << ":" << port << " (" << a << ")\n";
93 LOG(DEBUG) << "\tTXT=" << t << "\n";
94 LOG(DEBUG) << "\tProto=" << (int)protocol << "\n";
95 LOG(DEBUG) << "\tcookie is " << avahi_string_list_get_service_cookie(txt) << "\n";
96 LOG(DEBUG) << "\tis_local: " << !!(flags & AVAHI_LOOKUP_RESULT_LOCAL) << "\n";
97 LOG(DEBUG) << "\tour_own: " << !!(flags & AVAHI_LOOKUP_RESULT_OUR_OWN) << "\n";
98 LOG(DEBUG) << "\twide_area: " << !!(flags & AVAHI_LOOKUP_RESULT_WIDE_AREA) << "\n";
99 LOG(DEBUG) << "\tmulticast: " << !!(flags & AVAHI_LOOKUP_RESULT_MULTICAST) << "\n";
100 LOG(DEBUG) << "\tcached: " << !!(flags & AVAHI_LOOKUP_RESULT_CACHED) << "\n";
94 LOG(INFO, LOG_TAG) << "\t" << host_name << ":" << port << " (" << a << ")\n";
95 LOG(DEBUG, LOG_TAG) << "\tTXT=" << t << "\n";
96 LOG(DEBUG, LOG_TAG) << "\tProto=" << (int)protocol << "\n";
97 LOG(DEBUG, LOG_TAG) << "\tcookie is " << avahi_string_list_get_service_cookie(txt) << "\n";
98 LOG(DEBUG, LOG_TAG) << "\tis_local: " << !!(flags & AVAHI_LOOKUP_RESULT_LOCAL) << "\n";
99 LOG(DEBUG, LOG_TAG) << "\tour_own: " << !!(flags & AVAHI_LOOKUP_RESULT_OUR_OWN) << "\n";
100 LOG(DEBUG, LOG_TAG) << "\twide_area: " << !!(flags & AVAHI_LOOKUP_RESULT_WIDE_AREA) << "\n";
101 LOG(DEBUG, LOG_TAG) << "\tmulticast: " << !!(flags & AVAHI_LOOKUP_RESULT_MULTICAST) << "\n";
102 LOG(DEBUG, LOG_TAG) << "\tcached: " << !!(flags & AVAHI_LOOKUP_RESULT_CACHED) << "\n";
101103 avahi_free(t);
102104 }
103105 }
119121 switch (event)
120122 {
121123 case AVAHI_BROWSER_FAILURE:
122 LOG(ERROR) << "(Browser) " << avahi_strerror(avahi_client_errno(avahi_service_browser_get_client(b))) << "\n";
124 LOG(ERROR, LOG_TAG) << "(Browser) " << avahi_strerror(avahi_client_errno(avahi_service_browser_get_client(b))) << "\n";
123125 avahi_simple_poll_quit(simple_poll);
124126 return;
125127
126128 case AVAHI_BROWSER_NEW:
127 LOG(INFO) << "(Browser) NEW: service '" << name << "' of type '" << type << "' in domain '" << domain << "'\n";
129 LOG(INFO, LOG_TAG) << "(Browser) NEW: service '" << name << "' of type '" << type << "' in domain '" << domain << "'\n";
128130
129131 /* We ignore the returned resolver object. In the callback
130132 function we free it. If the server is terminated before
133135
134136 if (!(avahi_service_resolver_new(browseAvahi->client_, interface, protocol, name, type, domain, AVAHI_PROTO_UNSPEC, (AvahiLookupFlags)0,
135137 resolve_callback, userdata)))
136 LOG(ERROR) << "Failed to resolve service '" << name << "': " << avahi_strerror(avahi_client_errno(browseAvahi->client_)) << "\n";
138 LOG(ERROR, LOG_TAG) << "Failed to resolve service '" << name << "': " << avahi_strerror(avahi_client_errno(browseAvahi->client_)) << "\n";
137139
138140 break;
139141
140142 case AVAHI_BROWSER_REMOVE:
141 LOG(INFO) << "(Browser) REMOVE: service '" << name << "' of type '" << type << "' in domain '" << domain << "'\n";
143 LOG(INFO, LOG_TAG) << "(Browser) REMOVE: service '" << name << "' of type '" << type << "' in domain '" << domain << "'\n";
142144 break;
143145
144146 case AVAHI_BROWSER_ALL_FOR_NOW:
145147 case AVAHI_BROWSER_CACHE_EXHAUSTED:
146 LOG(INFO) << "(Browser) " << (event == AVAHI_BROWSER_CACHE_EXHAUSTED ? "CACHE_EXHAUSTED" : "ALL_FOR_NOW") << "\n";
148 LOG(INFO, LOG_TAG) << "(Browser) " << (event == AVAHI_BROWSER_CACHE_EXHAUSTED ? "CACHE_EXHAUSTED" : "ALL_FOR_NOW") << "\n";
147149 break;
148150 }
149151 }
158160
159161 if (state == AVAHI_CLIENT_FAILURE)
160162 {
161 LOG(ERROR) << "Server connection failure: " << avahi_strerror(avahi_client_errno(c)) << "\n";
163 LOG(ERROR, LOG_TAG) << "Server connection failure: " << avahi_strerror(avahi_client_errno(c)) << "\n";
162164 avahi_simple_poll_quit(simple_poll);
163165 }
164166 }
1515
1616 using namespace std;
1717
18 static constexpr auto LOG_TAG = "Bonjour";
19
1820 struct DNSServiceRefDeleter
1921 {
2022 void operator()(DNSServiceRef* ref)
2426 }
2527 };
2628
27 typedef std::unique_ptr<DNSServiceRef, DNSServiceRefDeleter> DNSServiceHandle;
29 using DNSServiceHandle = std::unique_ptr<DNSServiceRef, DNSServiceRefDeleter>;
2830
2931 string BonjourGetError(DNSServiceErrorType error)
3032 {
236238 runService(service);
237239 }
238240
239 if (resultCollection.size() == 0)
241 resultCollection.erase(std::remove_if(resultCollection.begin(), resultCollection.end(), [](const mDNSResult& res) { return res.ip.empty(); }),
242 resultCollection.end());
243
244 if (resultCollection.empty())
240245 return false;
241246
242 if (resultCollection.size() != 1)
243 LOG(NOTICE) << "Multiple servers found. Using first" << endl;
244
245 result = resultCollection[0];
247 if (resultCollection.size() > 1)
248 LOG(NOTICE, LOG_TAG) << "Multiple servers found. Using first" << endl;
249
250 result = resultCollection.front();
246251
247252 return true;
248253 }
2727
2828 #if defined(HAS_AVAHI)
2929 #include "browse_avahi.hpp"
30 typedef BrowseAvahi BrowseZeroConf;
30 using BrowseZeroConf = BrowseAvahi;
3131 #elif defined(HAS_BONJOUR)
3232 #include "browse_bonjour.hpp"
33 typedef BrowseBonjour BrowseZeroConf;
33 using BrowseZeroConf = BrowseBonjour;
3434 #endif
3535
3636 #endif
00 #/bin/sh
11
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 exit
5 fi
2 export NDK_DIR="$1"
3 export JNI_LIBS_DIR="$2"
4 export TOOLCHAIN="$NDK_DIR/toolchains/llvm/prebuilt/linux-x86_64"
65
7 if [ -z "$JNI_LIBS_DIR" ]; then
8 echo "Specify the snapdroid jniLibs dir JNI_LIBS_DIR"
9 exit
10 fi
6 export ARCH=x86
7 make clean;
8 make TARGET=ANDROID -j 4; $TOOLCHAIN/x86_64-linux-android/bin/strip ./snapclient; cp ./snapclient "$JNI_LIBS_DIR/x86_64/libsnapclient.so"; mv ./snapclient "$JNI_LIBS_DIR/x86/libsnapclient.so"
119
12 if [ -n "$NDK_DIR_ARM" ]; then
13 export NDK_DIR="$NDK_DIR_ARM"
14 export ARCH=arm
15 make clean;
16 make TARGET=ANDROID -j 4; $NDK_DIR/bin/arm-linux-androideabi-strip ./snapclient; mv ./snapclient "$JNI_LIBS_DIR/armeabi-v7a/libsnapclient.so"
17 fi
10 export ARCH=arm
11 make clean;
12 make TARGET=ANDROID -j 4; $TOOLCHAIN/arm-linux-androideabi/bin/strip ./snapclient; mv ./snapclient "$JNI_LIBS_DIR/armeabi-v7a/libsnapclient.so"
1813
19 if [ -n "$NDK_DIR_ARM64" ]; then
20 export NDK_DIR="$NDK_DIR_ARM64"
21 export ARCH=arm64
22 make clean;
23 make TARGET=ANDROID -j 4; $NDK_DIR/bin/aarch64-linux-android-strip ./snapclient; mv ./snapclient "$JNI_LIBS_DIR/arm64-v8a/libsnapclient.so"
24 fi
25
26 if [ -n "$NDK_DIR_X86" ]; then
27 export NDK_DIR="$NDK_DIR_X86"
28 export ARCH=x86
29 make clean;
30 make TARGET=ANDROID -j 4; $NDK_DIR/bin/i686-linux-android-strip ./snapclient; mv ./snapclient "$JNI_LIBS_DIR/x86/libsnapclient.so"
31 fi
14 export ARCH=aarch64
15 make clean;
16 make TARGET=ANDROID -j 4; $TOOLCHAIN/aarch64-linux-android/bin/strip ./snapclient; mv ./snapclient "$JNI_LIBS_DIR/arm64-v8a/libsnapclient.so"
1919 #include "common/aixlog.hpp"
2020 #include "common/snap_exception.hpp"
2121 #include "common/str_compat.hpp"
22 #include "message/factory.hpp"
2322 #include "message/hello.hpp"
2423 #include <iostream>
2524 #include <mutex>
2726
2827 using namespace std;
2928
30
31 ClientConnection::ClientConnection(MessageReceiver* receiver, const std::string& host, size_t port)
32 : socket_(io_context_), active_(false), messageReceiver_(receiver), reqId_(1), host_(host), port_(port), readerThread_(nullptr),
33 sumTimeout_(chronos::msec(0))
29 static constexpr auto LOG_TAG = "Connection";
30
31 ClientConnection::ClientConnection(boost::asio::io_context& io_context, const ClientSettings::Server& server)
32 : io_context_(io_context), resolver_(io_context_), socket_(io_context_), reqId_(1), server_(server), strand_(io_context_)
3433 {
3534 base_msg_size_ = base_message_.getSize();
3635 buffer_.resize(base_msg_size_);
3938
4039 ClientConnection::~ClientConnection()
4140 {
42 stop();
43 }
44
45
46 void ClientConnection::socketRead(void* _to, size_t _bytes)
47 {
48 size_t toRead = _bytes;
49 size_t len = 0;
50 do
51 {
52 len += socket_.read_some(boost::asio::buffer((char*)_to + len, toRead));
53 // cout << "len: " << len << ", error: " << error << endl;
54 toRead = _bytes - len;
55 } while (toRead > 0);
41 disconnect();
5642 }
5743
5844
5945 std::string ClientConnection::getMacAddress()
6046 {
61 std::string mac = ::getMacAddress(socket_.native_handle());
47 std::string mac =
48 #ifndef WINDOWS
49 ::getMacAddress(socket_.native_handle());
50 #else
51 ::getMacAddress(socket_.local_endpoint().address().to_string());
52 #endif
6253 if (mac.empty())
6354 mac = "00:00:00:00:00:00";
64 LOG(INFO) << "My MAC: \"" << mac << "\", socket: " << socket_.native_handle() << "\n";
55 LOG(INFO, LOG_TAG) << "My MAC: \"" << mac << "\", socket: " << socket_.native_handle() << "\n";
6556 return mac;
6657 }
6758
6859
69 void ClientConnection::start()
70 {
71 tcp::resolver resolver(io_context_);
72 tcp::resolver::query query(host_, cpt::to_string(port_), boost::asio::ip::resolver_query_base::numeric_service);
73 auto iterator = resolver.resolve(query);
74 LOG(DEBUG) << "Connecting\n";
75 // struct timeval tv;
76 // tv.tv_sec = 5;
77 // tv.tv_usec = 0;
78 // cout << "socket: " << socket->native_handle() << "\n";
79 // setsockopt(socket->native_handle(), SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
80 // setsockopt(socket->native_handle(), SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
81 socket_.connect(*iterator);
82 SLOG(NOTICE) << "Connected to " << socket_.remote_endpoint().address().to_string() << endl;
83 active_ = true;
84 sumTimeout_ = chronos::msec(0);
85 readerThread_ = make_unique<thread>(&ClientConnection::reader, this);
86 }
87
88
89 void ClientConnection::stop()
90 {
91 active_ = false;
92 try
60 void ClientConnection::connect(const ResultHandler& handler)
61 {
62 tcp::resolver::query query(server_.host, cpt::to_string(server_.port), boost::asio::ip::resolver_query_base::numeric_service);
63 boost::system::error_code ec;
64 LOG(INFO, LOG_TAG) << "Resolving host IP for: " << server_.host << "\n";
65 auto iterator = resolver_.resolve(query, ec);
66 if (ec)
9367 {
94 boost::system::error_code ec;
95 socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
68 LOG(ERROR, LOG_TAG) << "Failed to resolve host '" << server_.host << "', error: " << ec.message() << "\n";
69 handler(ec);
70 return;
71 }
72
73 LOG(INFO, LOG_TAG) << "Connecting\n";
74 socket_.connect(*iterator, ec);
75 if (ec)
76 {
77 LOG(ERROR, LOG_TAG) << "Failed to connect to host '" << server_.host << "', error: " << ec.message() << "\n";
78 handler(ec);
79 return;
80 }
81 LOG(NOTICE, LOG_TAG) << "Connected to " << socket_.remote_endpoint().address().to_string() << endl;
82 handler(ec);
83
84 #if 0
85 resolver_.async_resolve(query, host_, cpt::to_string(port_), [this, handler](const boost::system::error_code& ec, tcp::resolver::results_type results) {
9686 if (ec)
97 LOG(ERROR) << "Error in socket shutdown: " << ec.message() << endl;
98 socket_.close(ec);
99 if (ec)
100 LOG(ERROR) << "Error in socket close: " << ec.message() << endl;
101 if (readerThread_)
10287 {
103 LOG(DEBUG) << "joining readerThread\n";
104 readerThread_->join();
88 LOG(ERROR, LOG_TAG) << "Failed to resolve host '" << host_ << "', error: " << ec.message() << "\n";
89 handler(ec);
90 return;
10591 }
92
93 resolver_.cancel();
94 socket_.async_connect(*results, [this, handler](const boost::system::error_code& ec) {
95 if (ec)
96 {
97 LOG(ERROR, LOG_TAG) << "Failed to connect to host '" << host_ << "', error: " << ec.message() << "\n";
98 handler(ec);
99 return;
100 }
101
102 LOG(NOTICE, LOG_TAG) << "Connected to " << socket_.remote_endpoint().address().to_string() << endl;
103 handler(ec);
104 getNextMessage();
105 });
106 });
107 #endif
108 }
109
110
111 void ClientConnection::disconnect()
112 {
113 LOG(DEBUG, LOG_TAG) << "Disconnecting\n";
114 if (!socket_.is_open())
115 {
116 LOG(DEBUG, LOG_TAG) << "Not connected\n";
117 return;
106118 }
107 catch (...)
108 {
109 }
110 readerThread_ = nullptr;
111 LOG(DEBUG) << "readerThread terminated\n";
112 }
113
114
115 bool ClientConnection::send(const msg::BaseMessage* message)
116 {
117 // std::unique_lock<std::mutex> mlock(mutex_);
118 // LOG(DEBUG) << "send: " << message->type << ", size: " << message->getSize() << "\n";
119 std::lock_guard<std::mutex> socketLock(socketMutex_);
120 if (!socket_.is_open())
121 return false;
122 // LOG(DEBUG) << "send: " << message->type << ", size: " << message->getSize() << "\n";
123 boost::asio::streambuf streambuf;
119 boost::system::error_code ec;
120 socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
121 if (ec)
122 LOG(ERROR, LOG_TAG) << "Error in socket shutdown: " << ec.message() << endl;
123 socket_.close(ec);
124 if (ec)
125 LOG(ERROR, LOG_TAG) << "Error in socket close: " << ec.message() << endl;
126 boost::asio::post(strand_, [this]() { pendingRequests_.clear(); });
127 LOG(DEBUG, LOG_TAG) << "Disconnected\n";
128 }
129
130
131 void ClientConnection::sendNext()
132 {
133 auto& message = messages_.front();
134 static boost::asio::streambuf streambuf;
124135 std::ostream stream(&streambuf);
125136 tv t;
126 message->sent = t;
127 message->serialize(stream);
128 boost::asio::write(socket_, streambuf);
129 return true;
130 }
131
132
133
134 unique_ptr<msg::BaseMessage> ClientConnection::sendRequest(const msg::BaseMessage* message, const chronos::msec& timeout)
135 {
136 unique_ptr<msg::BaseMessage> response(nullptr);
137 if (++reqId_ >= 10000)
138 reqId_ = 1;
139 message->id = reqId_;
140 // LOG(INFO) << "Req: " << message->id << "\n";
141 shared_ptr<PendingRequest> pendingRequest = make_shared<PendingRequest>(reqId_);
142
143 { // scope for lock
144 std::unique_lock<std::mutex> lock(pendingRequestsMutex_);
145 pendingRequests_.insert(pendingRequest);
146 send(message);
147 }
148
149 if ((response = pendingRequest->waitForResponse(std::chrono::milliseconds(timeout))) != nullptr)
150 {
151 sumTimeout_ = chronos::msec(0);
152 // LOG(INFO) << "Resp: " << pendingRequest->id << "\n";
153 }
154 else
155 {
156 sumTimeout_ += timeout;
157 LOG(WARNING) << "timeout while waiting for response to: " << reqId_ << ", timeout " << sumTimeout_.count() << "\n";
158 if (sumTimeout_ > chronos::sec(10))
159 throw SnapException("sum timeout exceeded 10s");
160 }
161
162 { // scope for lock
163 std::unique_lock<std::mutex> lock(pendingRequestsMutex_);
164 pendingRequests_.erase(pendingRequest);
165 }
166 return response;
167 }
168
169
170 void ClientConnection::getNextMessage()
171 {
172 socketRead(&buffer_[0], base_msg_size_);
173 base_message_.deserialize(buffer_.data());
174 // LOG(DEBUG) << "getNextMessage: " << base_message_.type << ", size: " << base_message_.size << ", id: " << base_message_.id
175 // << ", refers: " << base_message_.refersTo << "\n";
176 if (base_message_.size > buffer_.size())
177 buffer_.resize(base_message_.size);
178 // {
179 // std::lock_guard<std::mutex> socketLock(socketMutex_);
180 socketRead(buffer_.data(), base_message_.size);
181 tv t;
182 base_message_.received = t;
183 // }
184
185 { // scope for lock
186 std::unique_lock<std::mutex> lock(pendingRequestsMutex_);
187 for (auto req : pendingRequests_)
137 message.msg->sent = t;
138 message.msg->serialize(stream);
139 auto handler = message.handler;
140
141 boost::asio::async_write(socket_, streambuf, boost::asio::bind_executor(strand_, [this, handler](boost::system::error_code ec, std::size_t length) {
142 if (ec)
143 LOG(ERROR, LOG_TAG) << "Failed to send message, error: " << ec.message() << "\n";
144 else
145 LOG(TRACE, LOG_TAG) << "Wrote " << length << " bytes to socket\n";
146
147 messages_.pop_front();
148 if (handler)
149 handler(ec);
150
151 if (!messages_.empty())
152 sendNext();
153 }));
154 }
155
156
157 void ClientConnection::send(const msg::message_ptr& message, const ResultHandler& handler)
158 {
159 strand_.post([this, message, handler]() {
160 messages_.emplace_back(message, handler);
161 if (messages_.size() > 1)
188162 {
189 if (req->id() == base_message_.refersTo)
190 {
191 auto response = msg::factory::createMessage(base_message_, buffer_.data());
192 req->setValue(std::move(response));
193 return;
194 }
163 LOG(DEBUG, LOG_TAG) << "outstanding async_write\n";
164 return;
195165 }
196 }
197
198 if (messageReceiver_ != nullptr)
199 messageReceiver_->onMessageReceived(this, base_message_, buffer_.data());
200 }
201
202
203
204 void ClientConnection::reader()
205 {
206 try
207 {
208 while (active_)
209 {
210 getNextMessage();
211 }
212 }
213 catch (...)
214 {
215 if (messageReceiver_ != nullptr)
216 messageReceiver_->onException(this, std::current_exception());
217 }
218 active_ = false;
219 }
166 sendNext();
167 });
168 }
169
170
171 void ClientConnection::sendRequest(const msg::message_ptr& message, const chronos::usec& timeout, const MessageHandler<msg::BaseMessage>& handler)
172 {
173 boost::asio::post(strand_, [this, message, timeout, handler]() {
174 pendingRequests_.erase(
175 std::remove_if(pendingRequests_.begin(), pendingRequests_.end(), [](std::weak_ptr<PendingRequest> request) { return request.expired(); }),
176 pendingRequests_.end());
177 unique_ptr<msg::BaseMessage> response(nullptr);
178 if (++reqId_ >= 10000)
179 reqId_ = 1;
180 message->id = reqId_;
181 auto request = make_shared<PendingRequest>(io_context_, strand_, reqId_, handler);
182 pendingRequests_.push_back(request);
183 request->startTimer(timeout);
184 send(message, [handler](const boost::system::error_code& ec) {
185 if (ec)
186 handler(ec, nullptr);
187 });
188 });
189 }
190
191
192 void ClientConnection::getNextMessage(const MessageHandler<msg::BaseMessage>& handler)
193 {
194 boost::asio::async_read(socket_, boost::asio::buffer(buffer_, base_msg_size_),
195 boost::asio::bind_executor(strand_, [this, handler](boost::system::error_code ec, std::size_t length) mutable {
196 if (ec)
197 {
198 LOG(ERROR, LOG_TAG) << "Error reading message header of length " << length << ": " << ec.message() << "\n";
199 if (handler)
200 handler(ec, nullptr);
201 return;
202 }
203
204 base_message_.deserialize(buffer_.data());
205 tv t;
206 base_message_.received = t;
207 // LOG(TRACE, LOG_TAG) << "getNextMessage: " << base_message_.type << ", size: " << base_message_.size << ", id: " <<
208 // base_message_.id << ", refers: " << base_message_.refersTo << "\n";
209 if (base_message_.type > message_type::kLast)
210 {
211 LOG(ERROR, LOG_TAG) << "unknown message type received: " << base_message_.type << ", size: " << base_message_.size << "\n";
212 if (handler)
213 handler(boost::asio::error::invalid_argument, nullptr);
214 return;
215 }
216 else if (base_message_.size > msg::max_size)
217 {
218 LOG(ERROR, LOG_TAG) << "received message of type " << base_message_.type << " to large: " << base_message_.size << "\n";
219 if (handler)
220 handler(boost::asio::error::invalid_argument, nullptr);
221 return;
222 }
223
224 if (base_message_.size > buffer_.size())
225 buffer_.resize(base_message_.size);
226
227 boost::asio::async_read(
228 socket_, boost::asio::buffer(buffer_, base_message_.size),
229 boost::asio::bind_executor(strand_, [this, handler](boost::system::error_code ec, std::size_t length) mutable {
230 if (ec)
231 {
232 LOG(ERROR, LOG_TAG) << "Error reading message body of length " << length << ": " << ec.message() << "\n";
233 if (handler)
234 handler(ec, nullptr);
235 return;
236 }
237
238 auto response = msg::factory::createMessage(base_message_, buffer_.data());
239 for (auto iter = pendingRequests_.begin(); iter != pendingRequests_.end(); ++iter)
240 {
241 auto request = *iter;
242 if (auto req = request.lock())
243 {
244 if (req->id() == base_message_.refersTo)
245 {
246 req->setValue(std::move(response));
247 pendingRequests_.erase(iter);
248 getNextMessage(handler);
249 return;
250 }
251 }
252 }
253
254 if (handler)
255 handler(ec, std::move(response));
256 }));
257 }));
258 }
1818 #ifndef CLIENT_CONNECTION_H
1919 #define CLIENT_CONNECTION_H
2020
21 #include "client_settings.hpp"
2122 #include "common/time_defs.hpp"
23 #include "message/factory.hpp"
2224 #include "message/message.hpp"
25
2326 #include <atomic>
2427 #include <boost/asio.hpp>
2528 #include <condition_variable>
29 #include <deque>
2630 #include <memory>
2731 #include <mutex>
2832 #include <set>
3539
3640 class ClientConnection;
3741
42 template <typename Message>
43 using MessageHandler = std::function<void(const boost::system::error_code&, std::unique_ptr<Message>)>;
3844
3945 /// Used to synchronize server requests (wait for server response)
40 class PendingRequest
46 class PendingRequest : public std::enable_shared_from_this<PendingRequest>
4147 {
4248 public:
43 PendingRequest(uint16_t reqId) : id_(reqId)
49 PendingRequest(boost::asio::io_context& io_context, boost::asio::io_context::strand& strand, uint16_t reqId,
50 const MessageHandler<msg::BaseMessage>& handler)
51 : id_(reqId), timer_(io_context), strand_(strand), handler_(handler){};
52
53 virtual ~PendingRequest()
4454 {
45 future_ = promise_.get_future();
46 };
47
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;
55 handler_ = nullptr;
56 timer_.cancel();
6057 }
6158
59 /// Set the response for the pending request and passes it to the handler
60 /// @param value the response message
6261 void setValue(std::unique_ptr<msg::BaseMessage> value)
6362 {
64 promise_.set_value(std::move(value));
63 boost::asio::post(strand_, [ this, self = shared_from_this(), val = std::move(value) ]() mutable {
64 timer_.cancel();
65 if (handler_)
66 handler_({}, std::move(val));
67 });
6568 }
6669
70 /// @return the id of the request
6771 uint16_t id() const
6872 {
6973 return id_;
7074 }
7175
76 /// Start the timer for the request
77 /// @param timeout the timeout to wait for the reception of the response
78 void startTimer(const chronos::usec& timeout)
79 {
80 timer_.expires_after(timeout);
81 timer_.async_wait(boost::asio::bind_executor(strand_, [ this, self = shared_from_this() ](boost::system::error_code ec) {
82 if (!handler_)
83 return;
84 if (!ec)
85 {
86 // !ec => expired => timeout
87 handler_(boost::asio::error::timed_out, nullptr);
88 handler_ = nullptr;
89 }
90 else if (ec != boost::asio::error::operation_aborted)
91 {
92 // ec != aborted => not cancelled (in setValue)
93 // => should not happen, but who knows => pass the error to the handler
94 handler_(ec, nullptr);
95 }
96 }));
97 }
98
99 /// Needed to put the requests in a container
100 bool operator<(const PendingRequest& other) const
101 {
102 return (id_ < other.id());
103 }
104
105
72106 private:
73107 uint16_t id_;
74
75 std::promise<std::unique_ptr<msg::BaseMessage>> promise_;
76 std::future<std::unique_ptr<msg::BaseMessage>> future_;
108 boost::asio::steady_timer timer_;
109 boost::asio::io_context::strand& strand_;
110 MessageHandler<msg::BaseMessage> handler_;
77111 };
78112
79
80 /// Interface: callback for a received message and error reporting
81 class MessageReceiver
82 {
83 public:
84 virtual ~MessageReceiver() = default;
85 virtual void onMessageReceived(ClientConnection* connection, const msg::BaseMessage& baseMessage, char* buffer) = 0;
86 virtual void onException(ClientConnection* connection, std::exception_ptr exception) = 0;
87 };
88113
89114
90115 /// Endpoint of the server connection
96121 class ClientConnection
97122 {
98123 public:
99 /// ctor. Received message from the server are passed to MessageReceiver
100 ClientConnection(MessageReceiver* receiver, const std::string& host, size_t port);
124 using ResultHandler = std::function<void(const boost::system::error_code&)>;
125
126 /// c'tor
127 ClientConnection(boost::asio::io_context& io_context, const ClientSettings::Server& server);
128 /// d'tor
101129 virtual ~ClientConnection();
102 virtual void start();
103 virtual void stop();
104 virtual bool send(const msg::BaseMessage* message);
130
131 /// async connect
132 /// @param handler async result handler
133 void connect(const ResultHandler& handler);
134 /// disconnect the socket
135 void disconnect();
136
137 /// async send a message
138 /// @param message the message
139 /// @param handler the result handler
140 void send(const msg::message_ptr& message, const ResultHandler& handler);
105141
106142 /// Send request to the server and wait for answer
107 virtual std::unique_ptr<msg::BaseMessage> sendRequest(const msg::BaseMessage* message, const chronos::msec& timeout = chronos::msec(1000));
143 /// @param message the message
144 /// @param timeout the send timeout
145 /// @param handler async result handler with the response message or error
146 void sendRequest(const msg::message_ptr& message, const chronos::usec& timeout, const MessageHandler<msg::BaseMessage>& handler);
108147
109 /// Send request to the server and wait for answer of type T
110 template <typename T>
111 std::unique_ptr<T> sendReq(const msg::BaseMessage* message, const chronos::msec& timeout = chronos::msec(1000))
148 /// @sa sendRequest with templated response message
149 template <typename Message>
150 void sendRequest(const msg::message_ptr& message, const chronos::usec& timeout, const MessageHandler<Message>& handler)
112151 {
113 std::unique_ptr<msg::BaseMessage> response = sendRequest(message, timeout);
114 if (!response)
115 return nullptr;
116
117 T* tmp = dynamic_cast<T*>(response.get());
118 std::unique_ptr<T> result;
119 if (tmp != nullptr)
120 {
121 response.release();
122 result.reset(tmp);
123 }
124 return result;
152 sendRequest(message, timeout, [handler](const boost::system::error_code& ec, std::unique_ptr<msg::BaseMessage> response) {
153 if (ec)
154 handler(ec, nullptr);
155 else
156 handler(ec, msg::message_cast<Message>(std::move(response)));
157 });
125158 }
126159
127160 std::string getMacAddress();
128161
129 virtual bool active() const
130 {
131 return active_;
132 }
162 /// async get the next message
163 /// @param handler the next received message or error
164 void getNextMessage(const MessageHandler<msg::BaseMessage>& handler);
133165
134166 protected:
135 virtual void reader();
136
137 void socketRead(void* to, size_t bytes);
138 void getNextMessage();
167 void sendNext();
139168
140169 msg::BaseMessage base_message_;
141170 std::vector<char> buffer_;
142171 size_t base_msg_size_;
143172
144 boost::asio::io_context io_context_;
145 mutable std::mutex socketMutex_;
173 boost::asio::io_context& io_context_;
174 tcp::resolver resolver_;
146175 tcp::socket socket_;
147 std::atomic<bool> active_;
148 MessageReceiver* messageReceiver_;
149 mutable std::mutex pendingRequestsMutex_;
150 std::set<std::shared_ptr<PendingRequest>> pendingRequests_;
176 std::vector<std::weak_ptr<PendingRequest>> pendingRequests_;
151177 uint16_t reqId_;
152 std::string host_;
153 size_t port_;
154 std::unique_ptr<std::thread> readerThread_;
155 chronos::msec sumTimeout_;
178 ClientSettings::Server server_;
179
180 boost::asio::io_context::strand strand_;
181 struct PendingMessage
182 {
183 PendingMessage(const msg::message_ptr& msg, ResultHandler handler) : msg(msg), handler(handler)
184 {
185 }
186 msg::message_ptr msg;
187 ResultHandler handler;
188 };
189 std::deque<PendingMessage> messages_;
156190 };
157191
158192
2121 #include <string>
2222 #include <vector>
2323
24 #include "common/sample_format.hpp"
2425 #include "player/pcm_device.hpp"
2526
2627
2728 struct ClientSettings
2829 {
29 struct ServerSettings
30 enum class SharingMode
31 {
32 unspecified,
33 exclusive,
34 shared
35 };
36
37 struct Mixer
38 {
39 enum class Mode
40 {
41 hardware,
42 software,
43 script,
44 none
45 };
46
47 Mode mode{Mode::software};
48 std::string parameter{""};
49 };
50
51 struct Server
3052 {
3153 std::string host{""};
3254 size_t port{1704};
3355 };
3456
35 struct PlayerSettings
57 struct Player
3658 {
3759 std::string player_name{""};
60 std::string parameter{""};
3861 int latency{0};
3962 PcmDevice pcm_device;
4063 SampleFormat sample_format;
64 SharingMode sharing_mode{SharingMode::unspecified};
65 Mixer mixer;
4166 };
4267
43 struct LoggingSettings
68 struct Logging
4469 {
45 bool debug{false};
46 std::string debug_logfile{""};
70 std::string sink{""};
71 std::string filter{"*:info"};
4772 };
4873
4974 size_t instance{1};
5075 std::string host_id;
5176
52 ServerSettings server;
53 PlayerSettings player;
54 LoggingSettings logging;
77 Server server;
78 Player player;
79 Logging logging;
5580 };
5681
5782 #endif
1515 along with this program. If not, see <http://www.gnu.org/licenses/>.
1616 ***/
1717
18 #ifndef NOMINMAX
19 #define NOMINMAX
20 #endif // NOMINMAX
21
1822 #include "controller.hpp"
1923 #include "decoder/pcm_decoder.hpp"
24 #if defined(HAS_OGG) && (defined(HAS_TREMOR) || defined(HAS_VORBIS))
25 #include "decoder/ogg_decoder.hpp"
26 #endif
27 #if defined(HAS_FLAC)
28 #include "decoder/flac_decoder.hpp"
29 #endif
30 #if defined(HAS_OPUS)
31 #include "decoder/opus_decoder.hpp"
32 #endif
33
34 #ifdef HAS_ALSA
35 #include "player/alsa_player.hpp"
36 #endif
37 #ifdef HAS_OPENSL
38 #include "player/opensl_player.hpp"
39 #endif
40 #ifdef HAS_OBOE
41 #include "player/oboe_player.hpp"
42 #endif
43 #ifdef HAS_COREAUDIO
44 #include "player/coreaudio_player.hpp"
45 #endif
46 #ifdef HAS_WASAPI
47 #include "player/wasapi_player.hpp"
48 #endif
49 #include "player/file_player.hpp"
50
51 #include "browseZeroConf/browse_mdns.hpp"
52 #include "common/aixlog.hpp"
53 #include "common/snap_exception.hpp"
54 #include "message/client_info.hpp"
55 #include "message/hello.hpp"
56 #include "message/time.hpp"
57 #include "time_provider.hpp"
58
59 #include <algorithm>
2060 #include <iostream>
2161 #include <memory>
2262 #include <string>
63
64 using namespace std;
65
66 static constexpr auto LOG_TAG = "Controller";
67 static constexpr auto TIME_SYNC_INTERVAL = 1s;
68
69 Controller::Controller(boost::asio::io_context& io_context, const ClientSettings& settings, std::unique_ptr<MetadataAdapter> meta)
70 : io_context_(io_context), timer_(io_context), settings_(settings), stream_(nullptr), decoder_(nullptr), player_(nullptr), meta_(std::move(meta)),
71 serverSettings_(nullptr)
72 {
73 }
74
75
76 template <typename PlayerType>
77 std::unique_ptr<Player> Controller::createPlayer(ClientSettings::Player& settings, const std::string& player_name)
78 {
79 if (settings.player_name.empty() || settings.player_name == player_name)
80 {
81 settings.player_name = player_name;
82 return make_unique<PlayerType>(io_context_, settings, stream_);
83 }
84 return nullptr;
85 }
86
87 std::vector<std::string> Controller::getSupportedPlayerNames()
88 {
89 std::vector<std::string> result;
90 #ifdef HAS_ALSA
91 result.emplace_back("alsa");
92 #endif
93 #ifdef HAS_OBOE
94 result.emplace_back("oboe");
95 #endif
96 #ifdef HAS_OPENSL
97 result.emplace_back("opensl");
98 #endif
99 #ifdef HAS_COREAUDIO
100 result.emplace_back("coreaudio");
101 #endif
102 #ifdef HAS_WASAPI
103 result.emplace_back("wasapi");
104 #endif
105 result.emplace_back("file");
106 return result;
107 }
108
109
110 void Controller::getNextMessage()
111 {
112 clientConnection_->getNextMessage([this](const boost::system::error_code& ec, std::unique_ptr<msg::BaseMessage> response) {
113 if (ec)
114 {
115 reconnect();
116 return;
117 }
118
119 if (response->type == message_type::kWireChunk)
120 {
121 if (stream_ && decoder_)
122 {
123 // execute on the io_context to do the (costly) decoding on another thread (if more than one thread is used)
124 // boost::asio::post(io_context_, [this, response = std::move(response)]() mutable {
125 auto pcmChunk = msg::message_cast<msg::PcmChunk>(std::move(response));
126 pcmChunk->format = sampleFormat_;
127 // LOG(TRACE, LOG_TAG) << "chunk: " << pcmChunk->payloadSize << ", sampleFormat: " << sampleFormat_.toString() << "\n";
128 if (decoder_->decode(pcmChunk.get()))
129 {
130 // LOG(TRACE, LOG_TAG) << ", decoded: " << pcmChunk->payloadSize << ", Duration: " << pcmChunk->durationMs() << ", sec: " <<
131 // pcmChunk->timestamp.sec << ", usec: " << pcmChunk->timestamp.usec / 1000 << ", type: " << pcmChunk->type << "\n";
132 stream_->addChunk(std::move(pcmChunk));
133 }
134 // });
135 }
136 }
137 else if (response->type == message_type::kServerSettings)
138 {
139 serverSettings_ = msg::message_cast<msg::ServerSettings>(std::move(response));
140 LOG(INFO, LOG_TAG) << "ServerSettings - buffer: " << serverSettings_->getBufferMs() << ", latency: " << serverSettings_->getLatency()
141 << ", volume: " << serverSettings_->getVolume() << ", muted: " << serverSettings_->isMuted() << "\n";
142 if (stream_ && player_)
143 {
144 player_->setVolume(serverSettings_->getVolume() / 100., serverSettings_->isMuted());
145 stream_->setBufferLen(std::max(0, serverSettings_->getBufferMs() - serverSettings_->getLatency() - settings_.player.latency));
146 }
147 }
148 else if (response->type == message_type::kCodecHeader)
149 {
150 headerChunk_ = msg::message_cast<msg::CodecHeader>(std::move(response));
151 decoder_.reset(nullptr);
152 stream_ = nullptr;
153 player_.reset(nullptr);
154
155 if (headerChunk_->codec == "pcm")
156 decoder_ = make_unique<decoder::PcmDecoder>();
23157 #if defined(HAS_OGG) && (defined(HAS_TREMOR) || defined(HAS_VORBIS))
24 #include "decoder/ogg_decoder.hpp"
158 else if (headerChunk_->codec == "ogg")
159 decoder_ = make_unique<decoder::OggDecoder>();
25160 #endif
26161 #if defined(HAS_FLAC)
27 #include "decoder/flac_decoder.hpp"
162 else if (headerChunk_->codec == "flac")
163 decoder_ = make_unique<decoder::FlacDecoder>();
28164 #endif
29165 #if defined(HAS_OPUS)
30 #include "decoder/opus_decoder.hpp"
31 #endif
32 #include "common/aixlog.hpp"
33 #include "common/snap_exception.hpp"
34 #include "message/hello.hpp"
35 #include "message/time.hpp"
36 #include "time_provider.hpp"
37
38 using namespace std;
39
40
41 Controller::Controller(const ClientSettings& settings, std::unique_ptr<MetadataAdapter> meta)
42 : MessageReceiver(), settings_(settings), active_(false), stream_(nullptr), decoder_(nullptr), player_(nullptr), meta_(std::move(meta)),
43 serverSettings_(nullptr), async_exception_(nullptr)
44 {
45 }
46
47
48 void Controller::onException(ClientConnection* /*connection*/, std::exception_ptr exception)
49 {
50 LOG(ERROR) << "Controller::onException\n";
51 async_exception_ = exception;
52 }
53
54
55 void Controller::onMessageReceived(ClientConnection* /*connection*/, const msg::BaseMessage& baseMessage, char* buffer)
56 {
57 std::lock_guard<std::mutex> lock(receiveMutex_);
58 if (baseMessage.type == message_type::kWireChunk)
166 else if (headerChunk_->codec == "opus")
167 decoder_ = make_unique<decoder::OpusDecoder>();
168 #endif
169 else
170 throw SnapException("codec not supported: \"" + headerChunk_->codec + "\"");
171
172 sampleFormat_ = decoder_->setHeader(headerChunk_.get());
173 LOG(INFO, LOG_TAG) << "Codec: " << headerChunk_->codec << ", sampleformat: " << sampleFormat_.toString() << "\n";
174
175 stream_ = make_shared<Stream>(sampleFormat_, settings_.player.sample_format);
176 stream_->setBufferLen(std::max(0, serverSettings_->getBufferMs() - serverSettings_->getLatency() - settings_.player.latency));
177
178 #ifdef HAS_ALSA
179 if (!player_)
180 player_ = createPlayer<AlsaPlayer>(settings_.player, "alsa");
181 #endif
182 #ifdef HAS_OBOE
183 if (!player_)
184 player_ = createPlayer<OboePlayer>(settings_.player, "oboe");
185 #endif
186 #ifdef HAS_OPENSL
187 if (!player_)
188 player_ = createPlayer<OpenslPlayer>(settings_.player, "opensl");
189 #endif
190 #ifdef HAS_COREAUDIO
191 if (!player_)
192 player_ = createPlayer<CoreAudioPlayer>(settings_.player, "coreaudio");
193 #endif
194 #ifdef HAS_WASAPI
195 if (!player_)
196 player_ = createPlayer<WASAPIPlayer>(settings_.player, "wasapi");
197 #endif
198 if (!player_ && (settings_.player.player_name == "file"))
199 player_ = createPlayer<FilePlayer>(settings_.player, "file");
200
201 if (!player_)
202 throw SnapException("No audio player support" + (settings_.player.player_name.empty() ? "" : " for: " + settings_.player.player_name));
203
204 player_->setVolumeCallback([this](double volume, bool muted) {
205 static double last_volume(-1);
206 static bool last_muted(true);
207 if ((volume != last_volume) || (last_muted != muted))
208 {
209 last_volume = volume;
210 last_muted = muted;
211 auto info = std::make_shared<msg::ClientInfo>();
212 info->setVolume(static_cast<uint16_t>(volume * 100.));
213 info->setMuted(muted);
214 clientConnection_->send(info, [this](const boost::system::error_code& ec) {
215 if (ec)
216 {
217 LOG(ERROR, LOG_TAG) << "Failed to send client info, error: " << ec.message() << "\n";
218 reconnect();
219 return;
220 }
221 });
222 }
223 });
224 player_->start();
225 // Don't change the initial hardware mixer volume on the user's device.
226 // The player class will send the device's volume to the server instead
227 if (settings_.player.mixer.mode != ClientSettings::Mixer::Mode::hardware)
228 {
229 player_->setVolume(serverSettings_->getVolume() / 100., serverSettings_->isMuted());
230 }
231 }
232 else if (response->type == message_type::kStreamTags)
233 {
234 if (meta_)
235 {
236 auto stream_tags = msg::message_cast<msg::StreamTags>(std::move(response));
237 meta_->push(stream_tags->msg);
238 }
239 }
240 else
241 {
242 LOG(WARNING, LOG_TAG) << "Unexpected message received, type: " << response->type << "\n";
243 }
244 getNextMessage();
245 });
246 }
247
248
249 void Controller::sendTimeSyncMessage(int quick_syncs)
250 {
251 auto timeReq = std::make_shared<msg::Time>();
252 clientConnection_->sendRequest<msg::Time>(timeReq, 2s, [this, quick_syncs](const boost::system::error_code& ec,
253 const std::unique_ptr<msg::Time>& response) mutable {
254 if (ec)
255 {
256 LOG(ERROR, LOG_TAG) << "Time sync request failed: " << ec.message() << "\n";
257 reconnect();
258 return;
259 }
260 else
261 {
262 TimeProvider::getInstance().setDiff(response->latency, response->received - response->sent);
263 }
264
265 std::chrono::microseconds next = TIME_SYNC_INTERVAL;
266 if (quick_syncs > 0)
267 {
268 if (--quick_syncs == 0)
269 LOG(INFO, LOG_TAG) << "diff to server [ms]: " << (float)TimeProvider::getInstance().getDiffToServer<chronos::usec>().count() / 1000.f << "\n";
270 next = 100us;
271 }
272 timer_.expires_after(next);
273 timer_.async_wait([this, quick_syncs](const boost::system::error_code& ec) {
274 if (!ec)
275 {
276 sendTimeSyncMessage(quick_syncs);
277 }
278 });
279 });
280 }
281
282 void Controller::browseMdns(const MdnsHandler& handler)
283 {
284 #if defined(HAS_AVAHI) || defined(HAS_BONJOUR)
285 try
59286 {
60 if (stream_ && decoder_)
61 {
62 auto pcmChunk = make_unique<msg::PcmChunk>(sampleFormat_, 0);
63 pcmChunk->deserialize(baseMessage, buffer);
64 // LOG(DEBUG) << "chunk: " << pcmChunk->payloadSize << ", sampleFormat: " << sampleFormat_.getFormat() << "\n";
65 if (decoder_->decode(pcmChunk.get()))
66 {
67 // TODO: do decoding in thread?
68 stream_->addChunk(move(pcmChunk));
69 // LOG(DEBUG) << ", decoded: " << pcmChunk->payloadSize << ", Duration: " << pcmChunk->getDuration() << ", sec: " << pcmChunk->timestamp.sec <<
70 // ", usec: " << pcmChunk->timestamp.usec/1000 << ", type: " << pcmChunk->type << "\n";
71 }
287 BrowseZeroConf browser;
288 mDNSResult avahiResult;
289 if (browser.browse("_snapcast._tcp", avahiResult, 1000))
290 {
291 string host = avahiResult.ip;
292 uint16_t port = avahiResult.port;
293 if (avahiResult.ip_version == IPVersion::IPv6)
294 host += "%" + cpt::to_string(avahiResult.iface_idx);
295 handler({}, host, port);
296 return;
72297 }
73298 }
74 else if (baseMessage.type == message_type::kTime)
299 catch (const std::exception& e)
75300 {
76 msg::Time reply;
77 reply.deserialize(baseMessage, buffer);
78 TimeProvider::getInstance().setDiff(reply.latency, reply.received - reply.sent); // ToServer(diff / 2);
301 LOG(ERROR, LOG_TAG) << "Exception: " << e.what() << std::endl;
79302 }
80 else if (baseMessage.type == message_type::kServerSettings)
303
304 timer_.expires_after(500ms);
305 timer_.async_wait([this, handler](const boost::system::error_code& ec) {
306 if (!ec)
307 {
308 browseMdns(handler);
309 }
310 else
311 {
312 handler(ec, "", 0);
313 }
314 });
315 #else
316 handler(boost::asio::error::operation_not_supported, "", 0);
317 #endif
318 }
319
320 void Controller::start()
321 {
322 if (settings_.server.host.empty())
81323 {
82 serverSettings_ = make_unique<msg::ServerSettings>();
83 serverSettings_->deserialize(baseMessage, buffer);
84 LOG(INFO) << "ServerSettings - buffer: " << serverSettings_->getBufferMs() << ", latency: " << serverSettings_->getLatency()
85 << ", volume: " << serverSettings_->getVolume() << ", muted: " << serverSettings_->isMuted() << "\n";
86 if (stream_ && player_)
87 {
88 player_->setVolume(serverSettings_->getVolume() / 100.);
89 player_->setMute(serverSettings_->isMuted());
90 stream_->setBufferLen(serverSettings_->getBufferMs() - serverSettings_->getLatency());
91 }
324 browseMdns([this](const boost::system::error_code& ec, const std::string& host, uint16_t port) {
325 if (ec)
326 {
327 LOG(ERROR, LOG_TAG) << "Failed to browse MDNS, error: " << ec.message() << "\n";
328 }
329 else
330 {
331 settings_.server.host = host;
332 settings_.server.port = port;
333 LOG(INFO, LOG_TAG) << "Found server " << settings_.server.host << ":" << settings_.server.port << "\n";
334 clientConnection_ = make_unique<ClientConnection>(io_context_, settings_.server);
335 worker();
336 }
337 });
92338 }
93 else if (baseMessage.type == message_type::kCodecHeader)
339 else
94340 {
95 headerChunk_ = make_unique<msg::CodecHeader>();
96 headerChunk_->deserialize(baseMessage, buffer);
97
98 LOG(INFO) << "Codec: " << headerChunk_->codec << "\n";
99 decoder_.reset(nullptr);
100 stream_ = nullptr;
101 player_.reset(nullptr);
102
103 if (headerChunk_->codec == "pcm")
104 decoder_ = make_unique<decoder::PcmDecoder>();
105 #if defined(HAS_OGG) && (defined(HAS_TREMOR) || defined(HAS_VORBIS))
106 else if (headerChunk_->codec == "ogg")
107 decoder_ = make_unique<decoder::OggDecoder>();
108 #endif
109 #if defined(HAS_FLAC)
110 else if (headerChunk_->codec == "flac")
111 decoder_ = make_unique<decoder::FlacDecoder>();
112 #endif
113 #if defined(HAS_OPUS)
114 else if (headerChunk_->codec == "opus")
115 decoder_ = make_unique<decoder::OpusDecoder>();
116 #endif
117 else
118 throw SnapException("codec not supported: \"" + headerChunk_->codec + "\"");
119
120 sampleFormat_ = decoder_->setHeader(headerChunk_.get());
121 LOG(NOTICE) << TAG("state") << "sampleformat: " << sampleFormat_.getFormat() << "\n";
122
123 stream_ = make_shared<Stream>(sampleFormat_, settings_.player.sample_format);
124 stream_->setBufferLen(serverSettings_->getBufferMs() - settings_.player.latency);
125
126 const auto& pcm_device = settings_.player.pcm_device;
127 const auto& player_name = settings_.player.player_name;
128 player_ = nullptr;
129 #ifdef HAS_ALSA
130 if (!player_ && (player_name.empty() || (player_name == "alsa")))
131 player_ = make_unique<AlsaPlayer>(pcm_device, stream_);
132 #endif
133 #ifdef HAS_OBOE
134 if (!player_ && (player_name.empty() || (player_name == "oboe")))
135 player_ = make_unique<OboePlayer>(pcm_device, stream_);
136 #endif
137 #ifdef HAS_OPENSL
138 if (!player_ && (player_name.empty() || (player_name == "opensl")))
139 player_ = make_unique<OpenslPlayer>(pcm_device, stream_);
140 #endif
141 #ifdef HAS_COREAUDIO
142 if (!player_ && (player_name.empty() || (player_name == "coreaudio")))
143 player_ = make_unique<CoreAudioPlayer>(pcm_device, stream_);
144 #endif
145 if (!player_)
146 throw SnapException("No audio player support");
147 player_->setVolume(serverSettings_->getVolume() / 100.);
148 player_->setMute(serverSettings_->isMuted());
149 player_->start();
341 clientConnection_ = make_unique<ClientConnection>(io_context_, settings_.server);
342 worker();
150343 }
151 else if (baseMessage.type == message_type::kStreamTags)
152 {
153 if (meta_)
154 {
155 msg::StreamTags streamTags_;
156 streamTags_.deserialize(baseMessage, buffer);
157 meta_->push(streamTags_.msg);
158 }
159 }
160
161 // if (baseMessage.type != message_type::kTime)
162 // if (sendTimeSyncMessage(1000))
163 // LOG(DEBUG) << "time sync onMessageReceived\n";
164 }
165
166
167 bool Controller::sendTimeSyncMessage(const std::chrono::milliseconds& after)
168 {
169 static chronos::time_point_clk lastTimeSync(chronos::clk::now());
170 auto now = chronos::clk::now();
171 if (lastTimeSync + after > now)
172 return false;
173
174 lastTimeSync = now;
175 msg::Time timeReq;
176 clientConnection_->send(&timeReq);
177 return true;
178 }
179
180
181 void Controller::start()
182 {
183 clientConnection_ = make_unique<ClientConnection>(this, settings_.server.host, settings_.server.port);
184 controllerThread_ = thread(&Controller::worker, this);
185 }
186
187
188 void Controller::run()
189 {
190 clientConnection_ = make_unique<ClientConnection>(this, settings_.server.host, settings_.server.port);
191 worker();
192 // controllerThread_ = thread(&Controller::worker, this);
193 }
194
195
196 void Controller::stop()
197 {
198 LOG(DEBUG) << "Stopping Controller" << endl;
199 active_ = false;
200 controllerThread_.join();
201 clientConnection_->stop();
202 }
203
344 }
345
346
347 // void Controller::stop()
348 // {
349 // LOG(DEBUG, LOG_TAG) << "Stopping\n";
350 // timer_.cancel();
351 // }
352
353 void Controller::reconnect()
354 {
355 timer_.cancel();
356 clientConnection_->disconnect();
357 player_.reset();
358 stream_.reset();
359 decoder_.reset();
360 timer_.expires_after(1s);
361 timer_.async_wait([this](const boost::system::error_code& ec) {
362 if (!ec)
363 {
364 worker();
365 }
366 });
367 }
204368
205369 void Controller::worker()
206370 {
207 active_ = true;
208
209 while (active_)
210 {
211 try
212 {
213 clientConnection_->start();
214
371 clientConnection_->connect([this](const boost::system::error_code& ec) {
372 if (!ec)
373 {
374 // LOG(INFO, LOG_TAG) << "Connected!\n";
215375 string macAddress = clientConnection_->getMacAddress();
216376 if (settings_.host_id.empty())
217377 settings_.host_id = ::getHostId(macAddress);
218378
219 /// Say hello to the server
220 msg::Hello hello(macAddress, settings_.host_id, settings_.instance);
221 clientConnection_->send(&hello);
222
223 /// Do initial time sync with the server
224 msg::Time timeReq;
225 for (size_t n = 0; n < 50 && active_; ++n)
226 {
227 if (async_exception_)
228 {
229 LOG(DEBUG) << "Async exception\n";
230 std::rethrow_exception(async_exception_);
231 }
232
233 auto reply = clientConnection_->sendReq<msg::Time>(&timeReq, chronos::msec(2000));
234 if (reply)
235 {
236 TimeProvider::getInstance().setDiff(reply->latency, reply->received - reply->sent);
237 chronos::usleep(100);
238 }
239 }
240 LOG(INFO) << "diff to server [ms]: " << (float)TimeProvider::getInstance().getDiffToServer<chronos::usec>().count() / 1000.f << "\n";
241
242 /// Main loop
243 while (active_)
244 {
245 if (async_exception_)
246 {
247 LOG(DEBUG) << "Async exception\n";
248 std::rethrow_exception(async_exception_);
249 }
250
251 if (sendTimeSyncMessage(1000ms))
252 LOG(DEBUG) << "time sync main loop\n";
253 this_thread::sleep_for(100ms);
254 }
255 }
256 catch (const std::exception& e)
257 {
258 async_exception_ = nullptr;
259 SLOG(ERROR) << "Exception in Controller::worker(): " << e.what() << endl;
260 clientConnection_->stop();
261 player_.reset();
262 stream_.reset();
263 decoder_.reset();
264 for (size_t n = 0; (n < 10) && active_; ++n)
265 chronos::sleep(100);
266 }
267 }
268 LOG(DEBUG) << "Thread stopped\n";
269 }
379 // Say hello to the server
380 auto hello = std::make_shared<msg::Hello>(macAddress, settings_.host_id, settings_.instance);
381 clientConnection_->sendRequest<msg::ServerSettings>(
382 hello, 2s, [this](const boost::system::error_code& ec, std::unique_ptr<msg::ServerSettings> response) mutable {
383 if (ec)
384 {
385 LOG(ERROR, LOG_TAG) << "Failed to send hello request, error: " << ec.message() << "\n";
386 reconnect();
387 return;
388 }
389 else
390 {
391 serverSettings_ = std::move(response);
392 LOG(INFO, LOG_TAG) << "ServerSettings - buffer: " << serverSettings_->getBufferMs() << ", latency: " << serverSettings_->getLatency()
393 << ", volume: " << serverSettings_->getVolume() << ", muted: " << serverSettings_->isMuted() << "\n";
394 }
395 });
396
397 // Do initial time sync with the server
398 sendTimeSyncMessage(50);
399 // Start receiver loop
400 getNextMessage();
401 }
402 else
403 {
404 LOG(ERROR, LOG_TAG) << "Error: " << ec.message() << "\n";
405 reconnect();
406 }
407 });
408 }
1818 #ifndef CONTROLLER_H
1919 #define CONTROLLER_H
2020
21 #include "client_connection.hpp"
22 #include "client_settings.hpp"
2123 #include "decoder/decoder.hpp"
2224 #include "message/message.hpp"
2325 #include "message/server_settings.hpp"
2426 #include "message/stream_tags.hpp"
27 #include "metadata.hpp"
28 #include "player/player.hpp"
29 #include "stream.hpp"
2530 #include <atomic>
2631 #include <thread>
27 #ifdef HAS_ALSA
28 #include "player/alsa_player.hpp"
29 #endif
30 #ifdef HAS_OPENSL
31 #include "player/opensl_player.hpp"
32 #endif
33 #ifdef HAS_OBOE
34 #include "player/oboe_player.hpp"
35 #endif
36 #ifdef HAS_COREAUDIO
37 #include "player/coreaudio_player.hpp"
38 #endif
39 #include "client_connection.hpp"
40 #include "client_settings.hpp"
41 #include "metadata.hpp"
42 #include "stream.hpp"
4332
4433 using namespace std::chrono_literals;
4534
5039 * Decodes audio (message_type::kWireChunk) and feeds PCM to the audio stream buffer
5140 * Does timesync with the server
5241 */
53 class Controller : public MessageReceiver
42 class Controller
5443 {
5544 public:
56 Controller(const ClientSettings& settings, std::unique_ptr<MetadataAdapter> meta);
45 Controller(boost::asio::io_context& io_context, const ClientSettings& settings, std::unique_ptr<MetadataAdapter> meta);
5746 void start();
58 void run();
59 void stop();
60
61 /// Implementation of MessageReceiver.
62 /// ClientConnection passes messages from the server through these callbacks
63 void onMessageReceived(ClientConnection* connection, const msg::BaseMessage& baseMessage, char* buffer) override;
64
65 /// Implementation of MessageReceiver.
66 /// Used for async exception reporting
67 void onException(ClientConnection* connection, std::exception_ptr exception) override;
47 // void stop();
48 static std::vector<std::string> getSupportedPlayerNames();
6849
6950 private:
51 using MdnsHandler = std::function<void(const boost::system::error_code& ec, const std::string& host, uint16_t port)>;
7052 void worker();
71 bool sendTimeSyncMessage(const std::chrono::milliseconds& after = 1000ms);
53 void reconnect();
54 void browseMdns(const MdnsHandler& handler);
55
56 template <typename PlayerType>
57 std::unique_ptr<Player> createPlayer(ClientSettings::Player& settings, const std::string& player_name);
58
59 void getNextMessage();
60 void sendTimeSyncMessage(int quick_syncs);
61
62 boost::asio::io_context& io_context_;
63 boost::asio::steady_timer timer_;
7264 ClientSettings settings_;
7365 std::string meta_callback_;
74 std::atomic<bool> active_;
75 std::thread controllerThread_;
7666 SampleFormat sampleFormat_;
7767 std::unique_ptr<ClientConnection> clientConnection_;
7868 std::shared_ptr<Stream> stream_;
8171 std::unique_ptr<MetadataAdapter> meta_;
8272 std::unique_ptr<msg::ServerSettings> serverSettings_;
8373 std::unique_ptr<msg::CodecHeader> headerChunk_;
84 std::mutex receiveMutex_;
85
86 std::exception_ptr async_exception_;
8774 };
8875
8976
2626
2727 using namespace std;
2828
29 static constexpr auto LOG_TAG = "FlacDecoder";
30
2931 namespace decoder
3032 {
3133
8284
8385 if (lastError_)
8486 {
85 LOG(ERROR) << "FLAC decode error: " << FLAC__StreamDecoderErrorStatusString[*lastError_] << "\n";
87 LOG(ERROR, LOG_TAG) << "FLAC decode error: " << FLAC__StreamDecoderErrorStatusString[*lastError_] << "\n";
8688 lastError_ = nullptr;
8789 return false;
8890 }
9294 {
9395 double diffMs = static_cast<double>(cacheInfo_.cachedBlocks_) / (static_cast<double>(cacheInfo_.sampleRate_) / 1000.);
9496 auto us = static_cast<uint64_t>(diffMs * 1000.);
95 tv diff(us / 1000000, us % 1000000);
96 LOG(DEBUG) << "Cached: " << cacheInfo_.cachedBlocks_ << ", " << diffMs << "ms, " << diff.sec << "s, " << diff.usec << "us\n";
97 tv diff(static_cast<int32_t>(us / 1000000), static_cast<int32_t>(us % 1000000));
98 LOG(TRACE, LOG_TAG) << "Cached: " << cacheInfo_.cachedBlocks_ << ", " << diffMs << "ms, " << diff.sec << "s, " << diff.usec << "us\n";
9799 chunk->timestamp = chunk->timestamp - diff;
98100 }
99101 return true;
143145
144146 memcpy(buffer, flacChunk->payload, *bytes);
145147 memmove(flacChunk->payload, flacChunk->payload + *bytes, flacChunk->payloadSize - *bytes);
146 flacChunk->payloadSize = flacChunk->payloadSize - *bytes;
148 flacChunk->payloadSize = flacChunk->payloadSize - static_cast<uint32_t>(*bytes);
147149 flacChunk->payload = (char*)realloc(flacChunk->payload, flacChunk->payloadSize);
148150 }
149151 return FLAC__STREAM_DECODER_READ_STATUS_CONTINUE;
167169 {
168170 if (buffer[channel] == nullptr)
169171 {
170 SLOG(ERROR) << "ERROR: buffer[" << channel << "] is NULL\n";
172 LOG(ERROR, LOG_TAG) << "ERROR: buffer[" << channel << "] is NULL\n";
171173 return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT;
172174 }
173175
190192 chunkBuffer[sampleFormat.channels() * i + channel] = SWAP_32((int32_t)(buffer[channel][i]));
191193 }
192194 }
193 pcmChunk->payloadSize += bytes;
195 pcmChunk->payloadSize += static_cast<uint32_t>(bytes);
194196 }
195197
196198 return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
210212
211213 void error_callback(const FLAC__StreamDecoder* /*decoder*/, FLAC__StreamDecoderErrorStatus status, void* client_data)
212214 {
213 SLOG(ERROR) << "Got error callback: " << FLAC__StreamDecoderErrorStatusString[status] << "\n";
215 LOG(ERROR, LOG_TAG) << "Got error callback: " << FLAC__StreamDecoderErrorStatusString[status] << "\n";
214216 static_cast<FlacDecoder*>(client_data)->lastError_ = std::make_unique<FLAC__StreamDecoderErrorStatus>(status);
215217 }
216218 } // namespace callback
1616 ***/
1717
1818 #include <cmath>
19 #include <cstdint>
1920 #include <cstring>
2021 #include <iostream>
2122
2627
2728
2829 using namespace std;
30
31 static constexpr auto LOG_TAG = "OpusDecoder";
2932
3033 namespace decoder
3134 {
7275 if (result < 0)
7376 {
7477 /* missing or corrupt data at this page position */
75 LOG(ERROR) << "Corrupt or missing data in bitstream; continuing...\n";
78 LOG(ERROR, LOG_TAG) << "Corrupt or missing data in bitstream; continuing...\n";
7679 continue;
7780 }
7881
103106 (-1.<=range<=1.) to whatever PCM format and write it out */
104107 while ((samples = vorbis_synthesis_pcmout(&vd, &pcm)) > 0)
105108 {
106 size_t bytes = sampleFormat_.sampleSize() * vi.channels * samples;
109 uint32_t bytes = sampleFormat_.sampleSize() * vi.channels * samples;
107110 chunk->payload = (char*)realloc(chunk->payload, chunk->payloadSize + bytes);
108111 for (int channel = 0; channel < vi.channels; ++channel)
109112 {
114117 {
115118 int8_t& val = chunkBuffer[sampleFormat_.channels() * i + channel];
116119 #ifdef HAS_TREMOR
117 val = clip<int8_t>(pcm[channel][i], -128, 127);
118 #else
119 val = clip<int8_t>(floor(pcm[channel][i] * 127.f + .5f), -128, 127);
120 val = clip<int8_t>(pcm[channel][i], INT8_MIN, INT8_MAX);
121 #else
122 val = clip<int8_t>(floor(pcm[channel][i] * 127.f + .5f), INT8_MIN, INT8_MAX);
120123 #endif
121124 }
122125 }
127130 {
128131 int16_t& val = chunkBuffer[sampleFormat_.channels() * i + channel];
129132 #ifdef HAS_TREMOR
130 val = SWAP_16(clip<int16_t>(pcm[channel][i] >> 9, -32768, 32767));
131 #else
132 val = SWAP_16(clip<int16_t>(floor(pcm[channel][i] * 32767.f + .5f), -32768, 32767));
133 val = SWAP_16(clip<int16_t>(pcm[channel][i] >> 9, INT16_MIN, INT16_MAX));
134 #else
135 val = SWAP_16(clip<int16_t>(floor(pcm[channel][i] * 32767.f + .5f), INT16_MIN, INT16_MAX));
133136 #endif
134137 }
135138 }
140143 {
141144 int32_t& val = chunkBuffer[sampleFormat_.channels() * i + channel];
142145 #ifdef HAS_TREMOR
143 val = SWAP_32(clip<int32_t>(pcm[channel][i] << 7, -2147483648, 2147483647));
144 #else
145 val = SWAP_32(clip<int32_t>(floor(pcm[channel][i] * 2147483647.f + .5f), -2147483648, 2147483647));
146 val = SWAP_32(clip<int32_t>(pcm[channel][i] << 7, INT32_MIN, INT32_MAX));
147 #else
148 val = SWAP_32(clip<int32_t>(floor(pcm[channel][i] * 2147483647.f + .5f), INT32_MIN, INT32_MAX));
146149 #endif
147150 }
148151 }
230233 std::string comment(*ptr);
231234 if (comment.find("SAMPLE_FORMAT=") == 0)
232235 sampleFormat_.setFormat(comment.substr(comment.find("=") + 1));
233 LOG(INFO) << "comment: " << comment << "\n";
236 LOG(INFO, LOG_TAG) << "comment: " << comment << "\n";
234237 ;
235238 ++ptr;
236239 }
237240
238 LOG(INFO) << "Encoded by: " << vc.vendor << "\n";
241 LOG(INFO, LOG_TAG) << "Encoded by: " << vc.vendor << "\n";
239242
240243 return sampleFormat_;
241244 }
3838
3939 private:
4040 bool decodePayload(msg::PcmChunk* chunk);
41 template <typename T>
42 T clip(const T& value, const T& lower, const T& upper) const
41 template <typename T, typename IN_TYPE>
42 T clip(const IN_TYPE& value, const T& lower, const T& upper) const
4343 {
44 if (value > upper)
44 auto val = static_cast<int64_t>(value);
45 if (val > upper)
4546 return upper;
46 if (value < lower)
47 if (val < lower)
4748 return lower;
48 return value;
49 return static_cast<T>(value);
4950 }
5051
5152 ogg_sync_state oy; /// sync and verify incoming physical bitstream
3131 /// 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.
3232 static constexpr int const_max_frame_size = 2880;
3333
34 static constexpr auto LOG_TAG = "OpusDecoder";
3435
3536 OpusDecoder::OpusDecoder() : Decoder(), dec_(nullptr)
3637 {
4748
4849 bool OpusDecoder::decode(msg::PcmChunk* chunk)
4950 {
50 int frame_size = 0;
51 int decoded_frames = 0;
5152
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)
53 while ((decoded_frames = opus_decode(dec_, (unsigned char*)chunk->payload, chunk->payloadSize, pcm_.data(),
54 static_cast<int>(pcm_.size()) / sample_format_.channels(), 0)) == OPUS_BUFFER_TOO_SMALL)
5455 {
5556 if (pcm_.size() < const_max_frame_size * sample_format_.channels())
5657 {
5758 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 LOG(DEBUG, LOG_TAG) << "OPUS encoding buffer too small, resizing to " << pcm_.size() / sample_format_.channels() << " samples per channel\n";
5960 }
6061 else
6162 break;
6263 }
6364
64 if (frame_size < 0)
65 if (decoded_frames < 0)
6566 {
66 LOG(ERROR) << "Failed to decode chunk: " << opus_strerror(frame_size) << ", IN size: " << chunk->payloadSize << ", OUT size: " << pcm_.size() << '\n';
67 LOG(ERROR, LOG_TAG) << "Failed to decode chunk: " << opus_strerror(decoded_frames) << ", IN size: " << chunk->payloadSize
68 << ", OUT size: " << pcm_.size() << '\n';
6769 return false;
6870 }
6971 else
7072 {
71 LOG(DEBUG) << "Decoded chunk: size " << chunk->payloadSize << " bytes, decoded " << frame_size << " samples" << '\n';
73 LOG(TRACE, LOG_TAG) << "Decode chunk: " << decoded_frames << " frames, size: " << chunk->payloadSize
74 << " bytes, decoded: " << decoded_frames * sample_format_.frameSize() << " bytes\n";
7275
7376 // copy encoded data to chunk
74 chunk->payloadSize = frame_size * sample_format_.channels() * sizeof(opus_int16);
77 chunk->payloadSize = decoded_frames * sample_format_.frameSize(); // decoded_frames * sample_format_.channels() * sizeof(opus_int16);
7578 chunk->payload = (char*)realloc(chunk->payload, chunk->payloadSize);
7679 memcpy(chunk->payload, (char*)pcm_.data(), chunk->payloadSize);
7780 return true;
9396
9497 // decode the sampleformat
9598 uint32_t rate;
96 memcpy(&rate, chunk->payload + 4, sizeof(id_opus));
99 memcpy(&rate, chunk->payload + 4, sizeof(rate));
97100 uint16_t bits;
98101 memcpy(&bits, chunk->payload + 8, sizeof(bits));
99102 uint16_t channels;
100103 memcpy(&channels, chunk->payload + 10, sizeof(channels));
101104
102105 sample_format_.setFormat(SWAP_32(rate), SWAP_16(bits), SWAP_16(channels));
103 LOG(DEBUG) << "Opus sampleformat: " << sample_format_.getFormat() << "\n";
106 LOG(DEBUG, LOG_TAG) << "Opus sampleformat: " << sample_format_.toString() << "\n";
104107
105108 // create the decoder
106109 int error;
1919 #define DOUBLE_BUFFER_H
2020
2121 #include <algorithm>
22 #include <array>
2223 #include <deque>
2324
2425
5051 }
5152
5253 /// Median as mean over N values around the median
53 T median(unsigned int mean = 1) const
54 T median(uint16_t mean = 1) const
5455 {
5556 if (buffer.empty())
5657 return 0;
6061 return tmpBuffer[tmpBuffer.size() / 2];
6162 else
6263 {
63 unsigned int low = tmpBuffer.size() / 2;
64 unsigned int high = low;
64 uint16_t low = static_cast<uint16_t>(tmpBuffer.size()) / 2;
65 uint16_t high = low;
6566 low -= mean / 2;
6667 high += mean / 2;
6768 T result((T)0);
68 for (unsigned int i = low; i <= high; ++i)
69 for (uint16_t i = low; i <= high; ++i)
6970 {
7071 result += tmpBuffer[i];
7172 }
8990 return 0;
9091 std::deque<T> tmpBuffer(buffer.begin(), buffer.end());
9192 std::sort(tmpBuffer.begin(), tmpBuffer.end());
92 return tmpBuffer[(size_t)(tmpBuffer.size() * ((float)percentile / (float)100))];
93 return tmpBuffer[(size_t)((tmpBuffer.size() - 1) * ((float)percentile / (float)100))];
94 }
95
96 template <std::size_t Size>
97 std::array<T, Size> percentiles(std::array<uint8_t, Size> percentiles) const
98 {
99 std::array<T, Size> result;
100 result.fill(0);
101 if (buffer.empty())
102 return result;
103 std::deque<T> tmpBuffer(buffer.begin(), buffer.end());
104 std::sort(tmpBuffer.begin(), tmpBuffer.end());
105 for (std::size_t i = 0; i < Size; ++i)
106 result[i] = tmpBuffer[(size_t)((tmpBuffer.size() - 1) * ((float)percentiles[i] / (float)100))];
107
108 return result;
93109 }
94110
95111 inline bool full() const
66 <key>ProgramArguments</key>
77 <array>
88 <string>/usr/local/bin/snapclient</string>
9 <string>--logsink=system</string>
910 <!-- <string>-d</string> -->
1011 </array>
1112 <key>RunAtLoad</key>
1919 #include "common/aixlog.hpp"
2020 #include "common/snap_exception.hpp"
2121 #include "common/str_compat.hpp"
22 #include "common/utils/string_utils.hpp"
2223
2324 //#define BUFFER_TIME 120000
24 #define PERIOD_TIME 30000
25 #define PERIOD_TIME 15000
26 #define exp10(x) (exp((x)*log(10)))
2527
2628 using namespace std;
2729
2830 static constexpr auto LOG_TAG = "Alsa";
29
30 AlsaPlayer::AlsaPlayer(const PcmDevice& pcmDevice, std::shared_ptr<Stream> stream) : Player(pcmDevice, stream), handle_(nullptr)
31 {
31 static constexpr auto DEFAULT_MIXER = "PCM";
32
33
34 AlsaPlayer::AlsaPlayer(boost::asio::io_context& io_context, const ClientSettings::Player& settings, std::shared_ptr<Stream> stream)
35 : Player(io_context, settings, stream), handle_(nullptr), ctl_(nullptr), mixer_(nullptr), elem_(nullptr), sd_(io_context), timer_(io_context)
36 {
37 if (settings_.mixer.mode == ClientSettings::Mixer::Mode::hardware)
38 {
39 string tmp;
40 if (settings_.mixer.parameter.empty())
41 mixer_name_ = DEFAULT_MIXER;
42 else
43 mixer_name_ = utils::string::split_left(settings_.mixer.parameter, ':', tmp);
44
45 string card;
46 // default:CARD=ALSA[,DEV=x] => default
47 mixer_device_ = utils::string::split_left(settings_.pcm_device.name, ':', card);
48 if (!card.empty())
49 {
50 auto pos = card.find("CARD=");
51 if (pos != string::npos)
52 {
53 card = card.substr(pos + 5);
54 card = utils::string::split_left(card, ',', tmp);
55 int card_idx = snd_card_get_index(card.c_str());
56 if ((card_idx >= 0) && (card_idx < 32))
57 mixer_device_ = "hw:" + std::to_string(card_idx);
58 }
59 }
60
61 LOG(DEBUG, LOG_TAG) << "Mixer: " << mixer_name_ << ", device: " << mixer_device_ << "\n";
62 }
63 }
64
65
66 void AlsaPlayer::setHardwareVolume(double volume, bool muted)
67 {
68 std::lock_guard<std::recursive_mutex> lock(mutex_);
69 if (elem_ == nullptr)
70 return;
71
72 last_change_ = std::chrono::steady_clock::now();
73 try
74 {
75 int val = muted ? 0 : 1;
76 int err = snd_mixer_selem_set_playback_switch_all(elem_, val);
77 if (err < 0)
78 LOG(ERROR, LOG_TAG) << "Failed to mute, error: " << snd_strerror(err) << "\n";
79
80 long minv, maxv;
81 if ((err = snd_mixer_selem_get_playback_dB_range(elem_, &minv, &maxv)) == 0)
82 {
83 double min_norm = exp10((minv - maxv) / 6000.0);
84 volume = volume * (1 - min_norm) + min_norm;
85 double mixer_volume = 6000.0 * log10(volume) + maxv;
86
87 LOG(DEBUG, LOG_TAG) << "Mixer playback dB range [" << minv << ", " << maxv << "], volume: " << volume << ", mixer volume: " << mixer_volume << "\n";
88 if ((err = snd_mixer_selem_set_playback_dB_all(elem_, mixer_volume, 0)) < 0)
89 throw SnapException(std::string("Failed to set playback volume, error: ") + snd_strerror(err));
90 }
91 else
92 {
93 if ((err = snd_mixer_selem_get_playback_volume_range(elem_, &minv, &maxv)) < 0)
94 throw SnapException(std::string("Failed to get playback volume range, error: ") + snd_strerror(err));
95
96 auto mixer_volume = volume * (maxv - minv) + minv;
97 LOG(DEBUG, LOG_TAG) << "Mixer playback volume range [" << minv << ", " << maxv << "], volume: " << volume << ", mixer volume: " << mixer_volume
98 << "\n";
99 if ((err = snd_mixer_selem_set_playback_volume_all(elem_, mixer_volume)) < 0)
100 throw SnapException(std::string("Failed to set playback volume, error: ") + snd_strerror(err));
101 }
102 }
103 catch (const std::exception& e)
104 {
105 LOG(ERROR, LOG_TAG) << "Exception: " << e.what() << "\n";
106 uninitMixer();
107 }
108 }
109
110
111 bool AlsaPlayer::getHardwareVolume(double& volume, bool& muted)
112 {
113 try
114 {
115 std::lock_guard<std::recursive_mutex> lock(mutex_);
116 if (elem_ == nullptr)
117 throw SnapException("Mixer not initialized");
118
119 long vol;
120 int err = 0;
121 while (snd_mixer_handle_events(mixer_) > 0)
122 this_thread::sleep_for(1us);
123 long minv, maxv;
124 if ((err = snd_mixer_selem_get_playback_dB_range(elem_, &minv, &maxv)) == 0)
125 {
126 if ((err = snd_mixer_selem_get_playback_dB(elem_, SND_MIXER_SCHN_MONO, &vol)) < 0)
127 throw SnapException(std::string("Failed to get playback volume, error: ") + snd_strerror(err));
128
129 volume = pow(10, (vol - maxv) / 6000.0);
130 if (minv != SND_CTL_TLV_DB_GAIN_MUTE)
131 {
132 double min_norm = pow(10, (minv - maxv) / 6000.0);
133 volume = (volume - min_norm) / (1 - min_norm);
134 }
135 }
136 else
137 {
138 if ((err = snd_mixer_selem_get_playback_volume_range(elem_, &minv, &maxv)) < 0)
139 throw SnapException(std::string("Failed to get playback volume range, error: ") + snd_strerror(err));
140 if ((err = snd_mixer_selem_get_playback_volume(elem_, SND_MIXER_SCHN_MONO, &vol)) < 0)
141 throw SnapException(std::string("Failed to get playback volume, error: ") + snd_strerror(err));
142
143 vol -= minv;
144 maxv = maxv - minv;
145 volume = static_cast<double>(vol) / static_cast<double>(maxv);
146 }
147 int val;
148 if ((err = snd_mixer_selem_get_playback_switch(elem_, SND_MIXER_SCHN_MONO, &val)) < 0)
149 throw SnapException(std::string("Failed to get mute state, error: ") + snd_strerror(err));
150 muted = (val == 0);
151 LOG(DEBUG, LOG_TAG) << "Get volume, mixer volume range [" << minv << ", " << maxv << "], volume: " << volume << ", muted: " << muted << "\n";
152 snd_mixer_handle_events(mixer_);
153 return true;
154 }
155 catch (const std::exception& e)
156 {
157 LOG(ERROR, LOG_TAG) << "Exception: " << e.what() << "\n";
158 return false;
159 }
160 }
161
162
163 void AlsaPlayer::waitForEvent()
164 {
165 sd_.async_wait(boost::asio::posix::stream_descriptor::wait_read, [this](const boost::system::error_code& ec) {
166 if (ec)
167 {
168 // TODO: fd is "Bad" after unplugging/plugging USB DAC, i.e. after init/uninit/init cycle
169 LOG(DEBUG, LOG_TAG) << "waitForEvent error: " << ec.message() << "\n";
170 return;
171 }
172
173 std::lock_guard<std::recursive_mutex> lock(mutex_);
174 if (ctl_ == nullptr)
175 return;
176
177 unsigned short revents;
178 snd_ctl_poll_descriptors_revents(ctl_, fd_.get(), 1, &revents);
179 if (revents & POLLIN || (revents == 0))
180 {
181 snd_ctl_event_t* event;
182 snd_ctl_event_alloca(&event);
183
184 if (((snd_ctl_read(ctl_, event) >= 0) && (snd_ctl_event_get_type(event) == SND_CTL_EVENT_ELEM)) || (revents == 0))
185 {
186 auto now = std::chrono::steady_clock::now();
187 if (now - last_change_ < 1s)
188 {
189 LOG(DEBUG, LOG_TAG) << "Last volume change by server: " << std::chrono::duration_cast<std::chrono::milliseconds>(now - last_change_).count()
190 << " ms => ignoring volume change\n";
191 waitForEvent();
192 return;
193 }
194 // Sometimes the old volume is reported after this event has been raised.
195 // As workaround we defer getting the volume by 20ms.
196 timer_.cancel();
197 timer_.expires_after(20ms);
198 timer_.async_wait([this](const boost::system::error_code& ec) {
199 if (!ec)
200 {
201 double volume;
202 bool muted;
203 if (getHardwareVolume(volume, muted))
204 {
205 LOG(DEBUG, LOG_TAG) << "Volume: " << volume << ", muted: " << muted << "\n";
206 notifyVolumeChange(volume, muted);
207 }
208 }
209 });
210 }
211 }
212 waitForEvent();
213 });
214 }
215
216 void AlsaPlayer::initMixer()
217 {
218 if (settings_.mixer.mode != ClientSettings::Mixer::Mode::hardware)
219 return;
220
221 LOG(DEBUG, LOG_TAG) << "initMixer\n";
222 std::lock_guard<std::recursive_mutex> lock(mutex_);
223 int err;
224 if ((err = snd_ctl_open(&ctl_, mixer_device_.c_str(), SND_CTL_READONLY)) < 0)
225 throw SnapException("Can't open control for " + mixer_device_ + ", error: " + snd_strerror(err));
226 if ((err = snd_ctl_subscribe_events(ctl_, 1)) < 0)
227 throw SnapException("Can't subscribe for events for " + mixer_device_ + ", error: " + snd_strerror(err));
228 fd_ = std::unique_ptr<pollfd, std::function<void(pollfd*)>>(new pollfd(), [](pollfd* p) {
229 close(p->fd);
230 delete p;
231 });
232 err = snd_ctl_poll_descriptors(ctl_, fd_.get(), 1);
233 LOG(DEBUG, LOG_TAG) << "Filled " << err << " poll descriptors, poll descriptor count: " << snd_ctl_poll_descriptors_count(ctl_) << ", fd: " << fd_->fd
234 << "\n";
235
236 snd_mixer_selem_id_t* sid;
237 snd_mixer_selem_id_alloca(&sid);
238 int mix_index = 0;
239 // sets simple-mixer index and name
240 snd_mixer_selem_id_set_index(sid, mix_index);
241 snd_mixer_selem_id_set_name(sid, mixer_name_.c_str());
242
243 if ((err = snd_mixer_open(&mixer_, 0)) < 0)
244 throw SnapException(std::string("Failed to open mixer, error: ") + snd_strerror(err));
245 if ((err = snd_mixer_attach(mixer_, mixer_device_.c_str())) < 0)
246 throw SnapException("Failed to attach mixer to " + mixer_device_ + ", error: " + snd_strerror(err));
247 if ((err = snd_mixer_selem_register(mixer_, NULL, NULL)) < 0)
248 throw SnapException(std::string("Failed to register selem, error: ") + snd_strerror(err));
249 if ((err = snd_mixer_load(mixer_)) < 0)
250 throw SnapException(std::string("Failed to load mixer, error: ") + snd_strerror(err));
251 elem_ = snd_mixer_find_selem(mixer_, sid);
252 if (!elem_)
253 throw SnapException("Failed to find mixer: " + mixer_name_);
254
255 sd_ = boost::asio::posix::stream_descriptor(io_context_, fd_->fd);
256 waitForEvent();
32257 }
33258
34259
35260 void AlsaPlayer::initAlsa()
36261 {
262 std::lock_guard<std::recursive_mutex> lock(mutex_);
37263 unsigned int tmp, rate;
38 int pcm, channels;
264 int err, channels;
39265 snd_pcm_hw_params_t* params;
40266
41267 const SampleFormat& format = stream_->getFormat();
43269 channels = format.channels();
44270
45271 /* Open the PCM device in playback mode */
46 if ((pcm = snd_pcm_open(&handle_, pcmDevice_.name.c_str(), SND_PCM_STREAM_PLAYBACK, 0)) < 0)
47 throw SnapException("Can't open " + pcmDevice_.name + " PCM device: " + snd_strerror(pcm));
272 if ((err = snd_pcm_open(&handle_, settings_.pcm_device.name.c_str(), SND_PCM_STREAM_PLAYBACK, 0)) < 0)
273 throw SnapException("Can't open " + settings_.pcm_device.name + ", error: " + snd_strerror(err), err);
48274
49275 /* struct snd_pcm_playback_info_t pinfo;
50276 if ( (pcm = snd_pcm_playback_info( pcm_handle, &pinfo )) < 0 )
54280 /* Allocate parameters object and fill it with default values*/
55281 snd_pcm_hw_params_alloca(&params);
56282
57 if ((pcm = snd_pcm_hw_params_any(handle_, params)) < 0)
58 throw SnapException("Can't fill params: " + string(snd_strerror(pcm)));
283 if ((err = snd_pcm_hw_params_any(handle_, params)) < 0)
284 throw SnapException("Can't fill params: " + string(snd_strerror(err)));
59285
60286 /* Set parameters */
61 if ((pcm = snd_pcm_hw_params_set_access(handle_, params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0)
62 throw SnapException("Can't set interleaved mode: " + string(snd_strerror(pcm)));
287 if ((err = snd_pcm_hw_params_set_access(handle_, params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0)
288 throw SnapException("Can't set interleaved mode: " + string(snd_strerror(err)));
63289
64290 snd_pcm_format_t snd_pcm_format;
65291 if (format.bits() == 8)
73299 else
74300 throw SnapException("Unsupported sample format: " + cpt::to_string(format.bits()));
75301
76 pcm = snd_pcm_hw_params_set_format(handle_, params, snd_pcm_format);
77 if (pcm == -EINVAL)
302 err = snd_pcm_hw_params_set_format(handle_, params, snd_pcm_format);
303 if (err == -EINVAL)
78304 {
79305 if (snd_pcm_format == SND_PCM_FORMAT_S24_LE)
80306 {
87313 }
88314 }
89315
90 pcm = snd_pcm_hw_params_set_format(handle_, params, snd_pcm_format);
91 if (pcm < 0)
92 {
93 cerr << "error: " << pcm << "\n";
316 err = snd_pcm_hw_params_set_format(handle_, params, snd_pcm_format);
317 if (err < 0)
318 {
94319 stringstream ss;
95 ss << "Can't set format: " << string(snd_strerror(pcm)) << ", supported: ";
320 ss << "Can't set format: " << string(snd_strerror(err)) << ", supported: ";
96321 for (int format = 0; format <= (int)SND_PCM_FORMAT_LAST; format++)
97322 {
98323 snd_pcm_format_t snd_pcm_format = static_cast<snd_pcm_format_t>(format);
102327 throw SnapException(ss.str());
103328 }
104329
105 if ((pcm = snd_pcm_hw_params_set_channels(handle_, params, channels)) < 0)
106 throw SnapException("Can't set channels number: " + string(snd_strerror(pcm)));
107
108 if ((pcm = snd_pcm_hw_params_set_rate_near(handle_, params, &rate, nullptr)) < 0)
109 throw SnapException("Can't set rate: " + string(snd_strerror(pcm)));
330 if ((err = snd_pcm_hw_params_set_channels(handle_, params, channels)) < 0)
331 throw SnapException("Can't set channel count: " + string(snd_strerror(err)));
332
333 if ((err = snd_pcm_hw_params_set_rate_near(handle_, params, &rate, nullptr)) < 0)
334 throw SnapException("Can't set rate: " + string(snd_strerror(err)));
110335
111336 unsigned int period_time;
112337 snd_pcm_hw_params_get_period_time_max(params, &period_time, nullptr);
123348 // LOG(ERROR, LOG_TAG) << "Unable to set buffer size " << (long int)periodsize << ": " << snd_strerror(pcm) << "\n";
124349
125350 /* Write parameters */
126 if ((pcm = snd_pcm_hw_params(handle_, params)) < 0)
127 throw SnapException("Can't set hardware parameters: " + string(snd_strerror(pcm)));
351 if ((err = snd_pcm_hw_params(handle_, params)) < 0)
352 throw SnapException("Can't set hardware parameters: " + string(snd_strerror(err)));
128353
129354 /* Resume information */
130355 LOG(DEBUG, LOG_TAG) << "PCM name: " << snd_pcm_name(handle_) << "\n";
137362
138363 /* Allocate buffer to hold single period */
139364 snd_pcm_hw_params_get_period_size(params, &frames_, nullptr);
140 LOG(INFO, LOG_TAG) << "frames: " << frames_ << "\n";
365 LOG(DEBUG, LOG_TAG) << "frames: " << frames_ << "\n";
141366
142367 snd_pcm_hw_params_get_period_time(params, &tmp, nullptr);
143368 LOG(DEBUG, LOG_TAG) << "period time: " << tmp << "\n";
150375 snd_pcm_sw_params_set_start_threshold(handle_, swparams, frames_);
151376 // snd_pcm_sw_params_set_stop_threshold(pcm_handle, swparams, frames_);
152377 snd_pcm_sw_params(handle_, swparams);
153 }
154
155
156 void AlsaPlayer::uninitAlsa()
157 {
378
379 if (ctl_ == nullptr)
380 initMixer();
381 }
382
383
384 void AlsaPlayer::uninitAlsa(bool uninit_mixer)
385 {
386 std::lock_guard<std::recursive_mutex> lock(mutex_);
387 if (uninit_mixer)
388 uninitMixer();
389
158390 if (handle_ != nullptr)
159391 {
160 snd_pcm_drain(handle_);
392 snd_pcm_drop(handle_);
161393 snd_pcm_close(handle_);
162394 handle_ = nullptr;
163395 }
164396 }
165397
166398
399 void AlsaPlayer::uninitMixer()
400 {
401 if (settings_.mixer.mode != ClientSettings::Mixer::Mode::hardware)
402 return;
403
404 LOG(DEBUG, LOG_TAG) << "uninitMixer\n";
405 std::lock_guard<std::recursive_mutex> lock(mutex_);
406 if (sd_.is_open())
407 {
408 boost::system::error_code ec;
409 sd_.cancel(ec);
410 }
411 if (ctl_ != nullptr)
412 {
413 snd_ctl_close(ctl_);
414 ctl_ = nullptr;
415 }
416 if (mixer_ != nullptr)
417 {
418 snd_mixer_close(mixer_);
419 mixer_ = nullptr;
420 }
421 fd_ = nullptr;
422 elem_ = nullptr;
423 }
424
425
167426 void AlsaPlayer::start()
168427 {
169 initAlsa();
428 try
429 {
430 initAlsa();
431 }
432 catch (const SnapException& e)
433 {
434 LOG(ERROR, LOG_TAG) << "Exception: " << e.what() << ", code: " << e.code() << "\n";
435 // Accept "Device or ressource busy", the worker loop will retry
436 if (e.code() != -EBUSY)
437 throw;
438 }
439
170440 Player::start();
171441 }
172442
180450 void AlsaPlayer::stop()
181451 {
182452 Player::stop();
183 uninitAlsa();
453 uninitAlsa(true);
454 }
455
456
457 bool AlsaPlayer::needsThread() const
458 {
459 return true;
184460 }
185461
186462
191467 snd_pcm_sframes_t framesAvail;
192468 long lastChunkTick = chronos::getTickCount();
193469 const SampleFormat& format = stream_->getFormat();
194
195470 while (active_)
196471 {
197472 if (handle_ == nullptr)
199474 try
200475 {
201476 initAlsa();
477 // set the hardware volume. It might have changed when we were not initialized
478 if (settings_.mixer.mode == ClientSettings::Mixer::Mode::hardware)
479 setHardwareVolume(volume_, muted_);
202480 }
203481 catch (const std::exception& e)
204482 {
205483 LOG(ERROR, LOG_TAG) << "Exception in initAlsa: " << e.what() << endl;
206484 chronos::sleep(100);
207485 }
486 if (handle_ == nullptr)
487 continue;
208488 }
209489
210490 int wait_result = snd_pcm_wait(handle_, 100);
216496 else if (wait_result < 0)
217497 {
218498 LOG(ERROR, LOG_TAG) << "ERROR. Can't wait for PCM to become ready: " << snd_strerror(wait_result) << "\n";
219 uninitAlsa();
499 uninitAlsa(true);
500 continue;
220501 }
221502 else if (wait_result == 0)
222503 {
252533 }
253534 }
254535
536 if (framesAvail < static_cast<snd_pcm_sframes_t>(frames_))
537 {
538 this_thread::sleep_for(10ms);
539 continue;
540 }
541
542 // LOG(TRACE, LOG_TAG) << "res: " << result << ", framesAvail: " << framesAvail << ", delay: " << framesDelay << ", frames: " << frames_ << "\n";
255543 chronos::usec delay(static_cast<chronos::usec::rep>(1000 * (double)framesDelay / format.msRate()));
256544 // LOG(TRACE, LOG_TAG) << "delay: " << framesDelay << ", delay[ms]: " << delay.count() / 1000 << ", avail: " << framesAvail << "\n";
257545
258546 if (buffer_.size() < static_cast<size_t>(framesAvail * format.frameSize()))
259547 {
260 LOG(INFO, LOG_TAG) << "Resizing buffer from " << buffer_.size() << " to " << framesAvail * format.frameSize() << "\n";
548 LOG(DEBUG, LOG_TAG) << "Resizing buffer from " << buffer_.size() << " to " << framesAvail * format.frameSize() << "\n";
261549 buffer_.resize(framesAvail * format.frameSize());
262550 }
263551 if (stream_->getPlayerChunk(buffer_.data(), delay, framesAvail))
272560 else if (pcm < 0)
273561 {
274562 LOG(ERROR, LOG_TAG) << "ERROR. Can't write to PCM device: " << snd_strerror(pcm) << "\n";
275 uninitAlsa();
563 uninitAlsa(true);
276564 }
277565 }
278566 else
284572 if ((handle_ != nullptr) && (chronos::getTickCount() - lastChunkTick > 5000))
285573 {
286574 LOG(NOTICE, LOG_TAG) << "No chunk received for 5000ms. Closing ALSA.\n";
287 uninitAlsa();
575 uninitAlsa(false);
288576 stream_->clearChunks();
289577 }
290578 }
2929 class AlsaPlayer : public Player
3030 {
3131 public:
32 AlsaPlayer(const PcmDevice& pcmDevice, std::shared_ptr<Stream> stream);
32 AlsaPlayer(boost::asio::io_context& io_context, const ClientSettings::Player& settings, std::shared_ptr<Stream> stream);
3333 ~AlsaPlayer() override;
3434
35 /// Set audio volume in range [0..1]
3635 void start() override;
3736 void stop() override;
3837
4140
4241 protected:
4342 void worker() override;
43 bool needsThread() const override;
4444
4545 private:
46 /// initialize alsa and the mixer (if neccessary)
4647 void initAlsa();
47 void uninitAlsa();
48 /// free alsa and optionally the mixer
49 /// @param uninit_mixer free the mixer
50 void uninitAlsa(bool uninit_mixer);
51
52 void initMixer();
53 void uninitMixer();
54
55 bool getHardwareVolume(double& volume, bool& muted) override;
56 void setHardwareVolume(double volume, bool muted) override;
57
58 void waitForEvent();
4859
4960 snd_pcm_t* handle_;
61 snd_ctl_t* ctl_;
62
63 snd_mixer_t* mixer_;
64 snd_mixer_elem_t* elem_;
65 std::string mixer_name_;
66 std::string mixer_device_;
67
68 std::unique_ptr<pollfd, std::function<void(pollfd*)>> fd_;
5069 std::vector<char> buffer_;
5170 snd_pcm_uframes_t frames_;
71 boost::asio::posix::stream_descriptor sd_;
72 std::chrono::time_point<std::chrono::steady_clock> last_change_;
73 std::recursive_mutex mutex_;
74 boost::asio::steady_timer timer_;
5275 };
5376
5477
2020
2121 #define NUM_BUFFERS 2
2222
23 static constexpr auto LOG_TAG = "CoreAudioPlayer";
2324
2425 // http://stackoverflow.com/questions/4863811/how-to-use-audioqueue-to-play-a-sound-for-mac-osx-in-c
2526 // https://gist.github.com/andormade/1360885
3132 }
3233
3334
34 CoreAudioPlayer::CoreAudioPlayer(const PcmDevice& pcmDevice, std::shared_ptr<Stream> stream) : Player(pcmDevice, stream), ms_(100), pubStream_(stream)
35 CoreAudioPlayer::CoreAudioPlayer(boost::asio::io_context& io_context, const ClientSettings::Player& settings, std::shared_ptr<Stream> stream)
36 : Player(io_context, settings, stream), ms_(100), pubStream_(stream)
3537 {
3638 }
3739
7880 char buf[1024];
7981 theAddress = {kAudioDevicePropertyDeviceName, kAudioDevicePropertyScopeOutput, 0};
8082 AudioObjectGetPropertyData(devids[i], &theAddress, 0, NULL, &maxlen, buf);
81 LOG(DEBUG) << "device: " << i << ", name: " << buf << ", channels: " << channels << "\n";
83 LOG(DEBUG, LOG_TAG) << "device: " << i << ", name: " << buf << ", channels: " << channels << "\n";
8284
8385 result.push_back(PcmDevice(i, buf));
8486 }
9799 size_t bufferedMs = bufferedFrames * 1000 / pubStream_->getFormat().rate() + (ms_ * (NUM_BUFFERS - 1));
98100 /// 15ms DAC delay. Based on trying.
99101 bufferedMs += 15;
100 // LOG(INFO) << "buffered: " << bufferedFrames << ", ms: " << bufferedMs << ", mSampleTime: " << timestamp.mSampleTime << "\n";
102 // LOG(INFO, LOG_TAG) << "buffered: " << bufferedFrames << ", ms: " << bufferedMs << ", mSampleTime: " << timestamp.mSampleTime << "\n";
101103
102104 /// TODO: sometimes this bufferedMS or AudioTimeStamp wraps around 1s (i.e. we're 1s out of sync (behind)) and recovers later on
103105 chronos::usec delay(bufferedMs * 1000);
106108 {
107109 if (chronos::getTickCount() - lastChunkTick > 5000)
108110 {
109 LOG(NOTICE) << "No chunk received for 5000ms. Closing Audio Queue.\n";
111 LOG(NOTICE, LOG_TAG) << "No chunk received for 5000ms. Closing Audio Queue.\n";
110112 uninitAudioQueue(queue);
111113 return;
112114 }
113 // LOG(INFO) << "Failed to get chunk. Playing silence.\n";
115 // LOG(INFO, LOG_TAG) << "Failed to get chunk. Playing silence.\n";
114116 memset(buffer, 0, buff_size_);
115117 }
116118 else
126128 {
127129 uninitAudioQueue(queue);
128130 }
131 }
132
133
134 bool CoreAudioPlayer::needsThread() const
135 {
136 return true;
129137 }
130138
131139
141149 }
142150 catch (const std::exception& e)
143151 {
144 LOG(ERROR) << "Exception in worker: " << e.what() << "\n";
152 LOG(ERROR, LOG_TAG) << "Exception in worker: " << e.what() << "\n";
145153 chronos::sleep(100);
146154 }
147155 }
178186 frames_ = (sampleFormat.rate() * ms_) / 1000;
179187 ms_ = frames_ * 1000 / sampleFormat.rate();
180188 buff_size_ = frames_ * sampleFormat.frameSize();
181 LOG(INFO) << "frames: " << frames_ << ", ms: " << ms_ << ", buffer size: " << buff_size_ << "\n";
189 LOG(INFO, LOG_TAG) << "frames: " << frames_ << ", ms: " << ms_ << ", buffer size: " << buff_size_ << "\n";
182190
183191 AudioQueueBufferRef buffers[NUM_BUFFERS];
184192 for (int i = 0; i < NUM_BUFFERS; i++)
188196 callback(this, queue, buffers[i]);
189197 }
190198
191 LOG(ERROR) << "CoreAudioPlayer::worker\n";
199 LOG(ERROR, LOG_TAG) << "CoreAudioPlayer::worker\n";
192200 AudioQueueCreateTimeline(queue, &timeLine_);
193201 AudioQueueStart(queue, NULL);
194202 CFRunLoopRun();
3636 class CoreAudioPlayer : public Player
3737 {
3838 public:
39 CoreAudioPlayer(const PcmDevice& pcmDevice, std::shared_ptr<Stream> stream);
39 CoreAudioPlayer(boost::asio::io_context& io_context, const ClientSettings::Player& settings, std::shared_ptr<Stream> stream);
4040 virtual ~CoreAudioPlayer();
4141
4242 void playerCallback(AudioQueueRef queue, AudioQueueBufferRef bufferRef);
4343 static std::vector<PcmDevice> pcm_list(void);
4444
4545 protected:
46 virtual void worker();
46 void worker() override;
47 bool needsThread() const override;
48
4749 void initAudioQueue();
4850 void uninitAudioQueue(AudioQueueRef queue);
4951
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 <assert.h>
19 #include <iostream>
20
21 #include "common/aixlog.hpp"
22 #include "common/snap_exception.hpp"
23 #include "common/str_compat.hpp"
24 #include "common/utils/string_utils.hpp"
25 #include "file_player.hpp"
26
27 using namespace std;
28
29 static constexpr auto LOG_TAG = "FilePlayer";
30 static constexpr auto kDefaultBuffer = 50ms;
31
32
33 FilePlayer::FilePlayer(boost::asio::io_context& io_context, const ClientSettings::Player& settings, std::shared_ptr<Stream> stream)
34 : Player(io_context, settings, stream), timer_(io_context), file_(nullptr)
35 {
36 auto params = utils::string::split_pairs(settings.parameter, ',', '=');
37 string filename;
38 if (params.find("filename") != params.end())
39 filename = params["filename"];
40
41 if (filename.empty() || (filename == "stdout"))
42 {
43 file_.reset(stdout, [](auto p) { std::ignore = p; });
44 }
45 else if (filename == "stderr")
46 {
47 file_.reset(stderr, [](auto p) { std::ignore = p; });
48 }
49 else
50 {
51 std::string mode = "w";
52 if (params.find("mode") != params.end())
53 mode = params["mode"];
54 if ((mode != "w") && (mode != "a"))
55 throw SnapException("Mode must be w (write) or a (append)");
56 mode += "b";
57 file_.reset(fopen(filename.c_str(), mode.c_str()), [](auto p) { fclose(p); });
58 if (!file_)
59 throw SnapException("Error opening file: '" + filename + "', error: " + cpt::to_string(errno));
60 }
61 }
62
63
64 FilePlayer::~FilePlayer()
65 {
66 LOG(DEBUG, LOG_TAG) << "Destructor\n";
67 stop();
68 }
69
70
71 bool FilePlayer::needsThread() const
72 {
73 return false;
74 }
75
76
77 void FilePlayer::requestAudio()
78 {
79 auto numFrames = static_cast<uint32_t>(stream_->getFormat().msRate() * kDefaultBuffer.count());
80 auto needed = numFrames * stream_->getFormat().frameSize();
81 if (buffer_.size() < needed)
82 buffer_.resize(needed);
83
84 if (!stream_->getPlayerChunk(buffer_.data(), 10ms, numFrames))
85 {
86 // LOG(INFO, LOG_TAG) << "Failed to get chunk. Playing silence.\n";
87 memset(buffer_.data(), 0, needed);
88 }
89 else
90 {
91 adjustVolume(static_cast<char*>(buffer_.data()), numFrames);
92 }
93 fwrite(buffer_.data(), 1, needed, file_.get());
94 fflush(file_.get());
95 loop();
96 }
97
98
99 void FilePlayer::loop()
100 {
101 next_request_ += kDefaultBuffer;
102 auto now = std::chrono::steady_clock::now();
103 if (next_request_ < now)
104 next_request_ = now + 1ms;
105
106 timer_.expires_at(next_request_);
107 timer_.async_wait([this](boost::system::error_code ec) {
108 if (ec)
109 return;
110 requestAudio();
111 });
112 }
113
114
115 void FilePlayer::start()
116 {
117 next_request_ = std::chrono::steady_clock::now();
118 loop();
119 }
120
121
122 void FilePlayer::stop()
123 {
124 LOG(INFO, LOG_TAG) << "Stop\n";
125 timer_.cancel();
126 }
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 FILE_PLAYER_HPP
19 #define FILE_PLAYER_HPP
20
21 #include "player.hpp"
22 #include <cstdio>
23 #include <memory>
24
25 /// File Player
26 /// Used for testing and doesn't even write the received audio to file at the moment,
27 /// but just discards it
28 class FilePlayer : public Player
29 {
30 public:
31 FilePlayer(boost::asio::io_context& io_context, const ClientSettings::Player& settings, std::shared_ptr<Stream> stream);
32 virtual ~FilePlayer();
33
34 void start() override;
35 void stop() override;
36
37 protected:
38 void requestAudio();
39 void loop();
40 bool needsThread() const override;
41 boost::asio::steady_timer timer_;
42 std::vector<char> buffer_;
43 std::chrono::time_point<std::chrono::steady_clock> next_request_;
44 std::shared_ptr<FILE> file_;
45 };
46
47
48 #endif
2929 static constexpr double kDefaultLatency = 50;
3030
3131
32 OboePlayer::OboePlayer(const PcmDevice& pcmDevice, std::shared_ptr<Stream> stream) : Player(pcmDevice, stream)
32 OboePlayer::OboePlayer(boost::asio::io_context& io_context, const ClientSettings::Player& settings, std::shared_ptr<Stream> stream)
33 : Player(io_context, settings, stream)
3334 {
3435 LOG(DEBUG, LOG_TAG) << "Contructor\n";
3536 LOG(INFO, LOG_TAG) << "Init start\n";
4344 LOG(INFO, LOG_TAG) << "DefaultStreamValues::SampleRate: " << oboe::DefaultStreamValues::SampleRate
4445 << ", DefaultStreamValues::FramesPerBurst: " << oboe::DefaultStreamValues::FramesPerBurst << "\n";
4546
47
48 auto result = openStream();
49 LOG(INFO, LOG_TAG) << "BufferSizeInFrames: " << out_stream_->getBufferSizeInFrames() << ", FramesPerBurst: " << out_stream_->getFramesPerBurst() << "\n";
50 if (result != oboe::Result::OK)
51 LOG(ERROR, LOG_TAG) << "Error building AudioStream: " << oboe::convertToText(result) << "\n";
52 LOG(INFO, LOG_TAG) << "Init done\n";
53 }
54
55
56 OboePlayer::~OboePlayer()
57 {
58 LOG(DEBUG, LOG_TAG) << "Destructor\n";
59 stop();
60 auto result = out_stream_->stop(std::chrono::nanoseconds(100ms).count());
61 if (result != oboe::Result::OK)
62 LOG(ERROR, LOG_TAG) << "Error in AudioStream::stop: " << oboe::convertToText(result) << "\n";
63 result = out_stream_->close();
64 if (result != oboe::Result::OK)
65 LOG(ERROR, LOG_TAG) << "Error in AudioStream::stop: " << oboe::convertToText(result) << "\n";
66 }
67
68
69 oboe::Result OboePlayer::openStream()
70 {
71 oboe::SharingMode sharing_mode = oboe::SharingMode::Shared;
72 if (settings_.sharing_mode == ClientSettings::SharingMode::exclusive)
73 sharing_mode = oboe::SharingMode::Exclusive;
74
4675 // The builder set methods can be chained for convenience.
4776 oboe::AudioStreamBuilder builder;
48 auto result = builder.setSharingMode(oboe::SharingMode::Exclusive)
77 auto result = builder.setSharingMode(sharing_mode)
4978 ->setPerformanceMode(oboe::PerformanceMode::LowLatency)
50 ->setChannelCount(stream->getFormat().channels())
51 ->setSampleRate(stream->getFormat().rate())
79 ->setChannelCount(stream_->getFormat().channels())
80 ->setSampleRate(stream_->getFormat().rate())
5281 ->setFormat(oboe::AudioFormat::I16)
5382 ->setCallback(this)
5483 ->setDirection(oboe::Direction::Output)
5584 //->setFramesPerCallback((8 * stream->getFormat().rate) / 1000)
5685 //->setFramesPerCallback(2 * oboe::DefaultStreamValues::FramesPerBurst)
5786 //->setFramesPerCallback(960) // 2*192)
58 ->openManagedStream(out_stream_);
59 LOG(INFO, LOG_TAG) << "BufferSizeInFrames: " << out_stream_->getBufferSizeInFrames() << ", FramesPerBurst: " << out_stream_->getFramesPerBurst() << "\n";
60 if (result != oboe::Result::OK)
61 LOG(ERROR, LOG_TAG) << "Error building AudioStream: " << oboe::convertToText(result) << "\n";
87 ->openStream(out_stream_);
6288
6389 if (out_stream_->getAudioApi() == oboe::AudioApi::AAudio)
6490 {
7197 LOG(INFO, LOG_TAG) << "AudioApi: OpenSL\n";
7298 out_stream_->setBufferSizeInFrames(4 * out_stream_->getFramesPerBurst());
7399 }
74 LOG(INFO, LOG_TAG) << "Init done\n";
75 }
76
77
78 OboePlayer::~OboePlayer()
79 {
80 LOG(DEBUG, LOG_TAG) << "Destructor\n";
81 stop();
82 auto result = out_stream_->stop(std::chrono::nanoseconds(100ms).count());
83 if (result != oboe::Result::OK)
84 LOG(ERROR, LOG_TAG) << "Error in AudioStream::stop: " << oboe::convertToText(result) << "\n";
85 result = out_stream_->close();
86 if (result != oboe::Result::OK)
87 LOG(ERROR, LOG_TAG) << "Error in AudioStream::stop: " << oboe::convertToText(result) << "\n";
100
101 return result;
102 }
103
104
105 bool OboePlayer::needsThread() const
106 {
107 return false;
88108 }
89109
90110
129149
130150 if (!stream_->getPlayerChunk(audioData, delay, numFrames))
131151 {
132 // LOG(INFO) << "Failed to get chunk. Playing silence.\n";
152 // LOG(INFO, LOG_TAG) << "Failed to get chunk. Playing silence.\n";
133153 memset(audioData, 0, numFrames * stream_->getFormat().frameSize());
134154 }
135155 else
138158 }
139159
140160 return oboe::DataCallbackResult::Continue;
161 }
162
163
164 void OboePlayer::onErrorBeforeClose(oboe::AudioStream* oboeStream, oboe::Result error)
165 {
166 std::ignore = oboeStream;
167 LOG(INFO, LOG_TAG) << "onErrorBeforeClose: " << oboe::convertToText(error) << "\n";
168 stop();
169 }
170
171
172 void OboePlayer::onErrorAfterClose(oboe::AudioStream* oboeStream, oboe::Result error)
173 {
174 // Tech Note: Disconnected Streams and Plugin Issues
175 // https://github.com/google/oboe/blob/master/docs/notes/disconnect.md
176 std::ignore = oboeStream;
177 LOG(INFO, LOG_TAG) << "onErrorAfterClose: " << oboe::convertToText(error) << "\n";
178 auto result = openStream();
179 if (result != oboe::Result::OK)
180 LOG(ERROR, LOG_TAG) << "Error building AudioStream: " << oboe::convertToText(result) << "\n";
181 start();
141182 }
142183
143184
158199 if (result != oboe::Result::OK)
159200 LOG(ERROR, LOG_TAG) << "Error in requestStop: " << oboe::convertToText(result) << "\n";
160201 }
161
162
163 void OboePlayer::worker()
164 {
165 }
2323
2424 #include "player.hpp"
2525
26 typedef int (*AndroidAudioCallback)(short* buffer, int num_samples);
2726
28
29 /// OpenSL Audio Player
27 /// Android Oboe Audio Player
3028 /**
31 * Player implementation for Oboe
29 * Player implementation for Android Oboe
3230 */
3331 class OboePlayer : public Player, public oboe::AudioStreamCallback
3432 {
3533 public:
36 OboePlayer(const PcmDevice& pcmDevice, std::shared_ptr<Stream> stream);
34 OboePlayer(boost::asio::io_context& io_context, const ClientSettings::Player& settings, std::shared_ptr<Stream> stream);
3735 virtual ~OboePlayer();
3836
3937 void start() override;
4038 void stop() override;
4139
4240 protected:
41 // AudioStreamCallback overrides
4342 oboe::DataCallbackResult onAudioReady(oboe::AudioStream* oboeStream, void* audioData, int32_t numFrames) override;
43 void onErrorBeforeClose(oboe::AudioStream* oboeStream, oboe::Result error) override;
44 void onErrorAfterClose(oboe::AudioStream* oboeStream, oboe::Result error) override;
45
46 protected:
47 oboe::Result openStream();
4448 double getCurrentOutputLatencyMillis() const;
4549
46 void worker() override;
47 oboe::ManagedStream out_stream_;
50 bool needsThread() const override;
51 std::shared_ptr<oboe::AudioStream> out_stream_;
4852
4953 std::unique_ptr<oboe::LatencyTuner> mLatencyTuner;
5054 };
4848 }
4949
5050
51 OpenslPlayer::OpenslPlayer(const PcmDevice& pcmDevice, std::shared_ptr<Stream> stream)
52 : Player(pcmDevice, stream), engineObject(NULL), engineEngine(NULL), outputMixObject(NULL), bqPlayerObject(NULL), bqPlayerPlay(NULL),
51 OpenslPlayer::OpenslPlayer(boost::asio::io_context& io_context, const ClientSettings::Player& settings, std::shared_ptr<Stream> stream)
52 : Player(io_context, settings, stream), engineObject(NULL), engineEngine(NULL), outputMixObject(NULL), bqPlayerObject(NULL), bqPlayerPlay(NULL),
5353 bqPlayerBufferQueue(NULL), bqPlayerVolume(NULL), curBuffer(0), ms_(50), buff_size(0), pubStream_(stream)
5454 {
5555 initOpensl();
141141 }
142142
143143
144 bool OpenslPlayer::needsThread() const
145 {
146 return false;
147 }
148
144149
145150 void OpenslPlayer::throwUnsuccess(const std::string& phase, const std::string& what, SLresult result)
146151 {
369374 (*bqPlayerBufferQueue)->Clear(bqPlayerBufferQueue);
370375 throwUnsuccess(kPhaseStop, "PlayerPlay::SetPlayState", result);
371376 }
372
373
374 void OpenslPlayer::worker()
375 {
376 }
3434 class OpenslPlayer : public Player
3535 {
3636 public:
37 OpenslPlayer(const PcmDevice& pcmDevice, std::shared_ptr<Stream> stream);
37 OpenslPlayer(boost::asio::io_context& io_context, const ClientSettings::Player& settings, std::shared_ptr<Stream> stream);
3838 virtual ~OpenslPlayer();
3939
4040 void start() override;
4646 void initOpensl();
4747 void uninitOpensl();
4848
49 void worker() override;
49 bool needsThread() const override;
5050 void throwUnsuccess(const std::string& phase, const std::string& what, SLresult result);
5151 std::string resultToString(SLresult result) const;
5252
1818 #include <cmath>
1919 #include <iostream>
2020
21 #ifdef WINDOWS
22 #include <cstdlib>
23 #else
24 #pragma GCC diagnostic push
25 #pragma GCC diagnostic ignored "-Wunused-result"
26 #pragma GCC diagnostic ignored "-Wunused-parameter"
27 #pragma GCC diagnostic ignored "-Wmissing-braces"
28 #include <boost/process/args.hpp>
29 #include <boost/process/child.hpp>
30 #include <boost/process/exe.hpp>
31 #pragma GCC diagnostic pop
32 #endif
33
2134 #include "common/aixlog.hpp"
35 #include "common/snap_exception.hpp"
36 #include "common/str_compat.hpp"
37 #include "common/utils/string_utils.hpp"
2238 #include "player.hpp"
2339
2440
2541 using namespace std;
2642
27
28 Player::Player(const PcmDevice& pcmDevice, std::shared_ptr<Stream> stream)
29 : active_(false), stream_(stream), pcmDevice_(pcmDevice), volume_(1.0), muted_(false), volCorrection_(1.0)
30 {
31 }
32
43 static constexpr auto LOG_TAG = "Player";
44
45 Player::Player(boost::asio::io_context& io_context, const ClientSettings::Player& settings, std::shared_ptr<Stream> stream)
46 : io_context_(io_context), active_(false), stream_(stream), settings_(settings), volume_(1.0), muted_(false), volCorrection_(1.0)
47 {
48 string sharing_mode;
49 switch (settings_.sharing_mode)
50 {
51 case ClientSettings::SharingMode::unspecified:
52 sharing_mode = "unspecified";
53 break;
54 case ClientSettings::SharingMode::exclusive:
55 sharing_mode = "exclusive";
56 break;
57 case ClientSettings::SharingMode::shared:
58 sharing_mode = "shared";
59 break;
60 }
61
62 auto not_empty = [](const std::string& value) -> std::string {
63 if (!value.empty())
64 return value;
65 else
66 return "<none>";
67 };
68 LOG(INFO, LOG_TAG) << "Player name: " << not_empty(settings_.player_name) << ", device: " << not_empty(settings_.pcm_device.name)
69 << ", description: " << not_empty(settings_.pcm_device.description) << ", idx: " << settings_.pcm_device.idx
70 << ", sharing mode: " << sharing_mode << ", parameters: " << not_empty(settings.parameter) << "\n";
71
72 string mixer;
73 switch (settings_.mixer.mode)
74 {
75 case ClientSettings::Mixer::Mode::hardware:
76 mixer = "hardware";
77 break;
78 case ClientSettings::Mixer::Mode::software:
79 mixer = "software";
80 break;
81 case ClientSettings::Mixer::Mode::script:
82 mixer = "script";
83 break;
84 case ClientSettings::Mixer::Mode::none:
85 mixer = "none";
86 break;
87 }
88 LOG(INFO, LOG_TAG) << "Mixer mode: " << mixer << ", parameters: " << not_empty(settings_.mixer.parameter) << "\n";
89 LOG(INFO, LOG_TAG) << "Sampleformat: " << (settings_.sample_format.isInitialized() ? settings_.sample_format.toString() : stream->getFormat().toString())
90 << ", stream: " << stream->getFormat().toString() << "\n";
91 }
92
93
94 Player::~Player()
95 {
96 stop();
97 }
3398
3499
35100 void Player::start()
36101 {
37102 active_ = true;
38 playerThread_ = thread(&Player::worker, this);
39 }
40
41
42 Player::~Player()
43 {
44 stop();
45 }
46
103 if (needsThread())
104 playerThread_ = thread(&Player::worker, this);
105
106 // If hardware mixer is used, send the initial volume to the server, because this is
107 // the volume that is configured by the user on his local device, so we shouldn't change it
108 // on client start up
109 if (settings_.mixer.mode == ClientSettings::Mixer::Mode::hardware)
110 {
111 double volume;
112 bool muted;
113 if (getHardwareVolume(volume, muted))
114 {
115 LOG(DEBUG, LOG_TAG) << "Volume: " << volume << ", muted: " << muted << "\n";
116 notifyVolumeChange(volume, muted);
117 }
118 }
119 }
47120
48121
49122 void Player::stop()
51124 if (active_)
52125 {
53126 active_ = false;
54 playerThread_.join();
55 }
127 if (playerThread_.joinable())
128 playerThread_.join();
129 }
130 }
131
132
133 void Player::worker()
134 {
135 }
136
137
138 void Player::setHardwareVolume(double volume, bool muted)
139 {
140 std::ignore = volume;
141 std::ignore = muted;
142 throw SnapException("Failed to set hardware mixer volume: not supported");
143 }
144
145
146 bool Player::getHardwareVolume(double& volume, bool& muted)
147 {
148 std::ignore = volume;
149 std::ignore = muted;
150 throw SnapException("Failed to get hardware mixer volume: not supported");
151 return false;
56152 }
57153
58154
59155 void Player::adjustVolume(char* buffer, size_t frames)
60156 {
61 double volume = volume_;
62 if (muted_)
63 volume = 0.;
64
65 const SampleFormat& sampleFormat = stream_->getFormat();
66
67 if ((volume < 1.0) || (volCorrection_ != 1.))
68 {
157 double volume = volCorrection_;
158 // apply volume changes only for software mixer
159 // for any other mixer, we might still have to apply the volCorrection_
160 if (settings_.mixer.mode == ClientSettings::Mixer::Mode::software)
161 {
162 volume = muted_ ? 0. : volume_;
69163 volume *= volCorrection_;
164 }
165
166 if (volume != 1.0)
167 {
168 const SampleFormat& sampleFormat = stream_->getFormat();
70169 if (sampleFormat.sampleSize() == 1)
71170 adjustVolume<int8_t>(buffer, frames * sampleFormat.channels(), volume);
72171 else if (sampleFormat.sampleSize() == 2)
83182 void Player::setVolume_poly(double volume, double exp)
84183 {
85184 volume_ = std::pow(volume, exp);
86 LOG(DEBUG) << "setVolume poly: " << volume << " => " << volume_ << "\n";
185 LOG(DEBUG, LOG_TAG) << "setVolume poly with exp " << exp << ": " << volume << " => " << volume_ << "\n";
87186 }
88187
89188
93192 // double base = M_E;
94193 // double base = 10.;
95194 volume_ = (pow(base, volume) - 1) / (base - 1);
96 LOG(DEBUG) << "setVolume exp: " << volume << " => " << volume_ << "\n";
97 }
98
99
100 void Player::setVolume(double volume)
101 {
102 setVolume_exp(volume, 10.);
103 }
104
105
106 void Player::setMute(bool mute)
107 {
195 LOG(DEBUG, LOG_TAG) << "setVolume exp with base " << base << ": " << volume << " => " << volume_ << "\n";
196 }
197
198
199 void Player::setVolume(double volume, bool mute)
200 {
201 volume_ = volume;
108202 muted_ = mute;
109 }
203 if (settings_.mixer.mode == ClientSettings::Mixer::Mode::hardware)
204 {
205 setHardwareVolume(volume, muted_);
206 }
207 else if (settings_.mixer.mode == ClientSettings::Mixer::Mode::software)
208 {
209 string param;
210 string mode = utils::string::split_left(settings_.mixer.parameter, ':', param);
211 double dparam = -1.;
212 if (!param.empty())
213 {
214 try
215 {
216 dparam = cpt::stod(param);
217 if (dparam < 0)
218 throw SnapException("must be a positive number");
219 }
220 catch (const std::exception& e)
221 {
222 throw SnapException("Invalid mixer param: " + param + ", error: " + string(e.what()));
223 }
224 }
225 if (mode == "poly")
226 setVolume_poly(volume, (dparam < 0) ? 3. : dparam);
227 else
228 setVolume_exp(volume, (dparam < 0) ? 10. : dparam);
229 }
230 else if (settings_.mixer.mode == ClientSettings::Mixer::Mode::script)
231 {
232 try
233 {
234 #ifdef WINDOWS
235 string cmd = settings_.mixer.parameter + " --volume " + cpt::to_string(volume) + " --mute " + (mute ? "true" : "false");
236 std::system(cmd.c_str());
237 #else
238 using namespace boost::process;
239 child c(exe = settings_.mixer.parameter, args = {"--volume", cpt::to_string(volume), "--mute", mute ? "true" : "false"});
240 c.detach();
241 #endif
242 }
243 catch (const std::exception& e)
244 {
245 LOG(ERROR, LOG_TAG) << "Failed to run script '" + settings_.mixer.parameter + "', error: " << e.what() << "\n";
246 }
247 }
248 }
1818 #ifndef PLAYER_H
1919 #define PLAYER_H
2020
21 #include "client_settings.hpp"
2122 #include "common/aixlog.hpp"
2223 #include "common/endian.hpp"
23 #include "pcm_device.hpp"
2424 #include "stream.hpp"
25
26 #include <boost/asio.hpp>
27
2528 #include <atomic>
29 #include <functional>
2630 #include <string>
2731 #include <thread>
2832 #include <vector>
3438 */
3539 class Player
3640 {
41 using volume_callback = std::function<void(double volume, bool muted)>;
42
3743 public:
38 Player(const PcmDevice& pcmDevice, std::shared_ptr<Stream> stream);
44 Player(boost::asio::io_context& io_context, const ClientSettings::Player& settings, std::shared_ptr<Stream> stream);
3945 virtual ~Player();
4046
4147 /// Set audio volume in range [0..1]
42 virtual void setVolume(double volume);
43 virtual void setMute(bool mute);
48 /// @param volume the volume on range [0..1]
49 /// @param muted muted or not
50 virtual void setVolume(double volume, bool mute);
51
52 /// Called on start, before the first audio sample is sent or any other function is called.
53 /// In case of hardware mixer, it will call getVolume and notify the server about the current volume
4454 virtual void start();
55 /// Called on stop
4556 virtual void stop();
57 /// Sets the hardware volume change callback
58 void setVolumeCallback(const volume_callback& callback)
59 {
60 onVolumeChanged_ = callback;
61 }
4662
4763 protected:
48 virtual void worker() = 0;
64 /// will be run in a thread if needsThread is true
65 virtual void worker();
66 /// @return true if the worker function should be started in a thread
67 virtual bool needsThread() const = 0;
68
69 /// get the hardware mixer volume
70 /// @param[out] volume the volume on range [0..1]
71 /// @param[out] muted muted or not
72 /// @return success or not
73 virtual bool getHardwareVolume(double& volume, bool& muted);
74
75 /// set the hardware mixer volume
76 /// @param volume the volume on range [0..1]
77 /// @param muted muted or not
78 virtual void setHardwareVolume(double volume, bool muted);
4979
5080 void setVolume_poly(double volume, double exp);
5181 void setVolume_exp(double volume, double base);
5282
83 void adjustVolume(char* buffer, size_t frames);
84
85 /// Notify the server about hardware volume changes
86 /// @param volume the volume in range [0..1]
87 /// @param muted if muted or not
88 void notifyVolumeChange(double volume, bool muted) const
89 {
90 if (onVolumeChanged_)
91 onVolumeChanged_(volume, muted);
92 }
93
94 boost::asio::io_context& io_context_;
95 std::atomic<bool> active_;
96 std::shared_ptr<Stream> stream_;
97 std::thread playerThread_;
98 ClientSettings::Player settings_;
99 double volume_;
100 bool muted_;
101 double volCorrection_;
102 volume_callback onVolumeChanged_;
103
104 private:
53105 template <typename T>
54106 void adjustVolume(char* buffer, size_t count, double volume)
55107 {
56108 T* bufferT = (T*)buffer;
57109 for (size_t n = 0; n < count; ++n)
58 bufferT[n] = endian::swap<T>(endian::swap<T>(bufferT[n]) * volume);
110 bufferT[n] = endian::swap<T>(static_cast<T>(endian::swap<T>(bufferT[n]) * volume));
59111 }
60
61 void adjustVolume(char* buffer, size_t frames);
62
63 std::atomic<bool> active_;
64 std::shared_ptr<Stream> stream_;
65 std::thread playerThread_;
66 PcmDevice pcmDevice_;
67 double volume_;
68 bool muted_;
69 double volCorrection_;
70112 };
71113
72114
0 #include "wasapi_player.hpp"
1 #include <initguid.h>
2 #include <mmdeviceapi.h>
3 //#include <functiondiscoverykeys_devpkey.h>
4 #include "common/aixlog.hpp"
5 #include "common/snap_exception.hpp"
6 #include <assert.h>
7 #include <audioclient.h>
8 #include <avrt.h>
9 #include <chrono>
10 #include <codecvt>
11 #include <comdef.h>
12 #include <comip.h>
13 #include <functional>
14 #include <ksmedia.h>
15 #include <locale>
16 #include <mmdeviceapi.h>
17
18 using namespace std;
19 using namespace std::chrono;
20 using namespace std::chrono_literals;
21
22 static constexpr auto LOG_TAG = "WASAPI";
23
24 template <typename T>
25 struct COMMemDeleter
26 {
27 void operator()(T* obj)
28 {
29 if (obj != NULL)
30 {
31 CoTaskMemFree(obj);
32 obj = NULL;
33 }
34 }
35 };
36
37 template <typename T>
38 using com_mem_ptr = unique_ptr<T, COMMemDeleter<T>>;
39
40 using com_handle = unique_ptr<void, function<BOOL(HANDLE)>>;
41
42 const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator);
43 const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator);
44 const IID IID_IAudioClient = __uuidof(IAudioClient);
45 const IID IID_IAudioRenderClient = __uuidof(IAudioRenderClient);
46 const IID IID_IAudioClock = __uuidof(IAudioClock);
47 const IID IID_IAudioEndpointVolume = _uuidof(IAudioEndpointVolume);
48
49 _COM_SMARTPTR_TYPEDEF(IMMDevice, __uuidof(IMMDevice));
50 _COM_SMARTPTR_TYPEDEF(IMMDeviceCollection, __uuidof(IMMDeviceCollection));
51 _COM_SMARTPTR_TYPEDEF(IMMDeviceEnumerator, __uuidof(IMMDeviceEnumerator));
52 _COM_SMARTPTR_TYPEDEF(IAudioClient, __uuidof(IAudioClient));
53 _COM_SMARTPTR_TYPEDEF(IPropertyStore, __uuidof(IPropertyStore));
54 _COM_SMARTPTR_TYPEDEF(IAudioSessionManager, __uuidof(IAudioSessionManager));
55 _COM_SMARTPTR_TYPEDEF(IAudioSessionControl, __uuidof(IAudioSessionControl));
56
57 #define REFTIMES_PER_SEC 10000000
58 #define REFTIMES_PER_MILLISEC 10000
59
60 EXTERN_C const PROPERTYKEY DECLSPEC_SELECTANY PKEY_Device_FriendlyName = {{0xa45c254e, 0xdf1c, 0x4efd, {0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0}}, 14};
61
62 #define CHECK_HR(hres) \
63 if (FAILED(hres)) \
64 { \
65 stringstream ss; \
66 ss << "HRESULT fault status: " << hex << (hres) << " line " << dec << __LINE__ << endl; \
67 LOG(FATAL, LOG_TAG) << ss.str(); \
68 throw SnapException(ss.str()); \
69 }
70
71 WASAPIPlayer::WASAPIPlayer(boost::asio::io_context& io_context, const ClientSettings::Player& settings, std::shared_ptr<Stream> stream)
72 : Player(io_context, settings, stream)
73 {
74 HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
75 CHECK_HR(hr);
76
77 audioEventListener_ = new AudioSessionEventListener();
78 }
79
80 WASAPIPlayer::~WASAPIPlayer()
81 {
82 WASAPIPlayer::stop();
83 }
84
85 inline PcmDevice convertToDevice(int idx, IMMDevicePtr& device)
86 {
87 HRESULT hr;
88 PcmDevice desc;
89
90 LPWSTR id = NULL;
91 hr = device->GetId(&id);
92 CHECK_HR(hr);
93
94 IPropertyStorePtr properties = nullptr;
95 hr = device->OpenPropertyStore(STGM_READ, &properties);
96
97 PROPVARIANT deviceName;
98 PropVariantInit(&deviceName);
99
100 hr = properties->GetValue(PKEY_Device_FriendlyName, &deviceName);
101 CHECK_HR(hr);
102
103 desc.idx = idx;
104 desc.name = wstring_convert<codecvt_utf8<wchar_t>, wchar_t>().to_bytes(id);
105 desc.description = wstring_convert<codecvt_utf8<wchar_t>, wchar_t>().to_bytes(deviceName.pwszVal);
106
107 CoTaskMemFree(id);
108
109 return desc;
110 }
111
112 vector<PcmDevice> WASAPIPlayer::pcm_list()
113 {
114 HRESULT hr;
115 IMMDeviceCollectionPtr devices = nullptr;
116 IMMDeviceEnumeratorPtr deviceEnumerator = nullptr;
117
118 hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
119 if (hr != CO_E_ALREADYINITIALIZED)
120 CHECK_HR(hr);
121
122 hr = CoCreateInstance(CLSID_MMDeviceEnumerator, NULL, CLSCTX_SERVER, IID_IMMDeviceEnumerator, (void**)&deviceEnumerator);
123 CHECK_HR(hr);
124
125 hr = deviceEnumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &devices);
126 CHECK_HR(hr);
127
128 UINT deviceCount;
129 devices->GetCount(&deviceCount);
130
131 if (deviceCount == 0)
132 throw SnapException("no valid devices");
133
134 vector<PcmDevice> deviceList;
135
136 {
137 IMMDevicePtr defaultDevice = nullptr;
138 hr = deviceEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, &defaultDevice);
139 CHECK_HR(hr);
140
141 auto dev = convertToDevice(0, defaultDevice);
142 dev.name = "default";
143 deviceList.push_back(dev);
144 }
145
146 for (UINT i = 0; i < deviceCount; ++i)
147 {
148 IMMDevicePtr device = nullptr;
149
150 hr = devices->Item(i, &device);
151 CHECK_HR(hr);
152 deviceList.push_back(convertToDevice(i + 1, device));
153 }
154
155 return deviceList;
156 }
157
158 void WASAPIPlayer::worker()
159 {
160 assert(sizeof(char) == sizeof(BYTE));
161
162 HRESULT hr;
163
164 // Create the format specifier
165 com_mem_ptr<WAVEFORMATEX> waveformat((WAVEFORMATEX*)(CoTaskMemAlloc(sizeof(WAVEFORMATEX))));
166 waveformat->wFormatTag = WAVE_FORMAT_PCM;
167 waveformat->nChannels = stream_->getFormat().channels();
168 waveformat->nSamplesPerSec = stream_->getFormat().rate();
169 waveformat->wBitsPerSample = stream_->getFormat().bits();
170
171 waveformat->nBlockAlign = waveformat->nChannels * waveformat->wBitsPerSample / 8;
172 waveformat->nAvgBytesPerSec = waveformat->nSamplesPerSec * waveformat->nBlockAlign;
173
174 waveformat->cbSize = 0;
175
176 com_mem_ptr<WAVEFORMATEXTENSIBLE> waveformatExtended((WAVEFORMATEXTENSIBLE*)(CoTaskMemAlloc(sizeof(WAVEFORMATEXTENSIBLE))));
177 waveformatExtended->Format = *waveformat;
178 waveformatExtended->Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
179 waveformatExtended->Format.cbSize = 22;
180 waveformatExtended->Samples.wValidBitsPerSample = waveformat->wBitsPerSample;
181 waveformatExtended->dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT;
182 waveformatExtended->SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
183
184
185 // Retrieve the device enumerator
186 IMMDeviceEnumeratorPtr deviceEnumerator = nullptr;
187 hr = CoCreateInstance(CLSID_MMDeviceEnumerator, NULL, CLSCTX_SERVER, IID_IMMDeviceEnumerator, (void**)&deviceEnumerator);
188 CHECK_HR(hr);
189
190 // Register the default playback device (eRender for playback)
191 IMMDevicePtr device = nullptr;
192 if (settings_.pcm_device.idx == 0)
193 {
194 hr = deviceEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, &device);
195 CHECK_HR(hr);
196 }
197 else
198 {
199 IMMDeviceCollectionPtr devices = nullptr;
200 hr = deviceEnumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &devices);
201 CHECK_HR(hr);
202
203 devices->Item(settings_.pcm_device.idx - 1, &device);
204 }
205
206 IPropertyStorePtr properties = nullptr;
207 hr = device->OpenPropertyStore(STGM_READ, &properties);
208 CHECK_HR(hr);
209
210 PROPVARIANT format;
211 hr = properties->GetValue(PKEY_AudioEngine_DeviceFormat, &format);
212 CHECK_HR(hr);
213
214 PWAVEFORMATEX formatEx = (PWAVEFORMATEX)format.blob.pBlobData;
215 LOG(INFO, LOG_TAG) << "Device accepts format: " << formatEx->nSamplesPerSec << ":" << formatEx->wBitsPerSample << ":" << formatEx->nChannels << "\n";
216 // Activate the device
217 IAudioClientPtr audioClient = nullptr;
218 hr = device->Activate(IID_IAudioClient, CLSCTX_SERVER, NULL, (void**)&audioClient);
219 CHECK_HR(hr);
220
221 if (settings_.sharing_mode == ClientSettings::SharingMode::exclusive)
222 {
223 hr = audioClient->IsFormatSupported(AUDCLNT_SHAREMODE_EXCLUSIVE, &(waveformatExtended->Format), NULL);
224 CHECK_HR(hr);
225 }
226
227 IAudioSessionManagerPtr sessionManager = nullptr;
228 // Get the session manager for the endpoint device.
229 hr = device->Activate(__uuidof(IAudioSessionManager), CLSCTX_INPROC_SERVER, NULL, (void**)&sessionManager);
230 CHECK_HR(hr);
231
232
233 // Get the control interface for the process-specific audio
234 // session with session GUID = GUID_NULL. This is the session
235 // that an audio stream for a DirectSound, DirectShow, waveOut,
236 // or PlaySound application stream belongs to by default.
237 IAudioSessionControlPtr control = nullptr;
238 hr = sessionManager->GetAudioSessionControl(NULL, 0, &control);
239 CHECK_HR(hr);
240
241 // register
242 hr = control->RegisterAudioSessionNotification(audioEventListener_);
243 CHECK_HR(hr);
244
245 AudioEndpointVolumeCallback audioEndpointVolumeCallback;
246 hr = device->Activate(IID_IAudioEndpointVolume, CLSCTX_ALL, NULL, (void**)&audioEndpointListener_);
247
248 audioEndpointListener_->RegisterControlChangeNotify((IAudioEndpointVolumeCallback*)&audioEndpointVolumeCallback);
249
250 // Get the device period
251 REFERENCE_TIME hnsRequestedDuration = REFTIMES_PER_SEC;
252 hr = audioClient->GetDevicePeriod(NULL, &hnsRequestedDuration);
253 CHECK_HR(hr);
254
255 LOG(INFO, LOG_TAG) << "Initializing WASAPI in " << (settings_.sharing_mode == ClientSettings::SharingMode::shared ? "shared" : "exclusive") << " mode\n";
256
257 _AUDCLNT_SHAREMODE share_mode = settings_.sharing_mode == ClientSettings::SharingMode::shared ? AUDCLNT_SHAREMODE_SHARED : AUDCLNT_SHAREMODE_EXCLUSIVE;
258 DWORD stream_flags = settings_.sharing_mode == ClientSettings::SharingMode::shared
259 ? AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY
260 : AUDCLNT_STREAMFLAGS_EVENTCALLBACK;
261
262 // Initialize the client at minimum latency
263 hr = audioClient->Initialize(share_mode, stream_flags, hnsRequestedDuration, hnsRequestedDuration, &(waveformatExtended->Format), NULL);
264
265 if (hr == AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED)
266 {
267 UINT32 alignedBufferSize;
268 hr = audioClient->GetBufferSize(&alignedBufferSize);
269 CHECK_HR(hr);
270 audioClient.Attach(NULL, false);
271 hnsRequestedDuration = (REFERENCE_TIME)((10000.0 * 1000 / waveformatExtended->Format.nSamplesPerSec * alignedBufferSize) + 0.5);
272 hr = device->Activate(IID_IAudioClient, CLSCTX_SERVER, NULL, (void**)&audioClient);
273 CHECK_HR(hr);
274 hr = audioClient->Initialize(share_mode, stream_flags, hnsRequestedDuration, hnsRequestedDuration, &(waveformatExtended->Format), NULL);
275 }
276 CHECK_HR(hr);
277
278 // Register an event to refill the buffer
279 com_handle eventHandle(CreateEvent(NULL, FALSE, FALSE, NULL), &::CloseHandle);
280 if (eventHandle == NULL)
281 CHECK_HR(E_FAIL);
282 hr = audioClient->SetEventHandle(HANDLE(eventHandle.get()));
283 CHECK_HR(hr);
284
285 // Get size of buffer
286 UINT32 bufferFrameCount;
287 hr = audioClient->GetBufferSize(&bufferFrameCount);
288 CHECK_HR(hr);
289
290 // Get the rendering service
291 IAudioRenderClient* renderClient = NULL;
292 hr = audioClient->GetService(IID_IAudioRenderClient, (void**)&renderClient);
293 CHECK_HR(hr);
294
295 // Grab the clock service
296 IAudioClock* clock = NULL;
297 hr = audioClient->GetService(IID_IAudioClock, (void**)&clock);
298 CHECK_HR(hr);
299
300 // Boost our priority
301 DWORD taskIndex = 0;
302 com_handle taskHandle(AvSetMmThreadCharacteristics(TEXT("Pro Audio"), &taskIndex), &::AvRevertMmThreadCharacteristics);
303 if (taskHandle == NULL)
304 CHECK_HR(E_FAIL);
305
306 // And, action!
307 hr = audioClient->Start();
308 CHECK_HR(hr);
309
310 size_t bufferSize = bufferFrameCount * waveformatExtended->Format.nBlockAlign;
311 BYTE* buffer;
312 unique_ptr<char[]> queueBuffer(new char[bufferSize]);
313 UINT64 position = 0, bufferPosition = 0, frequency;
314 clock->GetFrequency(&frequency);
315
316 while (active_)
317 {
318 DWORD returnVal = WaitForSingleObject(eventHandle.get(), 2000);
319 if (returnVal != WAIT_OBJECT_0)
320 {
321 // stop();
322 LOG(INFO, LOG_TAG) << "Got timeout waiting for audio device callback\n";
323 CHECK_HR(ERROR_TIMEOUT);
324
325 hr = audioClient->Stop();
326 CHECK_HR(hr);
327 hr = audioClient->Reset();
328 CHECK_HR(hr);
329
330 while (active_ && !stream_->waitForChunk(std::chrono::milliseconds(100)))
331 LOG(INFO, LOG_TAG) << "Waiting for chunk\n";
332
333 hr = audioClient->Start();
334 CHECK_HR(hr);
335 bufferPosition = 0;
336 break;
337 }
338
339 // Thread was sleeping above, double check that we are still running
340 if (!active_)
341 break;
342
343 // update our volume from IAudioControl
344 if (mode_ == ClientSettings::SharingMode::exclusive)
345 {
346 volCorrection_ = audioEventListener_->getVolume();
347 // muteOverride = audioEventListener_->getMuted(); // use this for also applying audio mixer mute state
348 }
349
350 // get audio device volume from IAudioEndpointVolume
351 // float deviceVolume = audioEndpointVolumeCallback.getVolume(); // system volume (for this audio device)
352 // bool deviceMuted = audioEndpointVolumeCallback.getMuted(); // system mute (for this audio device)
353
354 clock->GetPosition(&position, NULL);
355
356 UINT32 padding = 0;
357 if (settings_.sharing_mode == ClientSettings::SharingMode::shared)
358 {
359 hr = audioClient->GetCurrentPadding(&padding);
360 CHECK_HR(hr);
361 }
362
363 int available = bufferFrameCount - padding;
364
365 if (stream_->getPlayerChunk(queueBuffer.get(),
366 microseconds(((bufferPosition * 1000000) / waveformat->nSamplesPerSec) - ((position * 1000000) / frequency)), available))
367 {
368 if (available > 0)
369 {
370 adjustVolume(queueBuffer.get(), available);
371 hr = renderClient->GetBuffer(available, &buffer);
372 CHECK_HR(hr);
373 memcpy(buffer, queueBuffer.get(), bufferSize);
374 hr = renderClient->ReleaseBuffer(available, 0);
375 CHECK_HR(hr);
376
377 bufferPosition += available;
378 }
379 }
380 else
381 {
382 LOG(INFO, LOG_TAG) << "Failed to get chunk\n";
383
384 hr = audioClient->Stop();
385 CHECK_HR(hr);
386 hr = audioClient->Reset();
387 CHECK_HR(hr);
388
389 while (active_ && !stream_->waitForChunk(std::chrono::milliseconds(100)))
390 LOG(INFO, LOG_TAG) << "Waiting for chunk\n";
391
392 hr = audioClient->Start();
393 CHECK_HR(hr);
394 bufferPosition = 0;
395 }
396 }
397 }
398
399 HRESULT STDMETHODCALLTYPE AudioSessionEventListener::QueryInterface(REFIID riid, VOID** ppvInterface)
400 {
401 if (IID_IUnknown == riid)
402 {
403 AddRef();
404 *ppvInterface = (IUnknown*)this;
405 }
406 else if (__uuidof(IAudioSessionEvents) == riid)
407 {
408 AddRef();
409 *ppvInterface = (IAudioSessionEvents*)this;
410 }
411 else
412 {
413 *ppvInterface = NULL;
414 return E_NOINTERFACE;
415 }
416 return S_OK;
417 }
418
419 HRESULT STDMETHODCALLTYPE AudioSessionEventListener::OnSimpleVolumeChanged(float NewVolume, BOOL NewMute, LPCGUID EventContext)
420 {
421 volume_ = NewVolume;
422 muted_ = NewMute;
423
424 if (NewMute)
425 {
426 LOG(DEBUG, LOG_TAG) << ("MUTE\n");
427 }
428 else
429 {
430 LOG(DEBUG, LOG_TAG) << "Volume = " << (UINT32)(100 * NewVolume + 0.5) << " percent\n";
431 }
432
433 return S_OK;
434 }
435
436 HRESULT STDMETHODCALLTYPE AudioSessionEventListener::OnStateChanged(AudioSessionState NewState)
437 {
438 char* pszState = "?????";
439
440 switch (NewState)
441 {
442 case AudioSessionStateActive:
443 pszState = "active";
444 break;
445 case AudioSessionStateInactive:
446 pszState = "inactive";
447 break;
448 }
449 LOG(DEBUG, LOG_TAG) << "New session state = " << pszState << "\n";
450
451 return S_OK;
452 }
453
454 HRESULT STDMETHODCALLTYPE AudioSessionEventListener::OnSessionDisconnected(AudioSessionDisconnectReason DisconnectReason)
455 {
456 char* pszReason = "?????";
457
458 switch (DisconnectReason)
459 {
460 case DisconnectReasonDeviceRemoval:
461 pszReason = "device removed";
462 break;
463 case DisconnectReasonServerShutdown:
464 pszReason = "server shut down";
465 break;
466 case DisconnectReasonFormatChanged:
467 pszReason = "format changed";
468 break;
469 case DisconnectReasonSessionLogoff:
470 pszReason = "user logged off";
471 break;
472 case DisconnectReasonSessionDisconnected:
473 pszReason = "session disconnected";
474 break;
475 case DisconnectReasonExclusiveModeOverride:
476 pszReason = "exclusive-mode override";
477 break;
478 }
479 LOG(INFO, LOG_TAG) << "Audio session disconnected (reason: " << pszReason << ")";
480
481 return S_OK;
482 }
483
484
485
486 HRESULT STDMETHODCALLTYPE AudioEndpointVolumeCallback::OnNotify(PAUDIO_VOLUME_NOTIFICATION_DATA pNotify)
487 {
488 if (pNotify == NULL)
489 {
490 return E_INVALIDARG;
491 }
492
493 if (pNotify->bMuted)
494 {
495 LOG(DEBUG, LOG_TAG) << ("MASTER MUTE\n");
496 }
497
498 LOG(DEBUG, LOG_TAG) << "Volume = " << (UINT32)(100 * pNotify->fMasterVolume + 0.5) << " percent\n";
499
500 volume_ = pNotify->fMasterVolume;
501 muted_ = pNotify->bMuted;
502
503 return S_OK;
504 }
0 /***
1 This file is part of snapcast
2 Copyright (C) 2014-2016 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 WASAPI_PLAYER_HPP
19 #define WASAPI_PLAYER_HPP
20
21 #include "player.hpp"
22 #include <audiopolicy.h>
23 #include <endpointvolume.h>
24
25 class AudioSessionEventListener : public IAudioSessionEvents
26 {
27 LONG _cRef;
28
29 float volume_ = 1.f;
30 bool muted_ = false;
31
32 public:
33 AudioSessionEventListener() : _cRef(1)
34 {
35 }
36
37 float getVolume()
38 {
39 return volume_;
40 }
41
42 bool getMuted()
43 {
44 return muted_;
45 }
46
47 ~AudioSessionEventListener()
48 {
49 }
50
51 // IUnknown methods -- AddRef, Release, and QueryInterface
52
53 ULONG STDMETHODCALLTYPE AddRef()
54 {
55 return InterlockedIncrement(&_cRef);
56 }
57
58 ULONG STDMETHODCALLTYPE Release()
59 {
60 ULONG ulRef = InterlockedDecrement(&_cRef);
61 if (0 == ulRef)
62 {
63 delete this;
64 }
65 return ulRef;
66 }
67
68 HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, VOID** ppvInterface);
69
70 // Notification methods for audio session events
71
72 HRESULT STDMETHODCALLTYPE OnDisplayNameChanged(LPCWSTR NewDisplayName, LPCGUID EventContext)
73 {
74 return S_OK;
75 }
76
77 HRESULT STDMETHODCALLTYPE OnIconPathChanged(LPCWSTR NewIconPath, LPCGUID EventContext)
78 {
79 return S_OK;
80 }
81
82 HRESULT STDMETHODCALLTYPE OnSimpleVolumeChanged(float NewVolume, BOOL NewMute, LPCGUID EventContext);
83
84 HRESULT STDMETHODCALLTYPE OnChannelVolumeChanged(DWORD ChannelCount, float NewChannelVolumeArray[], DWORD ChangedChannel, LPCGUID EventContext)
85 {
86 return S_OK;
87 }
88
89 HRESULT STDMETHODCALLTYPE OnGroupingParamChanged(LPCGUID NewGroupingParam, LPCGUID EventContext)
90 {
91 return S_OK;
92 }
93
94 HRESULT STDMETHODCALLTYPE OnStateChanged(AudioSessionState NewState);
95
96 HRESULT STDMETHODCALLTYPE OnSessionDisconnected(AudioSessionDisconnectReason DisconnectReason);
97 };
98
99
100 class AudioEndpointVolumeCallback : public IAudioEndpointVolumeCallback
101 {
102 LONG _cRef;
103 float volume_ = 1.f;
104 bool muted_ = false;
105
106 public:
107 AudioEndpointVolumeCallback() : _cRef(1)
108 {
109 }
110
111 ~AudioEndpointVolumeCallback()
112 {
113 }
114
115 float getVolume()
116 {
117 return volume_;
118 }
119
120 bool getMuted()
121 {
122 return muted_;
123 }
124
125 // IUnknown methods -- AddRef, Release, and QueryInterface
126
127 ULONG STDMETHODCALLTYPE AddRef()
128 {
129 return InterlockedIncrement(&_cRef);
130 }
131
132 ULONG STDMETHODCALLTYPE Release()
133 {
134 ULONG ulRef = InterlockedDecrement(&_cRef);
135 if (0 == ulRef)
136 {
137 delete this;
138 }
139 return ulRef;
140 }
141
142 HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, VOID** ppvInterface)
143 {
144 if (IID_IUnknown == riid)
145 {
146 AddRef();
147 *ppvInterface = (IUnknown*)this;
148 }
149 else if (__uuidof(IAudioEndpointVolumeCallback) == riid)
150 {
151 AddRef();
152 *ppvInterface = (IAudioEndpointVolumeCallback*)this;
153 }
154 else
155 {
156 *ppvInterface = NULL;
157 return E_NOINTERFACE;
158 }
159
160 return S_OK;
161 }
162
163 // Callback method for endpoint-volume-change notifications.
164
165 HRESULT STDMETHODCALLTYPE OnNotify(PAUDIO_VOLUME_NOTIFICATION_DATA pNotify);
166 };
167
168 class WASAPIPlayer : public Player
169 {
170 public:
171 WASAPIPlayer(boost::asio::io_context& io_context, const ClientSettings::Player& settings, std::shared_ptr<Stream> stream);
172 virtual ~WASAPIPlayer();
173
174 static std::vector<PcmDevice> pcm_list(void);
175
176 protected:
177 virtual void worker();
178 virtual bool needsThread() const override
179 {
180 return true;
181 }
182
183 private:
184 AudioSessionEventListener* audioEventListener_;
185 IAudioEndpointVolume* audioEndpointListener_;
186 ClientSettings::SharingMode mode_;
187 };
188
189 #endif
00 .\"groff -Tascii -man snapclient.1
1 .TH SNAPCLIENT 1 "January 2020"
1 .TH SNAPCLIENT 1 "June 2020"
22 .SH NAME
33 snapclient - Snapcast client
44 .SH SYNOPSIS
1313 into this file will be send to the connected clients. One of the most generic
1414 ways to use Snapcast is in conjunction with the music player daemon or Mopidy,
1515 which can be configured to use a named pipe as audio output.
16 .SS Options
16 .SS Allowed options:
1717 .TP
1818 \fB--help\fR
1919 produce help message
2121 \fB-v, --version\fR
2222 show version number
2323 .TP
24 \fB-l, --list\fR
25 list pcm devices
26 .TP
27 \fB-s, --soundcard arg (=default)\fR
28 index or name of the soundcard
29 .TP
30 \fB-e, --mstderr\fR
31 send metadata to stderr
32 .TP
3324 \fB-h, --host arg\fR
3425 server hostname or ip address
3526 .TP
3627 \fB-p, --port arg (=1704)\fR
3728 server port
29 .TP
30 \fB-i, --instance arg (=1)\fR
31 instance id when running multiple instances on the same host
32 .TP
33 \fB--hostID arg\fR
34 unique host id, default is MAC address
35 .TP
36 \fB-l, --list\fR
37 list PCM devices
38 .TP
39 \fB-s, --soundcard arg (=default)\fR
40 index or name of the pcm device
41 .TP
42 \fB--latency arg (=0)\fR
43 latency of the PCM device
44 .TP
45 \fB--sampleformat arg\fR
46 resample audio stream to <rate>:<bits>:<channels>
47 .TP
48 \fB--player arg (=alsa)\fR
49 alsa|file[:<options>|?]
50 .TP
51 \fB--mixer arg (=software)\fR
52 software|hardware|script|none|?[:<options>]
53 .TP
54 \fB-e, --mstderr\fR
55 send metadata to stderr
3856 .TP
3957 \fB-d, --daemon [=arg(=-3)]\fR
4058 daemonize, optional process priority [-20..19]
4260 \fB--user arg\fR
4361 the user[:group] to run snapclient as when daemonized
4462 .TP
45 \fB--latency arg (=0)\fR
46 latency of the soundcard
63 \fB--logsink arg\fR
64 log sink [null,system,stdout,stderr,file:<filename>]
4765 .TP
48 \fB-i, --instance arg (=1)\fR
49 instance id
50 .TP
51 \fB--hostID arg\fR
52 unique host id
66 \fB--logfilter arg (=*:info)\fR
67 log filter <tag>:<level>[,<tag>:<level>]* with tag = * or <log tag> and level = [trace,debug,info,notice,warning,error,fatal]
5368 .SH FILES
5469 .TP
5570 \fI/etc/default/snapclient\fR
1717
1818 #include <chrono>
1919 #include <iostream>
20 #ifndef WINDOWS
21 #include <signal.h>
2022 #include <sys/resource.h>
21
22 #include "browseZeroConf/browse_mdns.hpp"
23 #endif
24
2325 #include "common/popl.hpp"
2426 #include "controller.hpp"
2527
2628 #ifdef HAS_ALSA
2729 #include "player/alsa_player.hpp"
2830 #endif
31 #ifdef HAS_WASAPI
32 #include "player/wasapi_player.hpp"
33 #endif
2934 #ifdef HAS_DAEMON
3035 #include "common/daemon.hpp"
3136 #endif
3237 #include "client_settings.hpp"
3338 #include "common/aixlog.hpp"
34 #include "common/signal_handler.hpp"
3539 #include "common/snap_exception.hpp"
3640 #include "common/str_compat.hpp"
3741 #include "common/utils.hpp"
4347
4448 using namespace std::chrono_literals;
4549
50 static constexpr auto LOG_TAG = "Snapclient";
51
4652 PcmDevice getPcmDevice(const std::string& soundcard)
4753 {
54 #if defined(HAS_ALSA) || defined(HAS_WASAPI)
55 vector<PcmDevice> pcmDevices =
4856 #ifdef HAS_ALSA
49 vector<PcmDevice> pcmDevices = AlsaPlayer::pcm_list();
57 AlsaPlayer::pcm_list();
58 #else
59 WASAPIPlayer::pcm_list();
60 #endif
5061
5162 try
5263 {
6273 for (auto dev : pcmDevices)
6374 if (dev.name.find(soundcard) != string::npos)
6475 return dev;
65 #else
6676 std::ignore = soundcard;
6777 #endif
6878
7181 return pcmDevice;
7282 }
7383
84 #ifdef WINDOWS
85 // hack to avoid case destinction in the signal handler
86 #define SIGHUP SIGINT
87 const char* strsignal(int sig)
88 {
89 switch (sig)
90 {
91 case SIGTERM:
92 return "SIGTERM";
93 case SIGINT:
94 return "SIGINT";
95 case SIGBREAK:
96 return "SIGBREAK";
97 case SIGABRT:
98 return "SIGABRT";
99 default:
100 return "Unhandled";
101 }
102 }
103 #endif
74104
75105 int main(int argc, char** argv)
76106 {
87117 OptionParser op("Allowed options");
88118 auto helpSwitch = op.add<Switch>("", "help", "produce help message");
89119 auto groffSwitch = op.add<Switch, Attribute::hidden>("", "groff", "produce groff message");
90 auto debugOption = op.add<Implicit<string>, Attribute::hidden>("", "debug", "enable debug logging", ""); // TODO: &settings.logging.debug);
91120 auto versionSwitch = op.add<Switch>("v", "version", "show version number");
92 #if defined(HAS_ALSA)
121 op.add<Value<string>>("h", "host", "server hostname or ip address", "", &settings.server.host);
122 op.add<Value<size_t>>("p", "port", "server port", 1704, &settings.server.port);
123 op.add<Value<size_t>>("i", "instance", "instance id when running multiple instances on the same host", 1, &settings.instance);
124 op.add<Value<string>>("", "hostID", "unique host id, default is MAC address", "", &settings.host_id);
125
126 // PCM device specific
127 #if defined(HAS_ALSA) || defined(HAS_WASAPI)
93128 auto listSwitch = op.add<Switch>("l", "list", "list PCM devices");
94129 /*auto soundcardValue =*/op.add<Value<string>>("s", "soundcard", "index or name of the pcm device", "default", &pcm_device);
95130 #endif
131 /*auto latencyValue =*/op.add<Value<int>>("", "latency", "latency of the PCM device", 0, &settings.player.latency);
132 #ifdef HAS_SOXR
133 auto sample_format = op.add<Value<string>>("", "sampleformat", "resample audio stream to <rate>:<bits>:<channels>", "");
134 #endif
135
136 auto supported_players = Controller::getSupportedPlayerNames();
137 string supported_players_str;
138 for (const auto& supported_player : supported_players)
139 supported_players_str += (!supported_players_str.empty() ? "|" : "") + supported_player;
140 op.add<Value<string>>("", "player", supported_players_str + "[:<options>|?]", supported_players.front(), &settings.player.player_name);
141
142 // sharing mode
143 #if defined(HAS_OBOE) || defined(HAS_WASAPI)
144 auto sharing_mode = op.add<Value<string>>("", "sharingmode", "audio mode to use [shared|exclusive]", "shared");
145 #endif
146
147 // mixer
148 bool hw_mixer_supported = false;
149 #if defined(HAS_ALSA)
150 hw_mixer_supported = true;
151 #endif
152 std::shared_ptr<popl::Value<std::string>> mixer_mode;
153 if (hw_mixer_supported)
154 mixer_mode = op.add<Value<string>>("", "mixer", "software|hardware|script|none|?[:<options>]", "software");
155 else
156 mixer_mode = op.add<Value<string>>("", "mixer", "software|script|none|?[:<options>]", "software");
157
96158 auto metaStderr = op.add<Switch>("e", "mstderr", "send metadata to stderr");
97 /*auto hostValue =*/op.add<Value<string>>("h", "host", "server hostname or ip address", "", &settings.server.host);
98 /*auto portValue =*/op.add<Value<size_t>>("p", "port", "server port", 1704, &settings.server.port);
159
160 // daemon settings
99161 #ifdef HAS_DAEMON
100162 int processPriority(-3);
101163 auto daemonOption = op.add<Implicit<int>>("d", "daemon", "daemonize, optional process priority [-20..19]", processPriority, &processPriority);
102164 auto userValue = op.add<Value<string>>("", "user", "the user[:group] to run snapclient as when daemonized");
103165 #endif
104 /*auto latencyValue =*/op.add<Value<int>>("", "latency", "latency of the PCM device", 0, &settings.player.latency);
105 /*auto instanceValue =*/op.add<Value<size_t>>("i", "instance", "instance id", 1, &settings.instance);
106 /*auto hostIdValue =*/op.add<Value<string>>("", "hostID", "unique host id", "", &settings.host_id);
107 op.add<Value<string>>("", "player", "audio backend", "", &settings.player.player_name);
108 #ifdef HAS_SOXR
109 auto sample_format = op.add<Value<string>>("", "sampleformat", "resample audio stream to <rate>:<bits>:<channels>", "");
110 #endif
166
167 // logging
168 op.add<Value<string>>("", "logsink", "log sink [null,system,stdout,stderr,file:<filename>]", settings.logging.sink, &settings.logging.sink);
169 auto logfilterOption = op.add<Value<string>>(
170 "", "logfilter", "log filter <tag>:<level>[,<tag>:<level>]* with tag = * or <log tag> and level = [trace,debug,info,notice,warning,error,fatal]",
171 settings.logging.filter);
111172
112173 try
113174 {
131192 exit(EXIT_SUCCESS);
132193 }
133194
195 #if defined(HAS_ALSA) || defined(HAS_WASAPI)
196 if (listSwitch->is_set())
197 {
198 vector<PcmDevice> pcmDevices =
134199 #ifdef HAS_ALSA
135 if (listSwitch->is_set())
136 {
137 vector<PcmDevice> pcmDevices = AlsaPlayer::pcm_list();
200 AlsaPlayer::pcm_list();
201 #else
202 WASAPIPlayer::pcm_list();
203 #endif
138204 for (auto dev : pcmDevices)
139205 {
140206 cout << dev.idx << ": " << dev.name << "\n" << dev.description << "\n\n";
158224
159225 // XXX: Only one metadata option must be set
160226
161 AixLog::Log::init<AixLog::SinkNative>("snapclient", AixLog::Severity::trace, AixLog::Type::special);
162 if (debugOption->is_set())
163 {
164 AixLog::Log::instance().add_logsink<AixLog::SinkCout>(AixLog::Severity::trace, AixLog::Type::all, "%Y-%m-%d %H-%M-%S.#ms [#severity] (#tag_func)");
165 if (!debugOption->value().empty())
166 AixLog::Log::instance().add_logsink<AixLog::SinkFile>(AixLog::Severity::trace, AixLog::Type::all, debugOption->value(),
167 "%Y-%m-%d %H-%M-%S.#ms [#severity] (#tag_func)");
168 }
227 settings.logging.filter = logfilterOption->value();
228 if (logfilterOption->is_set())
229 {
230 for (size_t n = 1; n < logfilterOption->count(); ++n)
231 settings.logging.filter += "," + logfilterOption->value(n);
232 }
233
234 if (settings.logging.sink.empty())
235 {
236 settings.logging.sink = "stdout";
237 #ifdef HAS_DAEMON
238 if (daemonOption->is_set())
239 settings.logging.sink = "system";
240 #endif
241 }
242 AixLog::Filter logfilter;
243 auto filters = utils::string::split(settings.logging.filter, ',');
244 for (const auto& filter : filters)
245 logfilter.add_filter(filter);
246
247 string logformat = "%Y-%m-%d %H-%M-%S.#ms [#severity] (#tag_func)";
248 if (settings.logging.sink.find("file:") != string::npos)
249 {
250 string logfile = settings.logging.sink.substr(settings.logging.sink.find(":") + 1);
251 AixLog::Log::init<AixLog::SinkFile>(logfilter, logfile, logformat);
252 }
253 else if (settings.logging.sink == "stdout")
254 AixLog::Log::init<AixLog::SinkCout>(logfilter, logformat);
255 else if (settings.logging.sink == "stderr")
256 AixLog::Log::init<AixLog::SinkCerr>(logfilter, logformat);
257 else if (settings.logging.sink == "system")
258 AixLog::Log::init<AixLog::SinkNative>("snapclient", logfilter);
259 else if (settings.logging.sink == "null")
260 AixLog::Log::init<AixLog::SinkNull>();
169261 else
170 {
171 AixLog::Log::instance().add_logsink<AixLog::SinkCout>(AixLog::Severity::info, AixLog::Type::all, "%Y-%m-%d %H-%M-%S [#severity] (#tag_func)");
172 }
262 throw SnapException("Invalid log sink: " + settings.logging.sink);
173263
174264 #ifdef HAS_DAEMON
175265 std::unique_ptr<Daemon> daemon;
192282 group = user_group[1];
193283 }
194284 daemon = std::make_unique<Daemon>(user, group, pidFile);
195 SLOG(NOTICE) << "daemonizing" << std::endl;
196 daemon->daemonize();
197 if (processPriority < -20)
198 processPriority = -20;
199 else if (processPriority > 19)
200 processPriority = 19;
285 processPriority = std::min(std::max(-20, processPriority), 19);
201286 if (processPriority != 0)
202287 setpriority(PRIO_PROCESS, 0, processPriority);
203 SLOG(NOTICE) << "daemon started" << std::endl;
288 LOG(NOTICE, LOG_TAG) << "daemonizing" << std::endl;
289 daemon->daemonize();
290 LOG(NOTICE, LOG_TAG) << "daemon started" << std::endl;
204291 }
205292 #endif
206293
225312 }
226313 #endif
227314
228 bool active = true;
229 std::shared_ptr<Controller> controller;
230 auto signal_handler = install_signal_handler({SIGHUP, SIGTERM, SIGINT},
231 [&active, &controller](int signal, const std::string& strsignal) {
232 SLOG(INFO) << "Received signal " << signal << ": " << strsignal << "\n";
233 active = false;
234 if (controller)
235 {
236 LOG(INFO) << "Stopping controller\n";
237 controller->stop();
238 }
239 });
240 if (settings.server.host.empty())
241 {
242 #if defined(HAS_AVAHI) || defined(HAS_BONJOUR)
243 BrowseZeroConf browser;
244 mDNSResult avahiResult;
245 while (active)
315 #if defined(HAS_OBOE) || defined(HAS_WASAPI)
316 settings.player.sharing_mode = (sharing_mode->value() == "exclusive") ? ClientSettings::SharingMode::exclusive : ClientSettings::SharingMode::shared;
317 #endif
318
319 settings.player.player_name = utils::string::split_left(settings.player.player_name, ':', settings.player.parameter);
320 if (settings.player.parameter == "?")
321 {
322 if (settings.player.player_name == "file")
246323 {
247 signal_handler.wait_for(500ms);
248 if (!active)
249 break;
250 try
251 {
252 if (browser.browse("_snapcast._tcp", avahiResult, 5000))
253 {
254 settings.server.host = avahiResult.ip;
255 settings.server.port = avahiResult.port;
256 if (avahiResult.ip_version == IPVersion::IPv6)
257 settings.server.host += "%" + cpt::to_string(avahiResult.iface_idx);
258 LOG(INFO) << "Found server " << settings.server.host << ":" << settings.server.port << "\n";
259 break;
260 }
261 }
262 catch (const std::exception& e)
263 {
264 SLOG(ERROR) << "Exception: " << e.what() << std::endl;
265 }
324 cout << "Options are a comma separated list of:\n"
325 << " \"filename:<filename>\" - with <filename> = \"stdout\", \"stderr\" or a filename\n"
326 << " \"mode:[w|a]\" - w: write (discarding the content), a: append (keeping the content)\n";
266327 }
267 #endif
268 }
269
270 if (active)
271 {
272 // Setup metadata handling
273 auto meta(metaStderr ? std::make_unique<MetaStderrAdapter>() : std::make_unique<MetadataAdapter>());
274
275 controller = make_shared<Controller>(settings, std::move(meta));
276 LOG(INFO) << "Latency: " << settings.player.latency << "\n";
277 controller->run();
278 // signal_handler.wait();
279 // controller->stop();
280 }
328 else
329 {
330 cout << "No options available for \"" << settings.player.player_name << "\n";
331 }
332 exit(EXIT_SUCCESS);
333 }
334
335 string mode = utils::string::split_left(mixer_mode->value(), ':', settings.player.mixer.parameter);
336 if (mode == "software")
337 settings.player.mixer.mode = ClientSettings::Mixer::Mode::software;
338 else if ((mode == "hardware") && hw_mixer_supported)
339 settings.player.mixer.mode = ClientSettings::Mixer::Mode::hardware;
340 else if (mode == "script")
341 settings.player.mixer.mode = ClientSettings::Mixer::Mode::script;
342 else if (mode == "none")
343 settings.player.mixer.mode = ClientSettings::Mixer::Mode::none;
344 else if ((mode == "?") || (mode == "help"))
345 {
346 cout << "mixer can be one of 'software', " << (hw_mixer_supported ? "'hardware', " : "") << "'script', 'none'\n"
347 << "followed by optional parameters:\n"
348 << " * software[:poly[:<exponent>]|exp[:<base>]]\n"
349 << (hw_mixer_supported ? " * hardware[:<mixer name>]\n" : "") << " * script[:<script filename>]\n";
350 exit(EXIT_SUCCESS);
351 }
352 else
353 throw SnapException("Mixer mode not supported: " + mode);
354
355 boost::asio::io_context io_context;
356 // Construct a signal set registered for process termination.
357 boost::asio::signal_set signals(io_context, SIGHUP, SIGINT, SIGTERM);
358 signals.async_wait([&](const boost::system::error_code& ec, int signal) {
359 if (!ec)
360 LOG(INFO, LOG_TAG) << "Received signal " << signal << ": " << strsignal(signal) << "\n";
361 else
362 LOG(INFO, LOG_TAG) << "Failed to wait for signal, error: " << ec.message() << "\n";
363 io_context.stop();
364 });
365
366 // Setup metadata handling
367 auto meta(metaStderr ? std::make_unique<MetaStderrAdapter>() : std::make_unique<MetadataAdapter>());
368 auto controller = make_shared<Controller>(io_context, settings, std::move(meta));
369 controller->start();
370
371 int num_threads = 0;
372 std::vector<std::thread> threads;
373 for (int n = 0; n < num_threads; ++n)
374 threads.emplace_back([&] { io_context.run(); });
375 io_context.run();
376 for (auto& t : threads)
377 t.join();
281378 }
282379 catch (const std::exception& e)
283380 {
284 SLOG(ERROR) << "Exception: " << e.what() << std::endl;
381 LOG(FATAL, LOG_TAG) << "Exception: " << e.what() << std::endl;
285382 exitcode = EXIT_FAILURE;
286383 }
287384
288 SLOG(NOTICE) << "daemon terminated." << endl;
385 LOG(NOTICE, LOG_TAG) << "daemon terminated." << endl;
289386 exit(exitcode);
290387 }
1414 You should have received a copy of the GNU General Public License
1515 along with this program. If not, see <http://www.gnu.org/licenses/>.
1616 ***/
17
18 #ifndef NOMINMAX
19 #define NOMINMAX
20 #endif // NOMINMAX
1721
1822 #include "stream.hpp"
1923 #include "common/aixlog.hpp"
2226 #include <iostream>
2327 #include <string.h>
2428
29
2530 using namespace std;
2631 namespace cs = chronos;
2732
2833 static constexpr auto LOG_TAG = "Stream";
2934 static constexpr auto kCorrectionBegin = 100us;
3035
36 // #define LOG_LATENCIES
3137
3238 Stream::Stream(const SampleFormat& in_format, const SampleFormat& out_format)
3339 : in_format_(in_format), median_(0), shortMedian_(0), lastUpdate_(0), playedFrames_(0), correctAfterXFrames_(0), bufferMs_(cs::msec(500)), frame_delta_(0),
3642 buffer_.setSize(500);
3743 shortBuffer_.setSize(100);
3844 miniBuffer_.setSize(20);
45 latencies_.setSize(100);
3946
4047 format_ = in_format_;
4148 if (out_format.isInitialized())
5259 x = 1,000016667 / (1,000016667 - 1)
5360 */
5461 // setRealSampleRate(format_.rate());
55 #ifdef HAS_SOXR
56 soxr_ = nullptr;
57 if ((format_.rate() != in_format_.rate()) || (format_.bits() != in_format_.bits()))
58 {
59 LOG(INFO, LOG_TAG) << "Resampling from " << in_format_.getFormat() << " to " << format_.getFormat() << "\n";
60 soxr_error_t error;
61
62 soxr_datatype_t in_type = SOXR_INT16_I;
63 soxr_datatype_t out_type = SOXR_INT16_I;
64 if (in_format_.sampleSize() > 2)
65 in_type = SOXR_INT32_I;
66 if (format_.sampleSize() > 2)
67 out_type = SOXR_INT32_I;
68 soxr_io_spec_t iospec = soxr_io_spec(in_type, out_type);
69 // HQ should be fine: http://sox.sourceforge.net/Docs/FAQ
70 soxr_quality_spec_t q_spec = soxr_quality_spec(SOXR_HQ, 0);
71 soxr_ = soxr_create(static_cast<double>(in_format_.rate()), static_cast<double>(format_.rate()), format_.channels(), &error, &iospec, &q_spec, NULL);
72 if (error)
73 {
74 LOG(ERROR, LOG_TAG) << "Error soxr_create: " << error << "\n";
75 soxr_ = nullptr;
76 }
77 // initialize the buffer with 20ms (~latency of the reampler)
78 resample_buffer_.resize(format_.frameSize() * ceil(format_.msRate()) * 20);
79 }
80 #endif
62 resampler_ = std::make_unique<Resampler>(in_format_, format_);
8163 }
8264
8365
8466 Stream::~Stream()
8567 {
86 #ifdef HAS_SOXR
87 if (soxr_)
88 soxr_delete(soxr_);
89 #endif
9068 }
9169
9270
9876 }
9977 else
10078 {
101 correctAfterXFrames_ = round((format_.rate() / sampleRate) / (format_.rate() / sampleRate - 1.));
79 correctAfterXFrames_ = static_cast<int32_t>(round((format_.rate() / sampleRate) / (format_.rate() / sampleRate - 1.)));
10280 // LOG(TRACE, LOG_TAG) << "Correct after X: " << correctAfterXFrames_ << " (Real rate: " << sampleRate << ", rate: " << format_.rate() << ")\n";
10381 }
10482 }
11290
11391 void Stream::clearChunks()
11492 {
93 std::lock_guard<std::mutex> lock(mutex_);
11594 while (chunks_.size() > 0)
11695 chunks_.pop();
11796 resetBuffers();
121100 void Stream::addChunk(unique_ptr<msg::PcmChunk> chunk)
122101 {
123102 // drop chunk if it's too old. Just in case, this shouldn't happen.
124 cs::usec age = std::chrono::duration_cast<cs::usec>(TimeProvider::serverNow() - chunk->start());
103 auto age = std::chrono::duration_cast<cs::msec>(TimeProvider::serverNow() - chunk->start());
125104 if (age > 5s + bufferMs_)
126105 return;
127106
128 // LOG(DEBUG, LOG_TAG) << "new chunk: " << chunk->durationMs() << " ms, Chunks: " << chunks_.size() << "\n";
129
130 #ifndef HAS_SOXR
131 chunks_.push(move(chunk));
132 #else
133 if (soxr_ == nullptr)
134 {
135 chunks_.push(move(chunk));
136 }
137 else
138 {
139 if (in_format_.bits() == 24)
140 {
141 // sox expects 32 bit input, shift 8 bits left
142 int32_t* frames = (int32_t*)chunk->payload;
143 for (size_t n = 0; n < chunk->getSampleCount(); ++n)
144 frames[n] = frames[n] << 8;
145 }
146
147 size_t idone;
148 size_t odone;
149 auto resample_buffer_framesize = resample_buffer_.size() / format_.frameSize();
150 auto error = soxr_process(soxr_, chunk->payload, chunk->getFrameCount(), &idone, resample_buffer_.data(), resample_buffer_framesize, &odone);
151 if (error)
152 {
153 LOG(ERROR, LOG_TAG) << "Error soxr_process: " << error << "\n";
154 }
155 else
156 {
157 LOG(TRACE, LOG_TAG) << "Resample idone: " << idone << "/" << chunk->getFrameCount() << ", odone: " << odone << "/"
158 << resample_buffer_.size() / format_.frameSize() << ", delay: " << soxr_delay(soxr_) << "\n";
159
160 // some data has been resampled (odone frames) and some is still in the pipe (soxr_delay frames)
161 if (odone > 0)
162 {
163 // get the resamples ts from the input ts
164 auto input_end_ts = chunk->start() + chunk->duration<std::chrono::microseconds>();
165 double resampled_ms = (odone + soxr_delay(soxr_)) / format_.msRate();
166 auto resampled_start = input_end_ts - std::chrono::microseconds(static_cast<int>(resampled_ms * 1000.));
167
168 auto resampled_chunk = new msg::PcmChunk(format_, 0);
169 auto us = chrono::duration_cast<chrono::microseconds>(resampled_start.time_since_epoch()).count();
170 resampled_chunk->timestamp.sec = us / 1000000;
171 resampled_chunk->timestamp.usec = us % 1000000;
172
173 // copy from the resample_buffer to the resampled chunk
174 resampled_chunk->payloadSize = odone * format_.frameSize();
175 resampled_chunk->payload = (char*)realloc(resampled_chunk->payload, resampled_chunk->payloadSize);
176 memcpy(resampled_chunk->payload, resample_buffer_.data(), resampled_chunk->payloadSize);
177
178 if (format_.bits() == 24)
179 {
180 // sox has quantized to 32 bit, shift 8 bits right
181 int32_t* frames = (int32_t*)resampled_chunk->payload;
182 for (size_t n = 0; n < resampled_chunk->getSampleCount(); ++n)
183 {
184 // +128 to round to the nearest so that quantisation steps are distributed evenly
185 frames[n] = (frames[n] + 128) >> 8;
186 if (frames[n] > 0x7fffffff)
187 frames[n] = 0x7fffffff;
188 }
189 }
190 chunks_.push(shared_ptr<msg::PcmChunk>(resampled_chunk));
191
192 // check if the resample_buffer is large enough, or if soxr was using all available space
193 if (odone == resample_buffer_framesize)
194 {
195 // buffer for resampled data too small, add space for 5ms
196 resample_buffer_.resize(resample_buffer_.size() + format_.frameSize() * ceil(format_.msRate()) * 5);
197 LOG(DEBUG, LOG_TAG) << "Resample buffer completely filled, adding space for 5ms; new buffer size: " << resample_buffer_.size()
198 << " bytes\n";
199 }
200
201 // //LOG(TRACE, LOG_TAG) << "ts: " << out->timestamp.sec << "s, " << out->timestamp.usec/1000.f << " ms, duration: " << odone / format_.msRate()
202 // << "\n";
203 // int64_t next_us = us + static_cast<int64_t>(odone / format_.msRate() * 1000);
204 // LOG(TRACE, LOG_TAG) << "ts: " << us << ", next: " << next_us << ", diff: " << next_us_ - us << "\n";
205 // next_us_ = next_us;
206 }
207 }
208 }
209 #endif
107 auto resampled = resampler_->resample(std::move(chunk));
108 if (resampled)
109 {
110 std::lock_guard<std::mutex> lock(mutex_);
111 recent_ = resampled;
112 chunks_.push(resampled);
113
114 std::shared_ptr<msg::PcmChunk> front_;
115 while (chunks_.front_copy(front_))
116 {
117 auto age = std::chrono::duration_cast<cs::msec>(TimeProvider::serverNow() - front_->start());
118 if ((age > 5s + bufferMs_) && chunks_.try_pop(front_))
119 LOG(TRACE, LOG_TAG) << "Oldest chunk too old: " << age.count() << " ms, removing. Chunks in queue left: " << chunks_.size() << "\n";
120 else
121 break;
122 }
123 }
124 // LOG(TRACE, LOG_TAG) << "new chunk: " << chunk->durationMs() << " ms, age: " << age.count() << " ms, Chunks: " << chunks_.size() << "\n";
210125 }
211126
212127
244159 if (framesCorrection < 0 && frames + framesCorrection <= 0)
245160 {
246161 // Avoid underflow in new char[] constructor.
247 framesCorrection = -frames + 1;
162 framesCorrection = -static_cast<int32_t>(frames) + 1;
248163 }
249164
250165 if (framesCorrection == 0)
269184 slices = max;
270185 }
271186 // Size of each slice. The last slice may be bigger.
272 int size = max / slices;
187 auto size = max / slices;
273188
274189 // LOG(TRACE, LOG_TAG) << "getNextPlayerChunk, frames: " << frames << ", correction: " << framesCorrection << " (" << toRead << "), slices: " << slices
275190 // << "\n";
304219 }
305220
306221
307 void Stream::updateBuffers(int age)
222 void Stream::updateBuffers(chronos::usec::rep age)
308223 {
309224 buffer_.add(age);
310225 miniBuffer_.add(age);
329244 return false;
330245 }
331246
247 std::lock_guard<std::mutex> lock(mutex_);
332248 time_t now = time(nullptr);
333249 if (!chunk_ && !chunks_.try_pop(chunk_))
334250 {
339255 }
340256 return false;
341257 }
258
259 #ifdef LOG_LATENCIES
260 // calculate the estimated end to end latency
261 if (recent_)
262 {
263 cs::nsec req_chunk_duration = cs::nsec(static_cast<cs::nsec::rep>(frames / format_.nsRate()));
264 auto youngest = recent_->end() - req_chunk_duration;
265 cs::msec age = std::chrono::duration_cast<cs::msec>(TimeProvider::serverNow() - youngest + outputBufferDacTime);
266 latencies_.add(age.count());
267 }
268 #endif
342269
343270 /// we have a chunk
344271 /// age = chunk age (server now - rec time: some positive value) - buffer (e.g. 1000ms) + time to DAC
366293 {
367294 if (age.count() > 0)
368295 {
369 LOG(DEBUG, LOG_TAG) << "age > 0: " << age.count() / 1000 << "ms\n";
296 LOG(DEBUG, LOG_TAG) << "age > 0: " << age.count() / 1000 << "ms, dropping old chunks\n";
370297 // age > 0: the top of the stream is too old. We must fast foward.
371298 // delete the current chunk, it's too old. This will avoid an endless loop if there is no chunk in the queue.
372299 chunk_ = nullptr;
376303 LOG(DEBUG, LOG_TAG) << "age: " << age.count() / 1000 << ", requested chunk_duration: "
377304 << std::chrono::duration_cast<std::chrono::milliseconds>(req_chunk_duration).count()
378305 << ", duration: " << chunk_->duration<std::chrono::milliseconds>().count() << "\n";
306 // check if the current chunk's end is older than age => can be player
307 if ((age.count() > 0) && (age < chunk_->duration<cs::usec>()))
308 {
309 // fast forward by "age" to get in sync, i.e. age = 0
310 chunk_->seek(static_cast<uint32_t>(chunk_->format.nsRate() * std::chrono::duration_cast<cs::nsec>(age).count()));
311 age = 0s;
312 }
379313 if (age.count() <= 0)
380314 break;
381315 }
387321 // e.g. age = -20ms (=> should be played in 20ms)
388322 // and the current chunk duration is 50ms, so we need to play 20ms silence (as we don't have data)
389323 // and can play 30ms of the stream
390 uint32_t silent_frames = static_cast<size_t>(-chunk_->format.nsRate() * std::chrono::duration_cast<cs::nsec>(age).count());
324 uint32_t silent_frames = static_cast<uint32_t>(-chunk_->format.nsRate() * std::chrono::duration_cast<cs::nsec>(age).count());
391325 bool result = (silent_frames <= frames);
392326 silent_frames = std::min(silent_frames, frames);
393 LOG(DEBUG, LOG_TAG) << "Silent frames: " << silent_frames << ", frames: " << frames
394 << ", age: " << std::chrono::duration_cast<cs::usec>(age).count() / 1000. << "\n";
395 getSilentPlayerChunk(outputBuffer, silent_frames);
327 if (silent_frames > 0)
328 {
329 LOG(DEBUG, LOG_TAG) << "Silent frames: " << silent_frames << ", frames: " << frames
330 << ", age: " << std::chrono::duration_cast<cs::usec>(age).count() / 1000. << "\n";
331 getSilentPlayerChunk(outputBuffer, silent_frames);
332 }
396333 getNextPlayerChunk((char*)outputBuffer + (chunk_->format.frameSize() * silent_frames), frames - silent_frames);
397334
398335 if (result)
471408
472409 updateBuffers(age.count());
473410
474 // print sync stats
411 // update median_ and shortMedian_ and print sync stats
475412 if (now != lastUpdate_)
476413 {
414 // log buffer stats
477415 lastUpdate_ = now;
478416 median_ = buffer_.median();
479417 shortMedian_ = shortBuffer_.median();
480 LOG(INFO, LOG_TAG) << "Chunk: " << age.count() / 100 << "\t" << miniBuffer_.median() / 100 << "\t" << shortMedian_ / 100 << "\t" << median_ / 100
481 << "\t" << buffer_.size() << "\t" << cs::duration<cs::msec>(outputBufferDacTime) << "\t" << frame_delta_ << "\n";
418 LOG(DEBUG, "Stats") << "Chunk: " << age.count() / 100 << "\t" << miniBuffer_.median() / 100 << "\t" << shortMedian_ / 100 << "\t" << median_ / 100
419 << "\t" << buffer_.size() << "\t" << cs::duration<cs::msec>(outputBufferDacTime) << "\t" << frame_delta_ << "\n";
482420 frame_delta_ = 0;
421
422 #ifdef LOG_LATENCIES
423 // log latencies
424 std::array<uint8_t, 5> percents = {100, 99, 95, 50, 5};
425 auto percentiles = latencies_.percentiles(percents);
426 std::stringstream ss;
427 for (std::size_t n = 0; n < percents.size(); ++n)
428 ss << ((n > 0) ? ", " : "") << (int)percents[n] << "%: " << percentiles[n];
429 LOG(DEBUG, "Latency") << ss.str() << "\n";
430 #endif
483431 }
484432 return (abs(cs::duration<cs::msec>(age)) < 500);
485433 }
486434 catch (int e)
487435 {
488 LOG(INFO) << "Exception\n";
436 LOG(INFO, LOG_TAG) << "Exception: " << e << "\n";
489437 hard_sync_ = true;
490438 return false;
491439 }
1515 along with this program. If not, see <http://www.gnu.org/licenses/>.
1616 ***/
1717
18 #ifndef STREAM_H
19 #define STREAM_H
18 #ifndef STREAM_HPP
19 #define STREAM_HPP
2020
2121 #include "common/queue.h"
2222 #include "common/sample_format.hpp"
2323 #include "double_buffer.hpp"
2424 #include "message/message.hpp"
2525 #include "message/pcm_chunk.hpp"
26 #include "resampler.hpp"
2627 #include <deque>
2728 #include <memory>
2829 #ifdef HAS_SOXR
5960 bool waitForChunk(const std::chrono::milliseconds& timeout) const;
6061
6162 private:
62 /// Request an audio chunk from the front of the stream.
63 /// Request an audio chunk from the front of the stream.
6364 /// @param outputBuffer will be filled with the chunk
6465 /// @param frames the number of requested frames
6566 /// @return the timepoint when this chunk should be audible
6869 /// Request an audio chunk from the front of the stream with a tempo adaption
6970 /// @param outputBuffer will be filled with the chunk
7071 /// @param frames the number of requested frames
71 /// @param framesCorrection number of frames that should be added or removed.
72 /// @param framesCorrection number of frames that should be added or removed.
7273 /// The function will allways return "frames" frames, but will fit "frames + framesCorrection" frames into "frames"
7374 /// so if frames is 100 and framesCorrection is 2, 102 frames will be read from the stream and 2 frames will be removed.
74 /// This makes us "fast-forward" by 2 frames, or if framesCorrection is -3, 97 frames will be read from the stream and
75 /// This makes us "fast-forward" by 2 frames, or if framesCorrection is -3, 97 frames will be read from the stream and
7576 /// filled with 3 frames (simply by dublication), this makes us effectively slower
7677 /// @return the timepoint when this chunk should be audible
7778 chronos::time_point_clk getNextPlayerChunk(void* outputBuffer, uint32_t frames, int32_t framesCorrection);
8182 /// @param frames the number of requested frames
8283 void getSilentPlayerChunk(void* outputBuffer, uint32_t frames) const;
8384
84 void updateBuffers(int age);
85 void updateBuffers(chronos::usec::rep age);
8586 void resetBuffers();
8687 void setRealSampleRate(double sampleRate);
8788
9293 DoubleBuffer<chronos::usec::rep> miniBuffer_;
9394 DoubleBuffer<chronos::usec::rep> shortBuffer_;
9495 DoubleBuffer<chronos::usec::rep> buffer_;
96 /// current chunk (oldest, to be played)
9597 std::shared_ptr<msg::PcmChunk> chunk_;
98 /// most recent chunk (newly queued)
99 std::shared_ptr<msg::PcmChunk> recent_;
100 DoubleBuffer<chronos::msec::rep> latencies_;
96101
97 int median_;
98 int shortMedian_;
102 chronos::usec::rep median_;
103 chronos::usec::rep shortMedian_;
99104 time_t lastUpdate_;
100105 uint32_t playedFrames_;
101106 int32_t correctAfterXFrames_;
102107 chronos::msec bufferMs_;
103108
104 #ifdef HAS_SOXR
105 soxr_t soxr_;
106 #endif
109 std::unique_ptr<Resampler> resampler_;
110
107111 std::vector<char> resample_buffer_;
108112 std::vector<char> read_buffer_;
109113 int frame_delta_;
110114 // int64_t next_us_;
115
116 mutable std::mutex mutex_;
111117
112118 bool hard_sync_;
113119 };
1818 #include "time_provider.hpp"
1919 #include "common/aixlog.hpp"
2020
21 #include <chrono>
22
23 static constexpr auto LOG_TAG = "TimeProvider";
2124
2225 TimeProvider::TimeProvider() : diffToServer_(0)
2326 {
24 diffBuffer_.setSize(100);
27 diffBuffer_.setSize(200);
2528 }
2629
2730
3639
3740 void TimeProvider::setDiffToServer(double ms)
3841 {
39 static int32_t lastTimeSync = 0;
40 timeval now;
41 chronos::steadytimeofday(&now);
42 using namespace std::chrono_literals;
43 auto now = std::chrono::system_clock::now();
44 static auto lastTimeSync = now;
45 auto diff = chronos::abs(now - lastTimeSync);
4246
4347 /// clear diffBuffer if last update is older than a minute
44 if (!diffBuffer_.empty() && (std::abs(now.tv_sec - lastTimeSync) > 60))
48 if (!diffBuffer_.empty() && (diff > 60s))
4549 {
46 LOG(INFO) << "Last time sync older than a minute. Clearing time buffer\n";
47 diffToServer_ = ms * 1000;
50 LOG(INFO, LOG_TAG) << "Last time sync older than a minute. Clearing time buffer\n";
51 diffToServer_ = static_cast<chronos::usec::rep>(ms * 1000);
4852 diffBuffer_.clear();
4953 }
50 lastTimeSync = now.tv_sec;
54 lastTimeSync = now;
5155
52 diffBuffer_.add(ms * 1000);
56 diffBuffer_.add(static_cast<chronos::usec::rep>(ms * 1000));
5357 diffToServer_ = diffBuffer_.median();
54 // LOG(INFO) << "setDiffToServer: " << ms << ", diff: " << diffToServer_ / 1000000 << " s, " << (diffToServer_ / 1000) % 1000 << "." << diffToServer_ % 1000
55 // << " ms\n";
58 // LOG(INFO, LOG_TAG) << "setDiffToServer: " << ms << ", diff: " << diffToServer_ / 1000000 << " s, " << (diffToServer_ / 1000) % 1000 << "." <<
59 // diffToServer_ % 1000 << " ms\n";
5660 }
5761
5862 /*
+0
-61
cmake/Findsoxr.cmake less more
0 # Try to find the soxr library
1 #
2 # Copyright 2018 Thincast Technologies GmbH
3 # Copyright 2018 Armin Novak <armin.novak@thincast.com>
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 #
16 # Once done this will define
17 #
18 # SOXR_ROOT - A list of search hints
19 #
20 # SOXR_FOUND - system has soxr
21 # SOXR_INCLUDE_DIR - the soxr include directory
22 # SOXR_LIBRARIES - libsoxr library
23
24 if (UNIX AND NOT ANDROID)
25 find_package(PkgConfig QUIET)
26 pkg_check_modules(PC_SOXR QUIET soxr)
27 endif (UNIX AND NOT ANDROID)
28
29 if (SOXR_INCLUDE_DIR AND SOXR_LIBRARY)
30 set(SOXR_FIND_QUIETLY TRUE)
31 endif (SOXR_INCLUDE_DIR AND SOXR_LIBRARY)
32
33 find_path(SOXR_INCLUDE_DIR NAMES soxr.h
34 PATH_SUFFIXES include
35 HINTS ${SOXR_ROOT} ${PC_SOXR_INCLUDE_DIRS})
36 find_library(SOXR_LIBRARY
37 NAMES soxr
38 PATH_SUFFIXES lib
39 HINTS ${SOXR_ROOT} ${PC_SOXR_LIBRARY_DIRS})
40
41 include(FindPackageHandleStandardArgs)
42 FIND_PACKAGE_HANDLE_STANDARD_ARGS(soxr DEFAULT_MSG SOXR_LIBRARY SOXR_INCLUDE_DIR)
43
44 if (SOXR_INCLUDE_DIR AND SOXR_LIBRARY)
45 set(SOXR_FOUND TRUE)
46 set(SOXR_INCLUDE_DIRS ${SOXR_INCLUDE_DIR})
47 set(SOXR_LIBRARIES ${SOXR_LIBRARY})
48 endif (SOXR_INCLUDE_DIR AND SOXR_LIBRARY)
49
50 if (SOXR_FOUND)
51 if (NOT SOXR_FIND_QUIETLY)
52 message(STATUS "Found soxr: ${SOXR_LIBRARIES}")
53 endif (NOT SOXR_FIND_QUIETLY)
54 else (SOXR_FOUND)
55 if (SOXR_FIND_REQUIRED)
56 message(FATAL_ERROR "soxr was not found")
57 endif(SOXR_FIND_REQUIRED)
58 endif (SOXR_FOUND)
59
60 mark_as_advanced(SOXR_INCLUDE_DIR SOXR_LIBRARY)
0 ########
1 # Find systemd service dir
2
3 pkg_check_modules(SYSTEMD "systemd")
4 if (SYSTEMD_FOUND AND "${SYSTEMD_SERVICES_INSTALL_DIR}" STREQUAL "")
5 execute_process(COMMAND ${PKG_CONFIG_EXECUTABLE}
6 --variable=systemdsystemunitdir systemd
7 OUTPUT_VARIABLE SYSTEMD_SERVICES_INSTALL_DIR)
8 string(REGEX REPLACE "[ \t\n]+" "" SYSTEMD_SERVICES_INSTALL_DIR
9 "${SYSTEMD_SERVICES_INSTALL_DIR}")
10 elseif (NOT SYSTEMD_FOUND AND SYSTEMD_SERVICES_INSTALL_DIR)
11 message (FATAL_ERROR "Variable SYSTEMD_SERVICES_INSTALL_DIR is\
12 defined, but we can't find systemd using pkg-config")
13 endif()
14
15 if (SYSTEMD_FOUND)
16 set(WITH_SYSTEMD "ON")
17 message(STATUS "systemd services install dir: ${SYSTEMD_SERVICES_INSTALL_DIR}")
18 else()
19 set(WITH_SYSTEMD "OFF")
20 endif (SYSTEMD_FOUND)
+0
-0
cmake/modules/FindAsio.cmake less more
(Empty file)
0 add_library(common STATIC daemon.cpp sample_format.cpp)
0 set(SOURCES
1 resampler.cpp
2 sample_format.cpp)
3
4 if(NOT WIN32)
5 list(APPEND SOURCES daemon.cpp)
6 endif()
7
8 if (SOXR_FOUND)
9 include_directories(${SOXR_INCLUDE_DIRS})
10 endif (SOXR_FOUND)
11
12 add_library(common STATIC ${SOURCES})
13
14 if (SOXR_FOUND)
15 target_link_libraries(common ${SOXR_LIBRARIES})
16 endif (SOXR_FOUND)
22 / _\ ( )( \/ )( ) / \ / __)
33 / \ )( ) ( / (_/\( O )( (_ \
44 \_/\_/(__)(_/\_)\____/ \__/ \___/
5 version 1.2.5
5 version 1.4.0
66 https://github.com/badaix/aixlog
77
88 This file is part of aixlog
3131 #endif
3232
3333 #include <algorithm>
34 #include <cctype>
3435 #include <chrono>
3536 #include <cstdio>
3637 #include <ctime>
3738 #include <fstream>
3839 #include <functional>
3940 #include <iostream>
41 #include <map>
4042 #include <memory>
4143 #include <mutex>
4244 #include <sstream>
45 #include <thread>
4346 #include <vector>
4447
4548 #ifdef __ANDROID__
9295 /// External logger macros
9396 // usage: LOG(SEVERITY) or LOG(SEVERITY, TAG)
9497 // e.g.: LOG(NOTICE) or LOG(NOTICE, "my tag")
98 #ifndef WIN32
9599 #define LOG(...) AIXLOG_INTERNAL__LOG_MACRO_CHOOSER(__VA_ARGS__)(__VA_ARGS__) << TIMESTAMP << FUNC
96 #define SLOG(...) AIXLOG_INTERNAL__LOG_MACRO_CHOOSER(__VA_ARGS__)(__VA_ARGS__) << TIMESTAMP << SPECIAL << FUNC
100 #endif
97101
98102 // usage: COLOR(TEXT_COLOR, BACKGROUND_COLOR) or COLOR(TEXT_COLOR)
99103 // e.g.: COLOR(yellow, blue) or COLOR(red)
102106 #define FUNC AixLog::Function(AIXLOG_INTERNAL__FUNC, __FILE__, __LINE__)
103107 #define TAG AixLog::Tag
104108 #define COND AixLog::Conditional
105 #define SPECIAL AixLog::Type::special
106109 #define TIMESTAMP AixLog::Timestamp(std::chrono::system_clock::now())
110
111
112 // stijnvdb: sorry! :) LOG(SEV, "tag") was not working for Windows and I couldn't figure out how to fix it for windows without potentially breaking everything
113 // else...
114 // https://stackoverflow.com/questions/3046889/optional-parameters-with-c-macros (Jason Deng)
115 #ifdef WIN32
116 #define LOG_2(severity, tag) AIXLOG_INTERNAL__LOG_SEVERITY_TAG(severity, tag)
117 #define LOG_1(severity) AIXLOG_INTERNAL__LOG_SEVERITY(severity)
118 #define LOG_0() LOG_1(0)
119
120 #define FUNC_CHOOSER(_f1, _f2, _f3, ...) _f3
121 #define FUNC_RECOMPOSER(argsWithParentheses) FUNC_CHOOSER argsWithParentheses
122 #define CHOOSE_FROM_ARG_COUNT(...) FUNC_RECOMPOSER((__VA_ARGS__, LOG_2, LOG_1, FUNC_, ...))
123 #define MACRO_CHOOSER(...) CHOOSE_FROM_ARG_COUNT(__VA_ARGS__())
124 #define LOG(...) MACRO_CHOOSER(__VA_ARGS__)(__VA_ARGS__) << TIMESTAMP << FUNC
125 #endif
107126
108127 /**
109128 * @brief
155174 fatal = SEVERITY::FATAL
156175 };
157176
158 /**
159 * @brief
160 * Type of the log message or Sink
161 *
162 * "normal" messages will be logged by "normal" or "all" Sinks
163 * "special" ones by "special" or "all" Sinks
164 */
165 enum class Type
166 {
167 normal,
168 special,
169 all
170 };
177
178 static Severity to_severity(std::string severity, Severity def = Severity::info)
179 {
180 std::transform(severity.begin(), severity.end(), severity.begin(), [](unsigned char c) { return std::tolower(c); });
181 if (severity == "trace")
182 return Severity::trace;
183 else if (severity == "debug")
184 return Severity::debug;
185 else if (severity == "info")
186 return Severity::info;
187 else if (severity == "notice")
188 return Severity::notice;
189 else if (severity == "warning")
190 return Severity::warning;
191 else if (severity == "error")
192 return Severity::error;
193 else if (severity == "fatal")
194 return Severity::fatal;
195 else
196 return def;
197 }
198
199
200 static std::string to_string(Severity logSeverity)
201 {
202 switch (logSeverity)
203 {
204 case Severity::trace:
205 return "Trace";
206 case Severity::debug:
207 return "Debug";
208 case Severity::info:
209 return "Info";
210 case Severity::notice:
211 return "Notice";
212 case Severity::warning:
213 return "Warn";
214 case Severity::error:
215 return "Error";
216 case Severity::fatal:
217 return "Fatal";
218 default:
219 std::stringstream ss;
220 ss << static_cast<int>(logSeverity);
221 return ss.str();
222 }
223 }
171224
172225 /**
173226 * @brief
324377 {
325378 }
326379
380 Tag(const char* text) : text(text), is_null_(false)
381 {
382 }
383
327384 Tag(const std::string& text) : text(text), is_null_(false)
328385 {
329386 }
337394 explicit operator bool() const
338395 {
339396 return !is_null_;
397 }
398
399 bool operator<(const Tag& other) const
400 {
401 return (text < other.text);
340402 }
341403
342404 std::string text;
388450 */
389451 struct Metadata
390452 {
391 Metadata() : severity(Severity::trace), tag(nullptr), type(Type::normal), function(nullptr), timestamp(nullptr)
453 Metadata() : severity(Severity::trace), tag(nullptr), function(nullptr), timestamp(nullptr)
392454 {
393455 }
394456
395457 Severity severity;
396458 Tag tag;
397 Type type;
398459 Function function;
399460 Timestamp timestamp;
400461 };
462
463
464 class Filter
465 {
466 public:
467 Filter()
468 {
469 }
470
471 Filter(Severity severity)
472 {
473 add_filter(severity);
474 }
475
476 bool match(const Metadata& metadata) const
477 {
478 if (tag_filter_.empty())
479 return true;
480
481 auto iter = tag_filter_.find(metadata.tag);
482 if (iter != tag_filter_.end())
483 return (metadata.severity >= iter->second);
484
485 iter = tag_filter_.find("*");
486 if (iter != tag_filter_.end())
487 return (metadata.severity >= iter->second);
488
489 return false;
490 }
491
492 void add_filter(const Tag& tag, Severity severity)
493 {
494 tag_filter_[tag] = severity;
495 }
496
497 void add_filter(Severity severity)
498 {
499 tag_filter_["*"] = severity;
500 }
501
502 void add_filter(const std::string& filter)
503 {
504 auto pos = filter.find(":");
505 if (pos != std::string::npos)
506 add_filter(filter.substr(0, pos), to_severity(filter.substr(pos + 1)));
507 else
508 add_filter(to_severity(filter));
509 }
510
511 private:
512 std::map<Tag, Severity> tag_filter_;
513 };
514
401515
402516 /**
403517 * @brief
407521 */
408522 struct Sink
409523 {
410 Sink(Severity severity, Type type) : severity(severity), sink_type_(type)
524 Sink(const Filter& filter) : filter(filter)
411525 {
412526 }
413527
414528 virtual ~Sink() = default;
415529
416530 virtual void log(const Metadata& metadata, const std::string& message) = 0;
417 virtual Type get_type() const
418 {
419 return sink_type_;
420 }
421
422 virtual Sink& set_type(Type sink_type)
423 {
424 sink_type_ = sink_type;
425 return *this;
426 }
427
428 Severity severity;
429
430 protected:
431 Type sink_type_;
531
532 Filter filter;
432533 };
433534
434535 /// ostream operators << for the meta data structs
435536 static std::ostream& operator<<(std::ostream& os, const Severity& log_severity);
436 static std::ostream& operator<<(std::ostream& os, const Type& log_type);
437537 static std::ostream& operator<<(std::ostream& os, const Timestamp& timestamp);
438538 static std::ostream& operator<<(std::ostream& os, const Tag& tag);
439539 static std::ostream& operator<<(std::ostream& os, const Function& function);
499599 log_sinks_.erase(std::remove(log_sinks_.begin(), log_sinks_.end(), sink), log_sinks_.end());
500600 }
501601
502 static std::string to_string(Severity logSeverity)
503 {
504 switch (logSeverity)
505 {
506 case Severity::trace:
507 return "Trace";
508 case Severity::debug:
509 return "Debug";
510 case Severity::info:
511 return "Info";
512 case Severity::notice:
513 return "Notice";
514 case Severity::warning:
515 return "Warn";
516 case Severity::error:
517 return "Error";
518 case Severity::fatal:
519 return "Fatal";
520 default:
521 std::stringstream ss;
522 ss << logSeverity;
523 return ss.str();
524 }
525 }
526
527602 protected:
528 Log() noexcept
603 Log() noexcept : last_buffer_(nullptr)
529604 {
530605 std::clog.rdbuf(this);
531 std::clog << Severity() << Type::normal << Tag() << Function() << Conditional() << AixLog::Color::NONE << std::flush;
606 std::clog << Severity() << Tag() << Function() << Conditional() << AixLog::Color::NONE << std::flush;
532607 }
533608
534609 virtual ~Log()
539614 int sync() override
540615 {
541616 std::lock_guard<std::recursive_mutex> lock(mutex_);
542 if (!buffer_.str().empty())
617 if (!get_stream().str().empty())
543618 {
544619 if (conditional_.is_true())
545620 {
546621 for (const auto& sink : log_sinks_)
547622 {
548 if ((metadata_.type == Type::all) || (sink->get_type() == Type::all) || (metadata_.type == sink->get_type()))
549 if (metadata_.severity >= sink->severity)
550 sink->log(metadata_, buffer_.str());
623 if (sink->filter.match(metadata_))
624 sink->log(metadata_, get_stream().str());
551625 }
552626 }
553 buffer_.str("");
554 buffer_.clear();
627 get_stream().str("");
628 get_stream().clear();
555629 }
556630
557631 return 0;
565639 if (c == '\n')
566640 sync();
567641 else
568 buffer_ << static_cast<char>(c);
642 get_stream() << static_cast<char>(c);
569643 }
570644 else
571645 {
576650
577651 private:
578652 friend std::ostream& operator<<(std::ostream& os, const Severity& log_severity);
579 friend std::ostream& operator<<(std::ostream& os, const Type& log_type);
580653 friend std::ostream& operator<<(std::ostream& os, const Timestamp& timestamp);
581654 friend std::ostream& operator<<(std::ostream& os, const Tag& tag);
582655 friend std::ostream& operator<<(std::ostream& os, const Function& function);
583656 friend std::ostream& operator<<(std::ostream& os, const Conditional& conditional);
584657
585 std::stringstream buffer_;
658 std::stringstream& get_stream()
659 {
660 auto id = std::this_thread::get_id();
661 if ((last_buffer_ == nullptr) || (last_id_ != id))
662 {
663 last_id_ = id;
664 last_buffer_ = &(buffer_[id]);
665 }
666 return *last_buffer_;
667 }
668
669 std::map<std::thread::id, std::stringstream> buffer_;
670 std::thread::id last_id_;
671 std::stringstream* last_buffer_ = nullptr;
586672 Metadata metadata_;
587673 Conditional conditional_;
588674 std::vector<log_sink_ptr> log_sinks_;
589675 std::recursive_mutex mutex_;
590676 };
677
678 /**
679 * @brief
680 * Null log sink
681 *
682 * Discards all log messages
683 */
684 struct SinkNull : public Sink
685 {
686 SinkNull() : Sink(Filter())
687 {
688 }
689
690 void log(const Metadata& /*metadata*/, const std::string& /*message*/) override
691 {
692 }
693 };
694
591695
592696 /**
593697 * @brief
605709 */
606710 struct SinkFormat : public Sink
607711 {
608 SinkFormat(Severity severity, Type type, const std::string& format) : Sink(severity, type), format_(format)
712 SinkFormat(const Filter& filter, const std::string& format) : Sink(filter), format_(format)
609713 {
610714 }
611715
625729
626730 size_t pos = result.find("#severity");
627731 if (pos != std::string::npos)
628 result.replace(pos, 9, Log::to_string(metadata.severity));
732 result.replace(pos, 9, to_string(metadata.severity));
733
734 pos = result.find("#color_severity");
735 if (pos != std::string::npos)
736 {
737 std::stringstream ss;
738 ss << TextColor(Color::RED) << to_string(metadata.severity) << TextColor(Color::NONE);
739 result.replace(pos, 15, ss.str());
740 }
629741
630742 pos = result.find("#tag_func");
631743 if (pos != std::string::npos)
663775 */
664776 struct SinkCout : public SinkFormat
665777 {
666 SinkCout(Severity severity, Type type, const std::string& format = "%Y-%m-%d %H-%M-%S.#ms [#severity] (#tag_func)") : SinkFormat(severity, type, format)
778 SinkCout(const Filter& filter, const std::string& format = "%Y-%m-%d %H-%M-%S.#ms [#severity] (#tag_func)") : SinkFormat(filter, format)
667779 {
668780 }
669781
679791 */
680792 struct SinkCerr : public SinkFormat
681793 {
682 SinkCerr(Severity severity, Type type, const std::string& format = "%Y-%m-%d %H-%M-%S.#ms [#severity] (#tag_func)") : SinkFormat(severity, type, format)
794 SinkCerr(const Filter& filter, const std::string& format = "%Y-%m-%d %H-%M-%S.#ms [#severity] (#tag_func)") : SinkFormat(filter, format)
683795 {
684796 }
685797
695807 */
696808 struct SinkFile : public SinkFormat
697809 {
698 SinkFile(Severity severity, Type type, const std::string& filename, const std::string& format = "%Y-%m-%d %H-%M-%S.#ms [#severity] (#tag_func)")
699 : SinkFormat(severity, type, format)
810 SinkFile(const Filter& filter, const std::string& filename, const std::string& format = "%Y-%m-%d %H-%M-%S.#ms [#severity] (#tag_func)")
811 : SinkFormat(filter, format)
700812 {
701813 ofs.open(filename.c_str(), std::ofstream::out | std::ofstream::trunc);
702814 }
724836 */
725837 struct SinkOutputDebugString : public Sink
726838 {
727 SinkOutputDebugString(Severity severity, Type type = Type::all) : Sink(severity, type)
839 SinkOutputDebugString(const Filter& filter) : Sink(filter)
728840 {
729841 }
730842
731843 void log(const Metadata& metadata, const std::string& message) override
732844 {
733 OutputDebugString(message.c_str());
845 std::wstring wide = std::wstring(message.begin(), message.end());
846 OutputDebugString(wide.c_str());
734847 }
735848 };
736849 #endif
742855 */
743856 struct SinkUnifiedLogging : public Sink
744857 {
745 SinkUnifiedLogging(Severity severity, Type type = Type::all) : Sink(severity, type)
858 SinkUnifiedLogging(const Filter& filter) : Sink(filter)
746859 {
747860 }
748861
782895 */
783896 struct SinkSyslog : public Sink
784897 {
785 SinkSyslog(const char* ident, Severity severity, Type type) : Sink(severity, type)
898 SinkSyslog(const char* ident, const Filter& filter) : Sink(filter)
786899 {
787900 openlog(ident, LOG_PID, LOG_USER);
788901 }
831944 */
832945 struct SinkAndroid : public Sink
833946 {
834 SinkAndroid(const std::string& ident, Severity severity, Type type = Type::all) : Sink(severity, type), ident_(ident)
947 SinkAndroid(const std::string& ident, const Filter& filter) : Sink(filter), ident_(ident)
835948 {
836949 }
837950
8881001 */
8891002 struct SinkEventLog : public Sink
8901003 {
891 SinkEventLog(const std::string& ident, Severity severity, Type type = Type::all) : Sink(severity, type)
892 {
893 event_log = RegisterEventSource(NULL, ident.c_str());
1004 SinkEventLog(const std::string& ident, const Filter& filter) : Sink(filter)
1005 {
1006 std::wstring wide = std::wstring(ident.begin(), ident.end()); // stijnvdb: RegisterEventSource expands to RegisterEventSourceW which takes wchar_t
1007 event_log = RegisterEventSource(NULL, wide.c_str());
8941008 }
8951009
8961010 WORD get_type(Severity severity) const
9161030
9171031 void log(const Metadata& metadata, const std::string& message) override
9181032 {
1033 std::wstring wide = std::wstring(message.begin(), message.end());
9191034 // We need this temp variable because we cannot take address of rValue
920 const char* c_str = message.c_str();
1035 const wchar_t* c_str = wide.c_str();
1036
9211037 ReportEvent(event_log, get_type(metadata.severity), 0, 0, NULL, 1, 0, &c_str, NULL);
9221038 }
9231039
9371053 */
9381054 struct SinkNative : public Sink
9391055 {
940 SinkNative(const std::string& ident, Severity severity, Type type = Type::all) : Sink(severity, type), log_sink_(nullptr), ident_(ident)
1056 SinkNative(const std::string& ident, const Filter& filter) : Sink(filter), log_sink_(nullptr), ident_(ident)
9411057 {
9421058 #ifdef __ANDROID__
943 log_sink_ = std::make_shared<SinkAndroid>(ident_, severity, type);
1059 log_sink_ = std::make_shared<SinkAndroid>(ident_, filter);
9441060 #elif HAS_APPLE_UNIFIED_LOG_
945 log_sink_ = std::make_shared<SinkUnifiedLogging>(severity, type);
1061 log_sink_ = std::make_shared<SinkUnifiedLogging>(filter);
9461062 #elif _WIN32
947 log_sink_ = std::make_shared<SinkEventLog>(ident, severity, type);
1063 log_sink_ = std::make_shared<SinkEventLog>(ident, filter);
9481064 #elif HAS_SYSLOG_
949 log_sink_ = std::make_shared<SinkSyslog>(ident_.c_str(), severity, type);
1065 log_sink_ = std::make_shared<SinkSyslog>(ident_.c_str(), filter);
9501066 #else
9511067 /// will not throw or something. Use "get_logger()" to check for success
9521068 log_sink_ = nullptr;
9811097 {
9821098 using callback_fun = std::function<void(const Metadata& metadata, const std::string& message)>;
9831099
984 SinkCallback(Severity severity, Type type, callback_fun callback) : Sink(severity, type), callback_(callback)
1100 SinkCallback(const Filter& filter, callback_fun callback) : Sink(filter), callback_(callback)
9851101 {
9861102 }
9871103
10111127 {
10121128 log->sync();
10131129 log->metadata_.severity = log_severity;
1014 log->metadata_.type = Type::normal;
10151130 log->metadata_.timestamp = nullptr;
10161131 log->metadata_.tag = nullptr;
10171132 log->metadata_.function = nullptr;
10201135 }
10211136 else
10221137 {
1023 os << Log::to_string(log_severity);
1024 }
1025 return os;
1026 }
1027
1028 static std::ostream& operator<<(std::ostream& os, const Type& log_type)
1029 {
1030 Log* log = dynamic_cast<Log*>(os.rdbuf());
1031 if (log != nullptr)
1032 {
1033 std::lock_guard<std::recursive_mutex> lock(log->mutex_);
1034 log->metadata_.type = log_type;
1138 os << to_string(log_severity);
10351139 }
10361140 return os;
10371141 }
6464 uid_t user_uid = (uid_t)-1;
6565 gid_t user_gid = (gid_t)-1;
6666 std::string user_name;
67 //#ifdef FREEBSD
68 // bool had_group = false;
69 //#endif
67 // #ifdef FREEBSD
68 // bool had_group = false;
69 // #endif
7070
7171 if (!user_.empty())
7272 {
8686 if (grp == nullptr)
8787 throw SnapException("no such group \"" + group_ + "\"");
8888 user_gid = grp->gr_gid;
89 //#ifdef FREEBSD
90 // had_group = true;
91 //#endif
89 // #ifdef FREEBSD
90 // had_group = true;
91 // #endif
9292 }
9393
9494 if (chown(pidfile_.c_str(), user_uid, user_gid) == -1)
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 CLIENT_INFO_H
19 #define CLIENT_INFO_H
20
21 #include "json_message.hpp"
22
23
24 namespace msg
25 {
26
27 /// Client information sent from client to server
28 /// Might also be used for sync stats and latency estimations
29 class ClientInfo : public JsonMessage
30 {
31 public:
32 ClientInfo() : JsonMessage(message_type::kClientInfo)
33 {
34 setVolume(100);
35 setMuted(false);
36 }
37
38 ~ClientInfo() override = default;
39
40 uint16_t getVolume()
41 {
42 return get("volume", 100);
43 }
44
45 bool isMuted()
46 {
47 return get("muted", false);
48 }
49
50 void setVolume(uint16_t volume)
51 {
52 msg["volume"] = volume;
53 }
54
55 void setMuted(bool muted)
56 {
57 msg["muted"] = muted;
58 }
59 };
60 }
61
62
63 #endif
2929 class CodecHeader : public BaseMessage
3030 {
3131 public:
32 CodecHeader(const std::string& codecName = "", size_t size = 0)
32 CodecHeader(const std::string& codecName = "", uint32_t size = 0)
3333 : BaseMessage(message_type::kCodecHeader), payloadSize(size), payload(nullptr), codec(codecName)
3434 {
3535 if (size > 0)
4949
5050 uint32_t getSize() const override
5151 {
52 return sizeof(uint32_t) + codec.size() + sizeof(uint32_t) + payloadSize;
52 return static_cast<uint32_t>(sizeof(uint32_t) + codec.size() + sizeof(uint32_t) + payloadSize);
5353 }
5454
5555 uint32_t payloadSize;
1818 #ifndef MESSAGE_FACTORY_HPP
1919 #define MESSAGE_FACTORY_HPP
2020
21 #include "client_info.hpp"
2122 #include "codec_header.hpp"
2223 #include "hello.hpp"
24 #include "pcm_chunk.hpp"
2325 #include "server_settings.hpp"
2426 #include "stream_tags.hpp"
2527 #include "time.hpp"
26 #include "wire_chunk.hpp"
2728
2829 #include "common/str_compat.hpp"
2930 #include "common/utils.hpp"
3334
3435 namespace msg
3536 {
37
38 template <typename ToType>
39 static std::unique_ptr<ToType> message_cast(std::unique_ptr<msg::BaseMessage> message)
40 {
41 ToType* tmp = dynamic_cast<ToType*>(message.get());
42 std::unique_ptr<ToType> result;
43 if (tmp != nullptr)
44 {
45 message.release();
46 result.reset(tmp);
47 return result;
48 }
49 return nullptr;
50 }
51
3652 namespace factory
3753 {
3854
4561 result->deserialize(base_message, buffer);
4662 return result;
4763 }
48
4964
5065 static std::unique_ptr<BaseMessage> createMessage(const BaseMessage& base_message, char* buffer)
5166 {
6378 case kTime:
6479 return createMessage<Time>(base_message, buffer);
6580 case kWireChunk:
66 return createMessage<WireChunk>(base_message, buffer);
81 // this is kind of cheated to safe the convertion from WireChunk to PcmChunk
82 // the user of the factory must be aware that a PcmChunk will be created
83 return createMessage<PcmChunk>(base_message, buffer);
84 case kClientInfo:
85 return createMessage<ClientInfo>(base_message, buffer);
6786 default:
6887 return nullptr;
6988 }
4646
4747 uint32_t getSize() const override
4848 {
49 return sizeof(uint32_t) + msg.dump().size();
49 return static_cast<uint32_t>(sizeof(uint32_t) + msg.dump().size());
5050 }
5151
5252 json msg;
7373 }
7474 }
7575 };
76 }
76 } // namespace msg
7777
7878
7979 #endif
2424 #include <cstring>
2525 #include <iostream>
2626 #include <streambuf>
27 #ifndef WINDOWS
2728 #include <sys/time.h>
29 #endif
2830 #include <vector>
2931
3032 /*
5759 kTime = 4,
5860 kHello = 5,
5961 kStreamTags = 6,
62 kClientInfo = 7,
6063
6164 kFirst = kBase,
62 kLast = kStreamTags
65 kLast = kClientInfo
6366 };
6467
6568
228231
229232 void writeVal(std::ostream& stream, const std::string& val) const
230233 {
231 uint32_t size = val.size();
234 uint32_t size = static_cast<uint32_t>(val.size());
232235 writeVal(stream, val.c_str(), size);
233236 }
234237
3535 class PcmChunk : public WireChunk
3636 {
3737 public:
38 PcmChunk(const SampleFormat& sampleFormat, size_t ms)
38 PcmChunk(const SampleFormat& sampleFormat, uint32_t ms)
3939 : WireChunk((sampleFormat.rate() * ms / 1000) * sampleFormat.frameSize()), format(sampleFormat), idx_(0)
4040 {
4141 }
5757 }
5858 #endif
5959
60 int readFrames(void* outputBuffer, size_t frameCount)
60 // std::unique_ptr<PcmChunk> consume(uint32_t frameCount)
61 // {
62 // auto result = std::make_unique<PcmChunk>(format, 0);
63 // if (frameCount * format.frameSize() > payloadSize)
64 // frameCount = payloadSize / format.frameSize();
65 // result->payload = payload;
66 // result->payloadSize = frameCount * format.frameSize();
67 // payloadSize -= result->payloadSize;
68 // payload = (char*)realloc(payload + result->payloadSize, payloadSize);
69 // // payload += result->payloadSize;
70 // return result;
71 // }
72
73 int readFrames(void* outputBuffer, uint32_t frameCount)
6174 {
6275 // logd << "read: " << frameCount << ", total: " << (wireChunk->length / format.frameSize()) << ", idx: " << idx;// << std::endl;
6376 int result = frameCount;
7689
7790 int seek(int frames)
7891 {
79 if ((frames < 0) && (-frames > (int)idx_))
80 frames = -idx_;
92 if ((frames < 0) && (-frames > static_cast<int>(idx_)))
93 frames = -static_cast<int>(idx_);
8194
8295 idx_ += frames;
8396 if (idx_ > getFrameCount())
8598
8699 return idx_;
87100 }
88
89101
90102 chronos::time_point_clk start() const override
91103 {
104116 return std::chrono::duration_cast<T>(chronos::nsec(static_cast<chronos::nsec::rep>(1000000 * getFrameCount() / format.msRate())));
105117 }
106118
119 // void append(const PcmChunk& chunk)
120 // {
121 // auto newSize = payloadSize + chunk.payloadSize;
122 // payload = (char*)realloc(payload, newSize);
123 // memcpy(payload + payloadSize, chunk.payload, chunk.payloadSize);
124 // payloadSize = newSize;
125 // }
126
107127 double durationMs() const
108128 {
109129 return static_cast<double>(getFrameCount()) / format.msRate();
120140 return idx_ >= getFrameCount();
121141 }
122142
123 inline size_t getFrameCount() const
143 inline uint32_t getFrameCount() const
124144 {
125145 return (payloadSize / format.frameSize());
126146 }
127147
128 inline size_t getSampleCount() const
148 inline uint32_t getSampleCount() const
129149 {
130150 return (payloadSize / format.sampleSize());
131151 }
133153 SampleFormat format;
134154
135155 private:
136 uint32_t idx_;
156 uint32_t idx_ = 0;
137157 };
138158 } // namespace msg
139159
3838 class WireChunk : public BaseMessage
3939 {
4040 public:
41 WireChunk(size_t size = 0) : BaseMessage(message_type::kWireChunk), payloadSize(size), payload(nullptr)
41 WireChunk(uint32_t size = 0) : BaseMessage(message_type::kWireChunk), payloadSize(size), payload(nullptr)
4242 {
4343 if (size > 0)
4444 payload = (char*)malloc(size * sizeof(char));
3232 #include <sstream>
3333 #include <stdexcept>
3434 #include <vector>
35 #ifdef WINDOWS
36 #include <cctype>
37 #endif
3538
3639
3740 namespace popl
763766 {
764767 if (this->assign_to_)
765768 {
766 if (this->is_set() || default_)
767 *this->assign_to_ = value();
769 if (!this->is_set() && default_)
770 *this->assign_to_ = *default_;
771 else if (this->is_set())
772 *this->assign_to_ = values_.back();
768773 }
769774 }
770775
3838 auto val = queue_.front();
3939 queue_.pop_front();
4040 return val;
41 }
42
43 T front()
44 {
45 std::unique_lock<std::mutex> mlock(mutex_);
46 while (queue_.empty())
47 cond_.wait(mlock);
48
49 return queue_.front();
5041 }
5142
5243 void abort_wait()
10697 cond_.notify_one();
10798 }
10899
100 bool back_copy(T& copy)
101 {
102 std::lock_guard<std::mutex> mlock(mutex_);
103 if (queue_.empty())
104 return false;
105 copy = queue_.back();
106 return true;
107 }
108
109 bool front_copy(T& copy)
110 {
111 std::lock_guard<std::mutex> mlock(mutex_);
112 if (queue_.empty())
113 return false;
114 copy = queue_.front();
115 return true;
116 }
117
109118 void push_front(T&& item)
110119 {
111120 {
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 "resampler.hpp"
19 #include "common/aixlog.hpp"
20 #include "common/snap_exception.hpp"
21
22 #include <cmath>
23
24 using namespace std;
25
26 static constexpr auto LOG_TAG = "Resampler";
27
28 Resampler::Resampler(const SampleFormat& in_format, const SampleFormat& out_format) : in_format_(in_format), out_format_(out_format)
29 {
30 #ifdef HAS_SOXR
31 soxr_ = nullptr;
32 if ((out_format_.rate() != in_format_.rate()) || (out_format_.bits() != in_format_.bits()))
33 {
34 LOG(INFO, LOG_TAG) << "Resampling from " << in_format_.toString() << " to " << out_format_.toString() << "\n";
35 soxr_error_t error;
36
37 soxr_datatype_t in_type = SOXR_INT16_I;
38 soxr_datatype_t out_type = SOXR_INT16_I;
39 if (in_format_.sampleSize() > 2)
40 in_type = SOXR_INT32_I;
41 if (out_format_.sampleSize() > 2)
42 out_type = SOXR_INT32_I;
43 soxr_io_spec_t iospec = soxr_io_spec(in_type, out_type);
44 // HQ should be fine: http://sox.sourceforge.net/Docs/FAQ
45 soxr_quality_spec_t q_spec = soxr_quality_spec(SOXR_HQ, 0);
46 soxr_ =
47 soxr_create(static_cast<double>(in_format_.rate()), static_cast<double>(out_format_.rate()), in_format_.channels(), &error, &iospec, &q_spec, NULL);
48 if (error)
49 {
50 LOG(ERROR, LOG_TAG) << "Error soxr_create: " << error << "\n";
51 soxr_ = nullptr;
52 }
53 // initialize the buffer with 20ms (~latency of the reampler)
54 resample_buffer_.resize(out_format_.frameSize() * static_cast<uint16_t>(ceil(out_format_.msRate() * 20)));
55 }
56 #else
57 LOG(WARNING, LOG_TAG) << "Soxr not available, resampling not supported\n";
58 if ((out_format_.rate() != in_format_.rate()) || (out_format_.bits() != in_format_.bits()))
59 {
60 throw SnapException("Resampling requested, but not supported");
61 }
62 #endif
63 // resampled_chunk_ = std::make_unique<msg::PcmChunk>(out_format_, 0);
64 }
65
66
67 bool Resampler::resamplingNeeded() const
68 {
69 #ifdef HAS_SOXR
70 return soxr_ != nullptr;
71 #else
72 return false;
73 #endif
74 }
75
76
77 // std::shared_ptr<msg::PcmChunk> Resampler::resample(std::shared_ptr<msg::PcmChunk> chunk, chronos::usec duration)
78 // {
79 // auto resampled_chunk = resample(chunk);
80 // if (!resampled_chunk)
81 // return nullptr;
82 // std::cerr << "1\n";
83 // resampled_chunk_->append(*resampled_chunk);
84 // std::cerr << "2\n";
85 // while (resampled_chunk_->duration<chronos::usec>() >= duration)
86 // {
87 // LOG(DEBUG, LOG_TAG) << "resampled duration: " << resampled_chunk_->durationMs() << ", consuming: " << out_format_.usRate() * duration.count() <<
88 // "\n";
89 // auto chunk = resampled_chunk_->consume(out_format_.usRate() * duration.count());
90 // LOG(DEBUG, LOG_TAG) << "consumed: " << chunk->durationMs() << ", resampled duration: " << resampled_chunk_->durationMs() << "\n";
91 // return chunk;
92 // }
93 // // resampled_chunks_.push_back(resampled_chunk);
94 // // chronos::usec avail;
95 // // for (const auto& chunk: resampled_chunks_)
96 // // {
97 // // avail += chunk->durationLeft<chronos::usec>();
98 // // if (avail >= duration)
99 // // {
100
101 // // }
102 // // }
103 // }
104
105
106 std::shared_ptr<msg::PcmChunk> Resampler::resample(const msg::PcmChunk& chunk)
107 {
108 #ifndef HAS_SOXR
109 return std::make_shared<msg::PcmChunk>(chunk);
110 #else
111 if (!resamplingNeeded())
112 {
113 return std::make_shared<msg::PcmChunk>(chunk);
114 }
115 else
116 {
117 if (in_format_.bits() == 24)
118 {
119 // sox expects 32 bit input, shift 8 bits left
120 int32_t* frames = (int32_t*)chunk.payload;
121 for (size_t n = 0; n < chunk.getSampleCount(); ++n)
122 frames[n] = frames[n] << 8;
123 }
124
125 size_t idone;
126 size_t odone;
127 auto resample_buffer_framesize = resample_buffer_.size() / out_format_.frameSize();
128 auto error = soxr_process(soxr_, chunk.payload, chunk.getFrameCount(), &idone, resample_buffer_.data(), resample_buffer_framesize, &odone);
129 if (error)
130 {
131 LOG(ERROR, LOG_TAG) << "Error soxr_process: " << error << "\n";
132 }
133 else
134 {
135 LOG(TRACE, LOG_TAG) << "Resample idone: " << idone << "/" << chunk.getFrameCount() << ", odone: " << odone << "/"
136 << resample_buffer_.size() / out_format_.frameSize() << ", delay: " << soxr_delay(soxr_) << "\n";
137
138 // some data has been resampled (odone frames) and some is still in the pipe (soxr_delay frames)
139 if (odone > 0)
140 {
141 // get the resampled ts from the input ts
142 auto input_end_ts = chunk.start() + chunk.duration<std::chrono::microseconds>();
143 double resampled_ms = (odone + soxr_delay(soxr_)) / out_format_.msRate();
144 auto resampled_start = input_end_ts - std::chrono::microseconds(static_cast<int>(resampled_ms * 1000.));
145
146 auto resampled_chunk = std::make_shared<msg::PcmChunk>(out_format_, 0);
147 auto us = chrono::duration_cast<chrono::microseconds>(resampled_start.time_since_epoch()).count();
148 resampled_chunk->timestamp.sec = static_cast<int32_t>(us / 1000000);
149 resampled_chunk->timestamp.usec = static_cast<int32_t>(us % 1000000);
150
151 // copy from the resample_buffer to the resampled chunk
152 resampled_chunk->payloadSize = static_cast<uint32_t>(odone * out_format_.frameSize());
153 resampled_chunk->payload = (char*)realloc(resampled_chunk->payload, resampled_chunk->payloadSize);
154 memcpy(resampled_chunk->payload, resample_buffer_.data(), resampled_chunk->payloadSize);
155
156 if (out_format_.bits() == 24)
157 {
158 // sox has quantized to 32 bit, shift 8 bits right
159 int32_t* frames = (int32_t*)resampled_chunk->payload;
160 for (size_t n = 0; n < resampled_chunk->getSampleCount(); ++n)
161 {
162 // +128 to round to the nearest so that quantisation steps are distributed evenly
163 frames[n] = (frames[n] + 128) >> 8;
164 if (frames[n] > 0x7fffffff)
165 frames[n] = 0x7fffffff;
166 }
167 }
168
169 // check if the resample_buffer is large enough, or if soxr was using all available space
170 if (odone == resample_buffer_framesize)
171 {
172 // buffer for resampled data too small, add space for 5ms
173 resample_buffer_.resize(resample_buffer_.size() + out_format_.frameSize() * static_cast<uint16_t>(ceil(out_format_.msRate() * 5)));
174 LOG(DEBUG, LOG_TAG) << "Resample buffer completely filled, adding space for 5ms; new buffer size: " << resample_buffer_.size()
175 << " bytes\n";
176 }
177
178 // //LOG(TRACE, LOG_TAG) << "ts: " << out->timestamp.sec << "s, " << out->timestamp.usec/1000.f << " ms, duration: " << odone / format_.msRate()
179 // << "\n";
180 // int64_t next_us = us + static_cast<int64_t>(odone / format_.msRate() * 1000);
181 // LOG(TRACE, LOG_TAG) << "ts: " << us << ", next: " << next_us << ", diff: " << next_us_ - us << "\n";
182 // next_us_ = next_us;
183
184 return resampled_chunk;
185 }
186 }
187 }
188 return nullptr;
189 #endif
190 }
191
192
193 shared_ptr<msg::PcmChunk> Resampler::resample(shared_ptr<msg::PcmChunk> chunk)
194 {
195 #ifndef HAS_SOXR
196 return chunk;
197 #else
198 if (!resamplingNeeded())
199 {
200 return chunk;
201 }
202 else
203 {
204 return resample(*chunk);
205 }
206 #endif
207 }
208
209
210 Resampler::~Resampler()
211 {
212 #ifdef HAS_SOXR
213 if (soxr_)
214 soxr_delete(soxr_);
215 #endif
216 }
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 RESAMPLER_HPP
19 #define RESAMPLER_HPP
20
21 #include "common/message/pcm_chunk.hpp"
22 #include "common/sample_format.hpp"
23 #include <deque>
24 #include <vector>
25 #ifdef HAS_SOXR
26 #include <soxr.h>
27 #endif
28
29
30 class Resampler
31 {
32 public:
33 Resampler(const SampleFormat& in_format, const SampleFormat& out_format);
34 virtual ~Resampler();
35
36 // std::shared_ptr<msg::PcmChunk> resample(std::shared_ptr<msg::PcmChunk> chunk, chronos::usec duration);
37 std::shared_ptr<msg::PcmChunk> resample(std::shared_ptr<msg::PcmChunk> chunk);
38 std::shared_ptr<msg::PcmChunk> resample(const msg::PcmChunk& chunk);
39 bool resamplingNeeded() const;
40
41 private:
42 std::vector<char> resample_buffer_;
43 // std::unique_ptr<msg::PcmChunk> resampled_chunk_;
44 SampleFormat in_format_;
45 SampleFormat out_format_;
46 #ifdef HAS_SOXR
47 soxr_t soxr_;
48 #endif
49 };
50
51 #endif
4848 }
4949
5050
51 string SampleFormat::getFormat() const
51 string SampleFormat::toString() const
5252 {
5353 stringstream ss;
5454 ss << rate_ << ":" << bits_ << ":" << channels_;
6161 std::vector<std::string> strs;
6262 strs = utils::string::split(format, ':');
6363 if (strs.size() == 3)
64 setFormat(strs[0] == "*" ? 0 : cpt::stoul(strs[0]), strs[1] == "*" ? 0 : cpt::stoul(strs[1]), strs[2] == "*" ? 0 : cpt::stoul(strs[2]));
64 setFormat(strs[0] == "*" ? 0 : cpt::stoul(strs[0]), strs[1] == "*" ? 0 : static_cast<uint16_t>(cpt::stoul(strs[1])),
65 strs[2] == "*" ? 0 : static_cast<uint16_t>(cpt::stoul(strs[2])));
6566 else
6667 throw SnapException("sampleformat must be <rate>:<bits>:<channels>");
6768 }
4040 SampleFormat(const std::string& format);
4141 SampleFormat(uint32_t rate, uint16_t bits, uint16_t channels);
4242
43 std::string getFormat() const;
43 std::string toString() const;
4444
4545 void setFormat(const std::string& format);
4646 void setFormat(uint32_t rate, uint16_t bits, uint16_t channels);
+0
-52
common/signal_handler.hpp less more
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 SIGNAL_HANDLER_HPP
19 #define SIGNAL_HANDLER_HPP
20
21 #include <functional>
22 #include <future>
23 #include <set>
24 #include <signal.h>
25
26 using signal_callback = std::function<void(int signal, const std::string& name)>;
27
28 static std::future<int> install_signal_handler(std::set<int> signals, const signal_callback& on_signal = nullptr)
29 {
30 static std::promise<int> promise;
31 std::future<int> future = promise.get_future();
32 static signal_callback callback = on_signal;
33
34 for (auto signal : signals)
35 {
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 });
47 }
48 return future;
49 }
50
51 #endif
1515 along with this program. If not, see <http://www.gnu.org/licenses/>.
1616 ***/
1717
18 #ifndef SNAP_EXCEPTION_H
19 #define SNAP_EXCEPTION_H
18 #ifndef SNAP_EXCEPTION_HPP
19 #define SNAP_EXCEPTION_HPP
2020
2121 #include <cstring> // std::strlen, std::strcpy
2222 #include <exception>
2626 class SnapException : public std::exception
2727 {
2828 std::string text_;
29 int error_code_;
2930
3031 public:
31 SnapException(const char* text) : text_(text)
32 SnapException(const char* text, int error_code = 0) : text_(text), error_code_(error_code)
3233 {
3334 }
3435
35 SnapException(const std::string& text) : SnapException(text.c_str())
36 SnapException(const std::string& text, int error_code = 0) : SnapException(text.c_str(), error_code)
3637 {
3738 }
3839
3940 ~SnapException() throw() override = default;
41
42 int code() const noexcept
43 {
44 return error_code_;
45 }
4046
4147 const char* what() const noexcept override
4248 {
4551 };
4652
4753
48
49 class AsyncSnapException : public SnapException
50 {
51 public:
52 AsyncSnapException(const char* text) : SnapException(text)
53 {
54 }
55
56 AsyncSnapException(const std::string& text) : SnapException(text)
57 {
58 }
59
60 ~AsyncSnapException() throw() override = default;
61 };
62
63
64
6554 #endif
1919 #define TIME_DEFS_H
2020
2121 #include <chrono>
22 #include <sys/time.h>
2322 #include <thread>
2423 #ifdef MACOS
2524 #include <mach/clock.h>
2625 #include <mach/mach.h>
2726 #endif
2827
28 #ifndef WINDOWS
29 #include <sys/time.h>
30 #else
31 #include <Windows.h>
32 #include <stdint.h>
33 #include <winsock2.h>
34 #endif
35
2936 namespace chronos
3037 {
31 using clk = std::chrono::steady_clock;
38 using clk =
39 #ifndef WINDOWS
40 std::chrono::steady_clock;
41 #else
42 std::chrono::system_clock;
43 #endif
3244 using time_point_clk = std::chrono::time_point<clk>;
3345 using sec = std::chrono::seconds;
3446 using msec = std::chrono::milliseconds;
4052 {
4153 auto now = Clock::now();
4254 auto microsecs = std::chrono::duration_cast<std::chrono::microseconds>(now.time_since_epoch());
43 tv->tv_sec = microsecs.count() / 1000000;
44 tv->tv_usec = microsecs.count() % 1000000;
55 tv->tv_sec = static_cast<long>(microsecs.count() / 1000000);
56 tv->tv_usec = static_cast<long>(microsecs.count() % 1000000);
4557 }
58
59 #ifdef WINDOWS
60 // Implementation from http://stackoverflow.com/a/26085827/2510022
61 inline static int gettimeofday(struct timeval* tp, struct timezone* tzp)
62 {
63 // Note: some broken versions only have 8 trailing zero's, the correct epoch has 9 trailing zero's
64 static const uint64_t EPOCH = ((uint64_t)116444736000000000ULL);
65
66 SYSTEMTIME system_time;
67 FILETIME file_time;
68 uint64_t time;
69
70 GetSystemTime(&system_time);
71 SystemTimeToFileTime(&system_time, &file_time);
72 time = ((uint64_t)file_time.dwLowDateTime);
73 time += ((uint64_t)file_time.dwHighDateTime) << 32;
74
75 tp->tv_sec = (long)((time - EPOCH) / 10000000L);
76 tp->tv_usec = (long)(system_time.wMilliseconds * 1000);
77 return 0;
78 }
79 #endif
4680
4781 inline static void steadytimeofday(struct timeval* tv)
4882 {
83 #ifndef WINDOWS
4984 timeofday<clk>(tv);
85 #else
86 gettimeofday(tv, NULL);
87 #endif
5088 }
5189
5290 inline static void systemtimeofday(struct timeval* tv)
67105 return std::chrono::duration_cast<ToDuration>(std::chrono::seconds(sec) + std::chrono::microseconds(usec));
68106 }
69107
70
71 inline static void addUs(timeval& tv, int us)
72 {
73 if (us < 0)
74 {
75 timeval t;
76 t.tv_sec = -us / 1000000;
77 t.tv_usec = (-us % 1000000);
78 timersub(&tv, &t, &tv);
79 return;
80 }
81 tv.tv_usec += us;
82 tv.tv_sec += (tv.tv_usec / 1000000);
83 tv.tv_usec %= 1000000;
84 }
85
86108 inline static long getTickCount()
87109 {
88 #ifdef MACOS
110 #if defined(MACOS)
89111 clock_serv_t cclock;
90112 mach_timespec_t mts;
91113 host_get_clock_service(mach_host_self(), SYSTEM_CLOCK, &cclock);
92114 clock_get_time(cclock, &mts);
93115 mach_port_deallocate(mach_task_self(), cclock);
94116 return mts.tv_sec * 1000 + mts.tv_nsec / 1000000;
117 #elif defined(WINDOWS)
118 return getTickCount();
95119 #else
96120 struct timespec now;
97121 clock_gettime(CLOCK_MONOTONIC, &now);
2020
2121 #include "string_utils.hpp"
2222 #include <fstream>
23 #ifndef WINDOWS
2324 #include <grp.h>
2425 #include <pwd.h>
26 #endif
2527 #include <stdexcept>
2628 #include <vector>
2729
3840 return infile.good();
3941 }
4042
41
43 #ifndef WINDOWS
4244 static void do_chown(const std::string& file_path, const std::string& user_name, const std::string& group_name)
4345 {
4446 if (user_name.empty() && group_name.empty())
8789 }
8890 return res;
8991 }
90
92 #endif
9193 } // namespace file
9294 } // namespace utils
9395
1919 #define STRING_UTILS_H
2020
2121 #include <algorithm>
22 #include <map>
2223 #include <sstream>
2324 #include <stdio.h>
2425 #include <string>
2526 #include <vector>
27 #ifdef WINDOWS
28 #include <cctype>
29 #endif
2630
2731 namespace utils
2832 {
110114 }
111115
112116
117 static std::string split_left(const std::string& s, char delim, std::string& right)
118 {
119 std::string left;
120 split_left(s, delim, left, right);
121 return left;
122 }
123
124
113125
114126 static std::vector<std::string>& split(const std::string& s, char delim, std::vector<std::string>& elems)
115127 {
130142 return elems;
131143 }
132144
145
146 static std::map<std::string, std::string> split_pairs(const std::string& s, char pair_delim, char key_value_delim)
147 {
148 std::map<std::string, std::string> result;
149 auto keyValueList = split(s, pair_delim);
150 for (auto& kv : keyValueList)
151 {
152 auto pos = kv.find(key_value_delim);
153 if (pos != std::string::npos)
154 {
155 std::string key = trim_copy(kv.substr(0, pos));
156 std::string value = trim_copy(kv.substr(pos + 1));
157 result[key] = value;
158 }
159 }
160 return result;
161 }
162
163
133164 } // namespace string
134165 } // namespace utils
135166
2323
2424 #include <cctype>
2525 #include <cerrno>
26 // #include <chrono>
2627 #include <cstring>
2728 #include <fstream>
2829 #include <functional>
2930 #include <iomanip>
30 #include <iomanip>
3131 #include <iterator>
3232 #include <locale>
3333 #include <memory>
34 #ifndef WINDOWS
3435 #include <net/if.h>
3536 #include <netinet/in.h>
37 #include <sys/ioctl.h>
38 #include <sys/utsname.h>
39 #include <unistd.h>
40 #endif
3641 #include <sstream>
3742 #include <string>
38 #include <sys/ioctl.h>
3943 #include <sys/stat.h>
4044 #include <sys/types.h>
41 #include <unistd.h>
4245 #include <vector>
43 #ifndef FREEBSD
46 #if !defined(WINDOWS) && !defined(FREEBSD)
4447 #include <sys/sysinfo.h>
4548 #endif
46 #include <sys/utsname.h>
4749 #ifdef MACOS
4850 #include <IOKit/IOCFPlugIn.h>
4951 #include <IOKit/IOTypes.h>
5355 #ifdef ANDROID
5456 #include <sys/system_properties.h>
5557 #endif
58 #ifdef WINDOWS
59 #include <chrono>
60 #include <direct.h>
61 #include <iphlpapi.h>
62 #include <versionhelpers.h>
63 #include <windows.h>
64 #include <winsock2.h>
65 #endif
5666
5767
5868 namespace strutils = utils::string;
5969
6070
61
71 #ifndef WINDOWS
6272 static std::string execGetOutput(const std::string& cmd)
6373 {
6474 std::shared_ptr<FILE> pipe(popen((cmd + " 2> /dev/null").c_str(), "r"), pclose);
7383 }
7484 return strutils::trim(result);
7585 }
86 #endif
7687
7788
7889 #ifdef ANDROID
89100
90101 static std::string getOS()
91102 {
92 std::string os;
103 static std::string os("");
104
105 if (!os.empty())
106 return os;
107
93108 #ifdef ANDROID
94109 os = strutils::trim_copy("Android " + getProp("ro.build.version.release"));
110 #elif WINDOWS
111 if (/*IsWindows10OrGreater()*/ FALSE)
112 os = "Windows 10";
113 else if (IsWindows8Point1OrGreater())
114 os = "Windows 8.1";
115 else if (IsWindows8OrGreater())
116 os = "Windows 8";
117 else if (IsWindows7SP1OrGreater())
118 os = "Windows 7 SP1";
119 else if (IsWindows7OrGreater())
120 os = "Windows 7";
121 else if (IsWindowsVistaSP2OrGreater())
122 os = "Windows Vista SP2";
123 else if (IsWindowsVistaSP1OrGreater())
124 os = "Windows Vista SP1";
125 else if (IsWindowsVistaOrGreater())
126 os = "Windows Vista";
127 else if (IsWindowsXPSP3OrGreater())
128 os = "Windows XP SP3";
129 else if (IsWindowsXPSP2OrGreater())
130 os = "Windows XP SP2";
131 else if (IsWindowsXPSP1OrGreater())
132 os = "Windows XP SP1";
133 else if (IsWindowsXPOrGreater())
134 os = "Windows XP";
135 else
136 os = "Unknown Windows";
95137 #else
96138 os = execGetOutput("lsb_release -d");
97139 if ((os.find(":") != std::string::npos) && (os.find("lsb_release") == std::string::npos))
98140 os = strutils::trim_copy(os.substr(os.find(":") + 1));
99141 #endif
142
143 #ifndef WINDOWS
100144 if (os.empty())
101145 {
102146 os = strutils::trim_copy(execGetOutput("grep /etc/os-release /etc/openwrt_release -e PRETTY_NAME -e DISTRIB_DESCRIPTION"));
113157 uname(&u);
114158 os = u.sysname;
115159 }
116 return strutils::trim_copy(os);
160 #endif
161 strutils::trim(os);
162 return os;
117163 }
118164
119165
142188 if (!arch.empty())
143189 return arch;
144190 #endif
191 #ifndef WINDOWS
145192 arch = execGetOutput("arch");
146193 if (arch.empty())
147194 arch = execGetOutput("uname -i");
148195 if (arch.empty() || (arch == "unknown"))
149196 arch = execGetOutput("uname -m");
197 #else
198 SYSTEM_INFO sysInfo;
199 GetSystemInfo(&sysInfo);
200 switch (sysInfo.wProcessorArchitecture)
201 {
202 case PROCESSOR_ARCHITECTURE_AMD64:
203 arch = "amd64";
204 break;
205
206 case PROCESSOR_ARCHITECTURE_ARM:
207 arch = "arm";
208 break;
209
210 case PROCESSOR_ARCHITECTURE_IA64:
211 arch = "ia64";
212 break;
213
214 case PROCESSOR_ARCHITECTURE_INTEL:
215 arch = "intel";
216 break;
217
218 default:
219 case PROCESSOR_ARCHITECTURE_UNKNOWN:
220 arch = "unknown";
221 break;
222 }
223 #endif
150224 return strutils::trim_copy(arch);
151225 }
152226
153
154 static long uptime()
155 {
156 #ifndef FREEBSD
157 struct sysinfo info;
158 sysinfo(&info);
159 return info.uptime;
160 #else
161 std::string uptime = execGetOutput("sysctl kern.boottime");
162 if ((uptime.find(" sec = ") != std::string::npos) && (uptime.find(",") != std::string::npos))
163 {
164 uptime = strutils::trim_copy(uptime.substr(uptime.find(" sec = ") + 7));
165 uptime.resize(uptime.find(","));
166 timeval now;
167 gettimeofday(&now, NULL);
168 try
169 {
170 return now.tv_sec - cpt::stoul(uptime);
171 }
172 catch (...)
173 {
174 }
175 }
176 return 0;
177 #endif
178 }
227 // Seems not to be used
228 // static std::chrono::seconds uptime()
229 // {
230 // #ifndef WINDOWS
231 // #ifndef FREEBSD
232 // struct sysinfo info;
233 // sysinfo(&info);
234 // return std::chrono::seconds(info.uptime);
235 // #else
236 // std::string uptime = execGetOutput("sysctl kern.boottime");
237 // if ((uptime.find(" sec = ") != std::string::npos) && (uptime.find(",") != std::string::npos))
238 // {
239 // uptime = strutils::trim_copy(uptime.substr(uptime.find(" sec = ") + 7));
240 // uptime.resize(uptime.find(","));
241 // timeval now;
242 // gettimeofday(&now, NULL);
243 // try
244 // {
245 // return std::chrono::seconds(now.tv_sec - cpt::stoul(uptime));
246 // }
247 // catch (...)
248 // {
249 // }
250 // }
251 // return 0s;
252 // #endif
253 // #else
254 // return std::chrono::duration_cast<std::chrono::seconds>(std::chrono::milliseconds(GetTickCount()));
255 // #endif
256 // }
179257
180258
181259 /// http://stackoverflow.com/questions/2174768/generating-random-uuids-in-linux
184262 static bool initialized(false);
185263 if (!initialized)
186264 {
187 std::srand(std::time(nullptr));
265 std::srand(static_cast<unsigned int>(std::time(nullptr)));
188266 initialized = true;
189267 }
190268 std::stringstream ss;
195273 }
196274
197275
276 #ifndef WINDOWS
198277 /// https://gist.github.com/OrangeTide/909204
199278 static std::string getMacAddress(int sock)
200279 {
300379 #endif
301380 return mac;
302381 }
303
382 #else
383 static std::string getMacAddress(const std::string& address)
384 {
385 IP_ADAPTER_INFO* first;
386 IP_ADAPTER_INFO* pos;
387 ULONG bufferLength = sizeof(IP_ADAPTER_INFO);
388 first = (IP_ADAPTER_INFO*)malloc(bufferLength);
389
390 if (GetAdaptersInfo(first, &bufferLength) == ERROR_BUFFER_OVERFLOW)
391 {
392 free(first);
393 first = (IP_ADAPTER_INFO*)malloc(bufferLength);
394 }
395
396 char mac[19];
397 if (GetAdaptersInfo(first, &bufferLength) == NO_ERROR)
398 for (pos = first; pos != NULL; pos = pos->Next)
399 {
400 IP_ADDR_STRING* firstAddr = &pos->IpAddressList;
401 IP_ADDR_STRING* posAddr;
402 for (posAddr = firstAddr; posAddr != NULL; posAddr = posAddr->Next)
403 if (_stricmp(posAddr->IpAddress.String, address.c_str()) == 0)
404 {
405 sprintf(mac, "%02x:%02x:%02x:%02x:%02x:%02x", pos->Address[0], pos->Address[1], pos->Address[2], pos->Address[3], pos->Address[4],
406 pos->Address[5]);
407
408 free(first);
409 return mac;
410 }
411 }
412 else
413 free(first);
414
415 return mac;
416 }
417 #endif
304418
305419 static std::string getHostId(const std::string defaultId = "")
306420 {
88 ```bash
99 curl --header "Content-Type: application/json" --request POST --data '{"id":1, "jsonrpc":"2.0", "method": "Server.GetStatus"}' http://127.0.0.1:1780/jsonrpc | jq .result.server.groups[].clients[].config.name
1010 ```
11
12 ### Set latency
13
14 ```bash
15 curl --header "Content-Type: application/json" --request POST --data '{"id":7,"jsonrpc":"2.0","method":"Client.SetLatency","params":{"id":"713aefd7-e6cb-4c3b-9f5e-91bbcf9bcbc2","latency":10}}' http://127.0.0.1:1780/jsonrpc
16 ```
17
18 ### Remove all disconnected clients
19 ```bash
20 curl -s -H "Content-Type: application/json" -d '{"id":1, "jsonrpc":"2.0", "method": "Server.GetStatus"}' http://127.0.0.1:1780/jsonrpc | jq '.result.server.groups[].clients[] | select(.connected==false) .id' | while read ln; do curl -s -H "Content-Type: application/json" -d '{"id":1, "jsonrpc":"2.0", "method": "Server.DeleteClient", "params": {"id":'$ln'}}' http://127.0.0.1:1780/jsonrpc; done
21 ```
0 snapcast (0.22.0-1) unstable; urgency=medium
1
2 * Features
3 -Server: Add Meta stream source (Issue #402, #569, #666)
4 -Client: Add file audio backend (Issue #681)
5 * Bugfixes
6 -Add missing define for alsa stream to makefile (Issue #692)
7 -Fix playback when plugging the headset on Android (Issue #699)
8 -Server discards old chunks if not consumed (Issue #708)
9 * General
10 -Less verbose logging during pipe reconnects (Issue #696)
11 -Add null encoder for streams used only as input for meta streams
12 -Snapweb: Change latency range to [-10s, 10s] (Issue #695)
13 -Update Snapweb, including PR #11, #12, #13, Issues #16, #17
14
15 -- Johannes Pohl <snapcast@badaix.de> Thu, 15 Oct 2020 00:13:37 +0200
16
17 snapcast (0.21.0-1) unstable; urgency=medium
18
19 * Features
20 -Server: Support for WebSocket streaming clients
21 -Server: Install Snapweb web client (Issue #579)
22 -Server: Resample input to 48000:16:2 when using opus codec
23 -Server: Add Alsa stream source
24 * Bugfixes
25 -make install will setup the snapserver home dir (Issue #643)
26 -Client retries to open a blocked alsa device (Issue #652)
27 * General
28 -debian packag generation switched from make to CMake buildsystem
29 -Reintroduce MACOS define, hopefully not breaking anything on macOS
30 -Snapcast uses GitHub actions for automated CI/CD
31 -CMake installs man files (Issue #507)
32 -Update documentation (Issue #615, #617)
33
34 -- Johannes Pohl <snapcast@badaix.de> Sun, 13 Sep 2020 00:13:37 +0200
35
36 snapcast (0.20.0-1) unstable; urgency=medium
37
38 * Features
39 -Client: Windows support (Issue #24)
40 -Client: add hardware mixer (Issue #318)
41 -Client: add "script" and "none" mixer (Issue #302)
42 -Client: add sharingmode for audio device (if supported)
43 -Logging: configurable sink and filters (Issue #30, #561, #122, #559)
44 -Librespot: add option "disable-audio-cache=[false|true]"
45 * Bugfixes
46 -Fix build failure on FreeBSD (Issue #565)
47 -Fix calling lsb_release multiple times (Issue #470)
48 -Client: high CPU load and crash during playback (Issue #609, #628)
49 -Client: improved handling of USB audio disconnects (Issue #64)
50 -Client: latency is forgotten (Issue #476, #588, Snapdroid #11)
51 -Client: fix segfault on mac when playback is paused (Issue #560)
52 -Client: fix bonjour on mac reports empty IP (Issue #632)
53 -Client: fix buzzing tone on Android (Issue #23, #24)
54 -Server: fix crash if client disconnects during connect (Issue #639)
55 -Server: fix reading metadata from shairport-sync (Issue #624)
56 -Server: fix crash on FreeBSD if settings.json is empty (Issue #620)
57 -Server: fix warning about unknown command line options (Issue #635)
58 -Readme: openWrt documentation (Issue #633)
59 -Fix setting the daemon's process priority (PR #448)
60 * General
61 -Client: use less threads and thus less ressources
62 -Update links to xiph externals (Issue #637, PR #616)
63
64 -- Johannes Pohl <snapcast@badaix.de> Sat, 13 Jun 2020 00:13:37 +0200
65
066 snapcast (0.19.0-1) unstable; urgency=medium
167
268 * Features
44 include /usr/share/dpkg/buildflags.mk
55
66 %:
7 dh $@
7 dh $@ --buildsystem=cmake
88
9 override_dh_auto_build:
10 dh_auto_build -- STRIP=: DEBUG= ADD_CFLAGS="$(CXXFLAGS) $(CPPFLAGS)" ADD_LDFLAGS="$(LDFLAGS)"
9 override_dh_auto_configure:
10 dh_auto_configure -- $(CMAKEFLAGS)
1111
12 override_dh_auto_install:
13 dh_auto_install --sourcedir=client --destdir=debian/snapclient/
14 dh_auto_install --sourcedir=server --destdir=debian/snapserver/
12 #override_dh_auto_build:
13 # dh_auto_build -- STRIP=: DEBUG= ADD_CFLAGS="$(CXXFLAGS) $(CPPFLAGS)" ADD_LDFLAGS="$(LDFLAGS)"
2525
2626 # Read configuration variable file if it is present
2727 [ -r /etc/default/$NAME ] && . /etc/default/$NAME
28 SNAPCLIENT_OPTS="--daemon $SNAPCLIENT_OPTS"
28 SNAPCLIENT_OPTS="--daemon --logsink=system --user $USERNAME:$USERNAME $SNAPCLIENT_OPTS"
2929
3030 if [ "$START_SNAPCLIENT" != "true" ] ; then
3131 exit 0
0 usr/bin/snapclient usr/bin/
0 client/snapclient.1
55
66 [Service]
77 EnvironmentFile=-/etc/default/snapclient
8 ExecStart=/usr/bin/snapclient $SNAPCLIENT_OPTS
8 ExecStart=/usr/bin/snapclient --logsink=system $SNAPCLIENT_OPTS
99 User=snapclient
1010 Group=snapclient
11 # very noisy on stdout
12 StandardOutput=null
1311 Restart=on-failure
1412
1513 [Install]
2525
2626 # Read configuration variable file if it is present
2727 [ -r /etc/default/$NAME ] && . /etc/default/$NAME
28 SNAPSERVER_OPTS="--daemon $SNAPSERVER_OPTS"
28 SNAPSERVER_OPTS="--daemon --logging.sink=system --server.datadir=$USERNAME $SNAPSERVER_OPTS"
2929
3030 if [ "$START_SNAPSERVER" != "true" ] ; then
3131 exit 0
0 usr/bin/snapserver usr/bin/
1 usr/share/snapserver usr/share/
02 server/etc/snapserver.conf etc/
0 server/snapserver.1
55 HOMEDIR=/var/lib/snapserver
66
77 if [ "$1" = configure ]; then
8 adduser --system --quiet --group --home $HOMEDIR --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 # adduser $USERNAME audio
11 fi
12 adduser --quiet $USERNAME audio > /dev/null || true
913
1014 if [ ! -d $HOMEDIR ]; then
1115 mkdir -m 0750 $HOMEDIR
55
66 [Service]
77 EnvironmentFile=-/etc/default/snapserver
8 ExecStart=/usr/bin/snapserver $SNAPSERVER_OPTS
8 ExecStart=/usr/bin/snapserver --logging.sink=system --server.datadir=${HOME} $SNAPSERVER_OPTS
99 User=snapserver
1010 Group=snapserver
1111 Restart=on-failure
Binary diff not shown
0 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
1 <!-- Created with Inkscape (http://www.inkscape.org/) -->
2
3 <svg
4 xmlns:dc="http://purl.org/dc/elements/1.1/"
5 xmlns:cc="http://creativecommons.org/ns#"
6 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
7 xmlns:svg="http://www.w3.org/2000/svg"
8 xmlns="http://www.w3.org/2000/svg"
9 xmlns:xlink="http://www.w3.org/1999/xlink"
10 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
11 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
12 width="250mm"
13 height="200mm"
14 viewBox="0 0 885.82677 708.66141"
15 id="svg2"
16 version="1.1"
17 inkscape:version="0.91 r13725"
18 sodipodi:docname="Overview.svg"
19 inkscape:export-filename="Overview.png"
20 inkscape:export-xdpi="90"
21 inkscape:export-ydpi="90">
22 <defs
23 id="defs4">
24 <marker
25 inkscape:stockid="Arrow2Mend"
26 orient="auto"
27 refY="0"
28 refX="0"
29 id="Arrow2Mend"
30 style="overflow:visible"
31 inkscape:isstock="true">
32 <path
33 id="path4169"
34 style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
35 d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
36 transform="scale(-0.6,-0.6)"
37 inkscape:connector-curvature="0" />
38 </marker>
39 <marker
40 inkscape:stockid="Arrow1Send"
41 orient="auto"
42 refY="0"
43 refX="0"
44 id="Arrow1Send"
45 style="overflow:visible"
46 inkscape:isstock="true">
47 <path
48 id="path4157"
49 d="M 0,0 5,-5 -12.5,0 5,5 0,0 Z"
50 style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1"
51 transform="matrix(-0.2,0,0,-0.2,-1.2,0)"
52 inkscape:connector-curvature="0" />
53 </marker>
54 <marker
55 inkscape:stockid="Arrow1Mstart"
56 orient="auto"
57 refY="0"
58 refX="0"
59 id="Arrow1Mstart"
60 style="overflow:visible"
61 inkscape:isstock="true">
62 <path
63 id="path4148"
64 d="M 0,0 5,-5 -12.5,0 5,5 0,0 Z"
65 style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
66 transform="matrix(0.4,0,0,0.4,4,0)"
67 inkscape:connector-curvature="0" />
68 </marker>
69 <marker
70 inkscape:stockid="Arrow1Lend"
71 orient="auto"
72 refY="0"
73 refX="0"
74 id="marker4570"
75 style="overflow:visible"
76 inkscape:isstock="true">
77 <path
78 id="path4572"
79 d="M 0,0 5,-5 -12.5,0 5,5 0,0 Z"
80 style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1"
81 transform="matrix(-0.8,0,0,-0.8,-10,0)"
82 inkscape:connector-curvature="0" />
83 </marker>
84 <marker
85 inkscape:stockid="Arrow1Lstart"
86 orient="auto"
87 refY="0"
88 refX="0"
89 id="marker4566"
90 style="overflow:visible"
91 inkscape:isstock="true">
92 <path
93 id="path4568"
94 d="M 0,0 5,-5 -12.5,0 5,5 0,0 Z"
95 style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
96 transform="matrix(0.8,0,0,0.8,10,0)"
97 inkscape:connector-curvature="0" />
98 </marker>
99 <marker
100 inkscape:stockid="Arrow1Lstart"
101 orient="auto"
102 refY="0"
103 refX="0"
104 id="marker4520"
105 style="overflow:visible"
106 inkscape:isstock="true">
107 <path
108 id="path4522"
109 d="M 0,0 5,-5 -12.5,0 5,5 0,0 Z"
110 style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
111 transform="matrix(0.8,0,0,0.8,10,0)"
112 inkscape:connector-curvature="0" />
113 </marker>
114 <marker
115 inkscape:stockid="Arrow1Lstart"
116 orient="auto"
117 refY="0"
118 refX="0"
119 id="marker4480"
120 style="overflow:visible"
121 inkscape:isstock="true">
122 <path
123 id="path4482"
124 d="M 0,0 5,-5 -12.5,0 5,5 0,0 Z"
125 style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
126 transform="matrix(0.8,0,0,0.8,10,0)"
127 inkscape:connector-curvature="0" />
128 </marker>
129 <marker
130 inkscape:stockid="Arrow1Lstart"
131 orient="auto"
132 refY="0"
133 refX="0"
134 id="marker4446"
135 style="overflow:visible"
136 inkscape:isstock="true">
137 <path
138 id="path4448"
139 d="M 0,0 5,-5 -12.5,0 5,5 0,0 Z"
140 style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
141 transform="matrix(0.8,0,0,0.8,10,0)"
142 inkscape:connector-curvature="0" />
143 </marker>
144 <marker
145 inkscape:stockid="Arrow1Lend"
146 orient="auto"
147 refY="0"
148 refX="0"
149 id="marker4418"
150 style="overflow:visible"
151 inkscape:isstock="true">
152 <path
153 id="path4420"
154 d="M 0,0 5,-5 -12.5,0 5,5 0,0 Z"
155 style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1"
156 transform="matrix(-0.8,0,0,-0.8,-10,0)"
157 inkscape:connector-curvature="0" />
158 </marker>
159 <marker
160 inkscape:stockid="Arrow1Lstart"
161 orient="auto"
162 refY="0"
163 refX="0"
164 id="marker4414"
165 style="overflow:visible"
166 inkscape:isstock="true">
167 <path
168 id="path4416"
169 d="M 0,0 5,-5 -12.5,0 5,5 0,0 Z"
170 style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
171 transform="matrix(0.8,0,0,0.8,10,0)"
172 inkscape:connector-curvature="0" />
173 </marker>
174 <marker
175 inkscape:stockid="Arrow1Lend"
176 orient="auto"
177 refY="0"
178 refX="0"
179 id="Arrow1Lend"
180 style="overflow:visible"
181 inkscape:isstock="true">
182 <path
183 id="path4145"
184 d="M 0,0 5,-5 -12.5,0 5,5 0,0 Z"
185 style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
186 transform="matrix(-0.8,0,0,-0.8,-10,0)"
187 inkscape:connector-curvature="0" />
188 </marker>
189 <marker
190 inkscape:stockid="Arrow1Lstart"
191 orient="auto"
192 refY="0"
193 refX="0"
194 id="Arrow1Lstart"
195 style="overflow:visible"
196 inkscape:isstock="true">
197 <path
198 id="path4142"
199 d="M 0,0 5,-5 -12.5,0 5,5 0,0 Z"
200 style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1"
201 transform="matrix(0.8,0,0,0.8,10,0)"
202 inkscape:connector-curvature="0" />
203 </marker>
204 <marker
205 inkscape:stockid="Arrow2Mend"
206 orient="auto"
207 refY="0"
208 refX="0"
209 id="Arrow2Mend-6"
210 style="overflow:visible"
211 inkscape:isstock="true">
212 <path
213 inkscape:connector-curvature="0"
214 id="path4169-1"
215 style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
216 d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
217 transform="scale(-0.6,-0.6)" />
218 </marker>
219 <marker
220 inkscape:stockid="Arrow2Mend"
221 orient="auto"
222 refY="0"
223 refX="0"
224 id="Arrow2Mend-0"
225 style="overflow:visible"
226 inkscape:isstock="true">
227 <path
228 inkscape:connector-curvature="0"
229 id="path4169-2"
230 style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
231 d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
232 transform="scale(-0.6,-0.6)" />
233 </marker>
234 <marker
235 inkscape:stockid="Arrow2Mend"
236 orient="auto"
237 refY="0"
238 refX="0"
239 id="Arrow2Mend-0-6"
240 style="overflow:visible"
241 inkscape:isstock="true">
242 <path
243 inkscape:connector-curvature="0"
244 id="path4169-2-8"
245 style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
246 d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
247 transform="scale(-0.6,-0.6)" />
248 </marker>
249 <marker
250 inkscape:stockid="Arrow2Mend"
251 orient="auto"
252 refY="0"
253 refX="0"
254 id="Arrow2Mend-0-9"
255 style="overflow:visible"
256 inkscape:isstock="true">
257 <path
258 inkscape:connector-curvature="0"
259 id="path4169-2-5"
260 style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
261 d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
262 transform="scale(-0.6,-0.6)" />
263 </marker>
264 </defs>
265 <sodipodi:namedview
266 id="base"
267 pagecolor="#ffffff"
268 bordercolor="#666666"
269 borderopacity="1.0"
270 inkscape:pageopacity="0.0"
271 inkscape:pageshadow="2"
272 inkscape:zoom="1"
273 inkscape:cx="431.85737"
274 inkscape:cy="354.9223"
275 inkscape:document-units="px"
276 inkscape:current-layer="layer1"
277 showgrid="true"
278 inkscape:snap-page="false"
279 inkscape:snap-object-midpoints="false"
280 inkscape:snap-others="false"
281 inkscape:snap-nodes="false"
282 inkscape:snap-center="false"
283 inkscape:snap-midpoints="true"
284 inkscape:object-paths="true"
285 inkscape:window-width="1440"
286 inkscape:window-height="847"
287 inkscape:window-x="0"
288 inkscape:window-y="0"
289 inkscape:window-maximized="1"
290 inkscape:snap-bbox="true"
291 inkscape:bbox-nodes="true"
292 inkscape:bbox-paths="true">
293 <inkscape:grid
294 type="xygrid"
295 id="grid5639" />
296 </sodipodi:namedview>
297 <metadata
298 id="metadata7">
299 <rdf:RDF>
300 <cc:Work
301 rdf:about="">
302 <dc:format>image/svg+xml</dc:format>
303 <dc:type
304 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
305 <dc:title />
306 </cc:Work>
307 </rdf:RDF>
308 </metadata>
309 <g
310 inkscape:label="Ebene 1"
311 inkscape:groupmode="layer"
312 id="layer1"
313 transform="translate(0,-343.70079)">
314 <rect
315 style="fill:#c8c8c8;fill-opacity:1;fill-rule:evenodd;stroke:#c8c8c8;stroke-width:12.12900639;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
316 id="rect4136"
317 width="327.871"
318 height="227.871"
319 x="46.064503"
320 y="458.4267" />
321 <text
322 xml:space="preserve"
323 style="font-style:normal;font-weight:normal;font-size:20px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
324 x="54.564339"
325 y="479.50381"
326 id="text4636"
327 sodipodi:linespacing="125%"><tspan
328 sodipodi:role="line"
329 id="tspan4638"
330 x="54.564339"
331 y="479.50381"
332 style="font-size:15px">Server</tspan></text>
333 <rect
334 style="fill:#ffd42a;fill-opacity:1;fill-rule:evenodd;stroke:#607d8b;stroke-width:4.96148014;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
335 id="rect4640"
336 width="115.15193"
337 height="182.91074"
338 x="237.36732"
339 y="486.97073" />
340 <text
341 xml:space="preserve"
342 style="font-style:normal;font-weight:normal;font-size:12.5px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
343 x="252.7113"
344 y="507.64835"
345 id="text4636-3"
346 sodipodi:linespacing="125%"><tspan
347 sodipodi:role="line"
348 x="252.7113"
349 y="507.64835"
350 id="tspan4660">Snapserver</tspan></text>
351 <rect
352 style="fill:#c2e4ff;fill-opacity:1;fill-rule:evenodd;stroke:#607d8b;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
353 id="rect4640-7"
354 width="79.999985"
355 height="39.999989"
356 x="53.76844"
357 y="508.371" />
358 <text
359 xml:space="preserve"
360 style="font-style:normal;font-weight:normal;font-size:11.25px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
361 x="62.33633"
362 y="544.72687"
363 id="text4636-3-5"
364 sodipodi:linespacing="125%"
365 transform="scale(1.024182,0.97638896)"><tspan
366 sodipodi:role="line"
367 x="62.33633"
368 y="544.72687"
369 id="tspan4660-3">MPD/UPnP</tspan></text>
370 <path
371 style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow2Mend)"
372 d="m 133.76844,528.37099 100,0"
373 id="path5641"
374 inkscape:connector-curvature="0" />
375 <text
376 xml:space="preserve"
377 style="font-style:normal;font-weight:normal;font-size:12.5px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
378 x="149.5442"
379 y="522.92725"
380 id="text4636-3-5-5"
381 sodipodi:linespacing="125%"><tspan
382 sodipodi:role="line"
383 x="149.5442"
384 y="522.92725"
385 id="tspan4660-3-6"
386 style="font-size:11.25px">Alsa</tspan></text>
387 <text
388 xml:space="preserve"
389 style="font-style:normal;font-weight:normal;font-size:12.5px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
390 x="295.77576"
391 y="336.91849"
392 id="text4636-3-5-5-2"
393 sodipodi:linespacing="125%"><tspan
394 sodipodi:role="line"
395 x="295.77576"
396 y="336.91849"
397 id="tspan4660-3-6-9" /></text>
398 <rect
399 style="fill:#c2e4ff;fill-opacity:1;fill-rule:evenodd;stroke:#607d8b;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
400 id="rect4640-7-0"
401 width="79.999985"
402 height="39.999989"
403 x="53.76844"
404 y="568.37097" />
405 <text
406 xml:space="preserve"
407 style="font-style:normal;font-weight:normal;font-size:11.25px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
408 x="61.081356"
409 y="605.70264"
410 id="text4636-3-5-9"
411 sodipodi:linespacing="125%"
412 transform="scale(1.024182,0.97638896)"><tspan
413 sodipodi:role="line"
414 x="61.081356"
415 y="605.70264"
416 id="tspan4660-3-3">Airplay</tspan></text>
417 <rect
418 style="fill:#c2e4ff;fill-opacity:1;fill-rule:evenodd;stroke:#607d8b;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
419 id="rect4640-7-6"
420 width="79.999985"
421 height="39.999989"
422 x="53.76844"
423 y="628.37097" />
424 <text
425 xml:space="preserve"
426 style="font-style:normal;font-weight:normal;font-size:11.25px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
427 x="62.031673"
428 y="667.15356"
429 id="text4636-3-5-0"
430 sodipodi:linespacing="125%"
431 transform="scale(1.024182,0.97638896)"><tspan
432 sodipodi:role="line"
433 x="62.031673"
434 y="667.15356"
435 id="tspan4660-3-62">Spotify</tspan></text>
436 <path
437 style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow2Mend-6)"
438 d="m 133.76844,588.37099 100,0"
439 id="path5641-8"
440 inkscape:connector-curvature="0" />
441 <text
442 xml:space="preserve"
443 style="font-style:normal;font-weight:normal;font-size:12.5px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
444 x="149.5442"
445 y="582.92725"
446 id="text4636-3-5-5-7"
447 sodipodi:linespacing="125%"><tspan
448 sodipodi:role="line"
449 x="149.5442"
450 y="582.92725"
451 id="tspan4660-3-6-92"
452 style="font-size:11.25px">Named Pipe</tspan></text>
453 <path
454 style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow2Mend-0)"
455 d="m 133.76844,648.37099 100,0"
456 id="path5641-3"
457 inkscape:connector-curvature="0" />
458 <text
459 xml:space="preserve"
460 style="font-style:normal;font-weight:normal;font-size:12.5px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
461 x="149.5442"
462 y="642.92725"
463 id="text4636-3-5-5-75"
464 sodipodi:linespacing="125%"><tspan
465 sodipodi:role="line"
466 x="149.5442"
467 y="642.92725"
468 id="tspan4660-3-6-922"
469 style="font-size:11.25px">Stdout</tspan></text>
470 <rect
471 style="fill:#fafafa;fill-opacity:1;fill-rule:evenodd;stroke:#c8c8c8;stroke-width:3.05648112;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
472 id="rect9040-7-0-9"
473 width="379.35333"
474 height="589.72687"
475 x="460.70465"
476 y="411.10712" />
477 <text
478 xml:space="preserve"
479 style="font-style:normal;font-weight:normal;font-size:21.74483299px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
480 x="519.18927"
481 y="399.26215"
482 id="text4636-7-2-6-7"
483 sodipodi:linespacing="125%"
484 transform="scale(0.91929956,1.0877847)"><tspan
485 sodipodi:role="line"
486 id="tspan4638-1-7-1-7"
487 x="519.18927"
488 y="399.26215"
489 style="font-size:16.30862617px">Streaming Clients</tspan><tspan
490 sodipodi:role="line"
491 x="519.18927"
492 y="419.64795"
493 style="font-size:16.30862617px"
494 id="tspan9253" /></text>
495 <rect
496 style="fill:#eaeaea;fill-opacity:1;fill-rule:evenodd;stroke:#c8c8c8;stroke-width:2.02899432;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
497 id="rect9040"
498 width="337.97101"
499 height="248.39276"
500 x="481.0145"
501 y="452.95496" />
502 <rect
503 style="fill:#c8c8c8;fill-opacity:1;fill-rule:evenodd;stroke:#c8c8c8;stroke-width:6.74250174;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
504 id="rect4136-8"
505 width="133.25751"
506 height="173.25751"
507 x="503.37125"
508 y="505.73346" />
509 <text
510 xml:space="preserve"
511 style="font-style:normal;font-weight:normal;font-size:20px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
512 x="509.35513"
513 y="526.17291"
514 id="text4636-9"
515 sodipodi:linespacing="125%"><tspan
516 sodipodi:role="line"
517 id="tspan4638-7"
518 x="509.35513"
519 y="526.17291"
520 style="font-size:15px">Streaming Client</tspan></text>
521 <text
522 xml:space="preserve"
523 style="font-style:normal;font-weight:normal;font-size:12.5px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
524 x="-652.45984"
525 y="264.49707"
526 id="text4636-3-3"
527 sodipodi:linespacing="125%"
528 transform="matrix(0,-1,1,0,0,0)"><tspan
529 sodipodi:role="line"
530 x="-652.45984"
531 y="264.49707"
532 id="tspan4660-6">Audio capture</tspan></text>
533 <text
534 xml:space="preserve"
535 style="font-style:normal;font-weight:normal;font-size:12.5px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
536 x="-653.58899"
537 y="284.49707"
538 id="text4636-3-3-1"
539 sodipodi:linespacing="125%"
540 transform="matrix(0,-1,1,0,0,0)"><tspan
541 sodipodi:role="line"
542 x="-653.58899"
543 y="284.49707"
544 id="tspan6685">Encoding</tspan><tspan
545 sodipodi:role="line"
546 x="-653.58899"
547 y="300.12207"
548 id="tspan6740" /></text>
549 <text
550 xml:space="preserve"
551 style="font-style:normal;font-weight:normal;font-size:12.5px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
552 x="-652.32556"
553 y="304.49707"
554 id="text4636-3-3-1-9"
555 sodipodi:linespacing="125%"
556 transform="matrix(0,-1,1,0,0,0)"><tspan
557 sodipodi:role="line"
558 x="-652.32556"
559 y="304.49707"
560 id="tspan4660-6-2-3">Timestamping</tspan><tspan
561 sodipodi:role="line"
562 x="-652.32556"
563 y="320.12207"
564 id="tspan6708" /><tspan
565 sodipodi:role="line"
566 x="-652.32556"
567 y="335.74707"
568 id="tspan6685-1" /></text>
569 <text
570 xml:space="preserve"
571 style="font-style:normal;font-weight:normal;font-size:12.5px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
572 x="-653.58899"
573 y="324.49707"
574 id="text4636-3-3-1-9-9"
575 sodipodi:linespacing="125%"
576 transform="matrix(0,-1,1,0,0,0)"
577 inkscape:transform-center-x="-31.112698"
578 inkscape:transform-center-y="32.526912"><tspan
579 sodipodi:role="line"
580 x="-653.58899"
581 y="324.49707"
582 id="tspan6734">Routing</tspan><tspan
583 sodipodi:role="line"
584 x="-653.58899"
585 y="340.12207"
586 id="tspan6708-7" /><tspan
587 sodipodi:role="line"
588 x="-653.58899"
589 y="355.74707"
590 id="tspan6685-1-8" /></text>
591 <rect
592 style="fill:#ffd42a;fill-opacity:1;fill-rule:evenodd;stroke:#607d8b;stroke-width:5.12594557;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
593 id="rect4640-1"
594 width="100.19241"
595 height="120.45921"
596 x="517.24463"
597 y="546.5191" />
598 <text
599 xml:space="preserve"
600 style="font-style:normal;font-weight:normal;font-size:12.5px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
601 x="-653.58899"
602 y="544.49707"
603 id="text4636-3-3-1-0"
604 sodipodi:linespacing="125%"
605 transform="matrix(0,-1,1,0,0,0)"><tspan
606 sodipodi:role="line"
607 x="-653.58899"
608 y="544.49707"
609 id="tspan6685-6">Decoding</tspan><tspan
610 sodipodi:role="line"
611 x="-653.58899"
612 y="560.12207"
613 id="tspan6823" /><tspan
614 sodipodi:role="line"
615 x="-653.58899"
616 y="575.74707"
617 id="tspan6740-3" /></text>
618 <text
619 xml:space="preserve"
620 style="font-style:normal;font-weight:normal;font-size:12.5px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
621 x="-652.32556"
622 y="564.49707"
623 id="text4636-3-3-1-9-2"
624 sodipodi:linespacing="125%"
625 transform="matrix(0,-1,1,0,0,0)"><tspan
626 sodipodi:role="line"
627 x="-652.32556"
628 y="564.49707"
629 id="tspan4660-6-2-3-0">Time sync</tspan><tspan
630 sodipodi:role="line"
631 x="-652.32556"
632 y="580.12207"
633 id="tspan6708-6" /><tspan
634 sodipodi:role="line"
635 x="-652.32556"
636 y="595.74707"
637 id="tspan6685-1-1" /></text>
638 <text
639 xml:space="preserve"
640 style="font-style:normal;font-weight:normal;font-size:12.5px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
641 x="-653.58899"
642 y="584.49707"
643 id="text4636-3-3-1-9-9-5"
644 sodipodi:linespacing="125%"
645 transform="matrix(0,-1,1,0,0,0)"
646 inkscape:transform-center-x="-31.112698"
647 inkscape:transform-center-y="32.526912"><tspan
648 sodipodi:role="line"
649 x="-653.58899"
650 y="584.49707"
651 id="tspan6734-5">Playback</tspan><tspan
652 sodipodi:role="line"
653 x="-653.58899"
654 y="600.12207"
655 id="tspan6708-7-4" /><tspan
656 sodipodi:role="line"
657 x="-653.58899"
658 y="615.74707"
659 id="tspan6685-1-8-7" /></text>
660 <text
661 xml:space="preserve"
662 style="font-style:normal;font-weight:normal;font-size:12.5px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
663 x="531.25085"
664 y="568.19257"
665 id="text4636-3-6"
666 sodipodi:linespacing="125%"><tspan
667 sodipodi:role="line"
668 x="531.25085"
669 y="568.19257"
670 id="tspan4660-5">Snapclient</tspan></text>
671 <rect
672 style="fill:#fafafa;fill-opacity:1;fill-rule:evenodd;stroke:#c8c8c8;stroke-width:2.91317511;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
673 id="rect9040-7-0"
674 width="352.08682"
675 height="247.08682"
676 x="41.456585"
677 y="733.81879" />
678 <text
679 xml:space="preserve"
680 style="font-style:normal;font-weight:normal;font-size:21.74483299px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
681 x="55.79063"
682 y="698.11395"
683 id="text4636-7-2-6"
684 sodipodi:linespacing="125%"
685 transform="scale(0.91929956,1.0877847)"><tspan
686 sodipodi:role="line"
687 id="tspan4638-1-7-1"
688 x="55.79063"
689 y="698.11395"
690 style="font-size:16.30862617px">Control Clients</tspan></text>
691 <rect
692 style="fill:#c8c8c8;fill-opacity:1;fill-rule:evenodd;stroke:#c8c8c8;stroke-width:6.74250031;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
693 id="rect4136-8-6"
694 width="133.25743"
695 height="173.25752"
696 x="233.37128"
697 y="785.73346" />
698 <text
699 xml:space="preserve"
700 style="font-style:normal;font-weight:normal;font-size:20px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
701 x="244.68234"
702 y="809.31561"
703 id="text4636-9-9"
704 sodipodi:linespacing="125%"><tspan
705 sodipodi:role="line"
706 id="tspan4638-7-3"
707 x="244.68234"
708 y="809.31561"
709 style="font-size:15px">Control Client</tspan></text>
710 <rect
711 style="fill:#ffd42a;fill-opacity:1;fill-rule:evenodd;stroke:#607d8b;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
712 id="rect4640-1-7"
713 width="95.229828"
714 height="120.58515"
715 x="252.50888"
716 y="825.59882" />
717 <text
718 xml:space="preserve"
719 style="font-style:normal;font-weight:normal;font-size:12.5px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
720 x="264.75754"
721 y="883.74207"
722 id="text4636-3-3-1-0-4"
723 sodipodi:linespacing="125%"><tspan
724 sodipodi:role="line"
725 x="264.75754"
726 y="883.74207"
727 id="tspan6685-6-5">Volume</tspan><tspan
728 sodipodi:role="line"
729 x="264.75754"
730 y="899.36707"
731 id="tspan6823-2" /><tspan
732 sodipodi:role="line"
733 x="264.75754"
734 y="914.99207"
735 id="tspan6740-3-5" /></text>
736 <text
737 xml:space="preserve"
738 style="font-style:normal;font-weight:normal;font-size:12.5px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
739 x="264.03122"
740 y="847.31903"
741 id="text4636-3-6-6"
742 sodipodi:linespacing="125%"><tspan
743 sodipodi:role="line"
744 x="264.03122"
745 y="847.31903"
746 id="tspan4660-5-8">Snapcontrol</tspan></text>
747 <text
748 xml:space="preserve"
749 style="font-style:normal;font-weight:normal;font-size:12.5px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
750 x="264.75754"
751 y="903.41614"
752 id="text4636-3-3-1-0-4-8"
753 sodipodi:linespacing="125%"><tspan
754 sodipodi:role="line"
755 x="264.75754"
756 y="903.41614"
757 id="tspan6685-6-5-4">Stream sel.</tspan><tspan
758 sodipodi:role="line"
759 x="264.75754"
760 y="919.04114"
761 id="tspan6823-2-3" /><tspan
762 sodipodi:role="line"
763 x="264.75754"
764 y="934.66614"
765 id="tspan6740-3-5-1" /></text>
766 <text
767 xml:space="preserve"
768 style="font-style:normal;font-weight:normal;font-size:12.5px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
769 x="264.75754"
770 y="923.74207"
771 id="text4636-3-3-1-0-4-8-4"
772 sodipodi:linespacing="125%"><tspan
773 sodipodi:role="line"
774 x="264.75754"
775 y="923.74207"
776 id="tspan6685-6-5-4-9">Grouping</tspan><tspan
777 sodipodi:role="line"
778 x="264.75754"
779 y="939.36707"
780 id="tspan6823-2-3-2" /><tspan
781 sodipodi:role="line"
782 x="264.75754"
783 y="954.99207"
784 id="tspan6740-3-5-1-0" /></text>
785 <path
786 style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:2.03357601;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow2Mend-0-6)"
787 d="m 295.26392,672.36221 0,103.38579"
788 id="path5641-3-9"
789 inkscape:connector-curvature="0" />
790 <text
791 xml:space="preserve"
792 style="font-style:normal;font-weight:normal;font-size:12.5px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
793 x="691.54559"
794 y="-302.9559"
795 id="text4636-3-5-5-75-6"
796 sodipodi:linespacing="125%"
797 transform="matrix(0,1,-1,0,0,0)"><tspan
798 sodipodi:role="line"
799 x="691.54559"
800 y="-302.9559"
801 style="font-size:11.25px"
802 id="tspan7843">Websocket</tspan></text>
803 <path
804 style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:2.49713778;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow2Mend-0-9)"
805 d="m 356.7966,591.91913 155.89243,0"
806 id="path5641-3-0"
807 inkscape:connector-curvature="0" />
808 <text
809 xml:space="preserve"
810 style="font-style:normal;font-weight:normal;font-size:12.5px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
811 x="388.93747"
812 y="585.46649"
813 id="text4636-3-5-5-75-4"
814 sodipodi:linespacing="125%"><tspan
815 sodipodi:role="line"
816 x="388.93747"
817 y="585.46649"
818 id="tspan4660-3-6-922-8"
819 style="font-size:11.25px">TCP or Websocket</tspan></text>
820 <text
821 xml:space="preserve"
822 style="font-style:normal;font-weight:normal;font-size:20px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
823 x="497.5925"
824 y="473.05191"
825 id="text4636-7"
826 sodipodi:linespacing="125%"><tspan
827 sodipodi:role="line"
828 id="tspan4638-1"
829 x="497.5925"
830 y="473.05191"
831 style="font-size:15px">Group: Spotify</tspan></text>
832 <rect
833 style="fill:#eaeaea;fill-opacity:1;fill-rule:evenodd;stroke:#c8c8c8;stroke-width:2.0320344;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
834 id="rect9040-7"
835 width="337.96796"
836 height="248.39128"
837 x="481.01602"
838 y="732.9549" />
839 <text
840 xml:space="preserve"
841 style="font-style:normal;font-weight:normal;font-size:20px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
842 x="498.58371"
843 y="753.05194"
844 id="text4636-7-2"
845 sodipodi:linespacing="125%"><tspan
846 sodipodi:role="line"
847 id="tspan4638-1-7"
848 x="498.58371"
849 y="753.05194"
850 style="font-size:15px">Group: MPD</tspan></text>
851 <rect
852 style="fill:#c8c8c8;fill-opacity:1;fill-rule:evenodd;stroke:#c8c8c8;stroke-width:6.74250174;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
853 id="rect4136-8-2"
854 width="133.25752"
855 height="173.25751"
856 x="663.37122"
857 y="505.73346" />
858 <text
859 xml:space="preserve"
860 style="font-style:normal;font-weight:normal;font-size:20px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
861 x="669.35522"
862 y="525.63593"
863 id="text4636-9-6"
864 sodipodi:linespacing="125%"><tspan
865 sodipodi:role="line"
866 id="tspan4638-7-1"
867 x="669.35522"
868 y="525.63593"
869 style="font-size:15px">Snapdroid</tspan></text>
870 <path
871 style="fill:#c8c8c8;fill-opacity:1;fill-rule:evenodd;stroke:#c8c8c8;stroke-width:6.74250174;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
872 d="M 663.37109 442.0332 L 663.37109 615.29102 L 796.62891 615.29102 L 796.62891 442.0332 L 663.37109 442.0332 z "
873 id="rect4136-8-2-6-5"
874 transform="translate(0,343.70079)" />
875 <text
876 xml:space="preserve"
877 style="font-style:normal;font-weight:normal;font-size:20px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
878 x="669.35522"
879 y="805.63593"
880 id="text4636-9-6-7-6"
881 sodipodi:linespacing="125%"><tspan
882 sodipodi:role="line"
883 id="tspan4638-7-1-3-3"
884 x="669.35522"
885 y="805.63593"
886 style="font-size:15px">Snapclient</tspan></text>
887 <rect
888 style="fill:#c8c8c8;fill-opacity:1;fill-rule:evenodd;stroke:#c8c8c8;stroke-width:6.78228188;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
889 id="rect4136-8-2-6-9"
890 width="134.86554"
891 height="173.21773"
892 x="63.39114"
893 y="786.19641" />
894 <text
895 xml:space="preserve"
896 style="font-style:normal;font-weight:normal;font-size:20px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
897 x="71.003052"
898 y="806.07898"
899 id="text4636-9-6-7-4"
900 sodipodi:linespacing="125%"><tspan
901 sodipodi:role="line"
902 id="tspan4638-7-1-3-8"
903 x="71.003052"
904 y="806.07898"
905 style="font-size:15px">Snapdroid /</tspan><tspan
906 sodipodi:role="line"
907 x="71.003052"
908 y="824.82898"
909 style="font-size:15px"
910 id="tspan9672">Snapweb</tspan></text>
911 <image
912 y="832.36218"
913 x="77.333366"
914 id="image9137-6-1"
915 xlink:href="data:image/png;base64,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 "
916 preserveAspectRatio="none"
917 height="119.99996"
918 width="106.66663" />
919 <rect
920 style="fill:#c8c8c8;fill-opacity:1;fill-rule:evenodd;stroke:#c8c8c8;stroke-width:6.78228188;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
921 id="rect4136-8-2-6-9-2"
922 width="134.86554"
923 height="173.21773"
924 x="501.74332"
925 y="785.75336" />
926 <text
927 xml:space="preserve"
928 style="font-style:normal;font-weight:normal;font-size:20px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
929 x="509.35522"
930 y="805.63593"
931 id="text4636-9-6-7-4-9"
932 sodipodi:linespacing="125%"><tspan
933 sodipodi:role="line"
934 id="tspan4638-7-1-3-8-3"
935 x="509.35522"
936 y="805.63593"
937 style="font-size:15px">Snapdroid /</tspan><tspan
938 sodipodi:role="line"
939 x="509.35522"
940 y="824.38593"
941 style="font-size:15px"
942 id="tspan9672-9">Snapweb</tspan></text>
943 <image
944 y="831.91913"
945 x="515.68555"
946 id="image9137-6-1-0"
947 xlink:href="data:image/png;base64,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 "
948 preserveAspectRatio="none"
949 height="119.99996"
950 width="106.66663" />
951 <image
952 y="898.36926"
953 x="664.93994"
954 id="image4293"
955 xlink:href="data:image/png;base64,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
1172 "
1173 preserveAspectRatio="none"
1174 height="58.992924"
1175 width="55.060062" />
1176 <image
1177 y="857.36218"
1178 x="708.86682"
1179 id="image4304"
1180 xlink:href="data:image/png;base64,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
1413 gg==
1414 "
1415 preserveAspectRatio="none"
1416 height="45.671844"
1417 width="46.133179" />
1418 <image
1419 y="807.36218"
1420 x="745"
1421 id="image4315"
1422 xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFgAAABoCAYAAAB17zeZAAAABHNCSVQICAgIfAhkiAAACCxJREFU
1423 eJztnXtsFMcdxz+757MN4VEg2LwxeI0QebYlAoFUDyJtozRUrVBQ06otTRtgcRoihbbESZuEFpKI
1424 pmqc0nUSpSKq8kdM2pQ0VV6QLrH6SNymD0ggZVQCqRJRwAnY2Ph12z/uzm/f3fpm9s7X/Ugn7d7+
1425 5veb++7c7Ozub2chJGQsYwQVyHErd4DxRVvIJUHFHI66l69YFi1ubwTKbSE/zGVdlOO4lue41uYc
1426 1+FPjmu9HlQ8M6hACb4L/MRxK8sCjguA41rXAsttIZcFFTNQgW0hdwGdYO4IMi7AzoaKCcBLwN1B
1427 xg26BWMQXQnetxzXCrQvnlJWvIH4730oyLiBC7xJHDkE/BN4LKiYjmtNhdhDrR/NXG4LeTGouBDg
1428 KKI/jmvNAD7oaJt8/e3X//WFEbZfDsZK8OYBi4HZwERgAnABOA+8D8juTu/v0RKjCWLvbKr+93+H
1429 8bcfKN9YLa80DTyNP20IOREYwHGrvgPeAxGmlGwQTd2OuzACkfXg3Q1UZOH6hOex7ZWnW555tv5U
1430 9531N8+ct/i194FrbCH/oqDqvsihwJVRME4De4BLgRuAyQpDnAOeA67r7iqt+/anD/9Ioe+MCbwP
1431 7sOYBhwDtgBfQa24JPx9FZheFL049Wf7l8xQ7D8jctKCdx+w7jAj/DjgsB3ALbaQvwwyaICnypYB
1432 TAJeBJYHFXcYnreFXBNUsMAE/vmrVrlh8i5QGlTMFFwEFtlCvqc7UCB9cM2Dc8oNk+Pkh7gQr8dx
1433 x7Wu0B1Iewuue/nKZdHitj/rjjNKWoGltpDv6AqgVWDHtUqJnxTkcLSSlg4wZtniWLMO59p+uONW
1434 jgNOpIjxu/PNc5fGeoq+rqsOQF1L85xrgO0pbErAe1tXBbS1YMetehK8r42weZ8t5BeSKzsaFiyY
1435 WhaRqN3hW2wh6/rqY90INKSwf9gW8naF8QFNLdhxrS+lENcDavt/cde648cBlX/RduDpQd+9CnSn
1436 KLOlZuc9UxTWAdDXRexKsc2AyIAL7o5rmcAlCuNHgaLE2DtJKWn+sZeveOqo41Yp1US5wI5r3QrM
1437 SW3V81L9QWshwPanKoqBrcA4hdUoAp4B82MAD7949WSgHoikKVcG3ucV1kNtH+y41iVAiw+/J4Hp
1438 qBV3uBizSS9ukk5byBJVwRW3YGMN/nbaPPSKm4yRqbgAxfFjiBoUC+zdodZfzrhNlSNlAjuuNQtY
1439 qspfjrlKlSOl406FvnLNeMetulGFIyUC17tLTOAzKnzlD17+COzRGSF+26eQWPuk+82sR1lKBC4u
1440 bY0AOcnW0YjZxsHyrJ2oqMl/jq2YCRSr8JVnzM7WQZGKWpTPfavcA5fU5/pjkR7tERzXKotnxoQM
1441 xnErZ9YfrEzZjQzbia/d+JvotTdtrQXu6WfT4cUi206dvOqR+9Y3DNiziYs1fs6WxgoxW8ghrdhx
1442 re3ANuIXlQDagLtsIX862HaIwI5rfRzYD4zUat8Fqm0hT/YrsxnYDcGmJQXALlvI7yVXah/7xqVz
1443 FzW+xYgHdOOELf61AIxeHQb0wd//xU3joMklngM2EhXAawxMb0ru5ZxlCmmiPbnwaOP8olhP4+nU
1444 5t58x63q7umxim5dLT0YNIqYtbDpfuK5C+mEmu+41g9HVeWxRa8O3Z3jH8mwjBmJGNt6VwZt9HO6
1445 W/Pgr1YpGYWMBcxI13WZW3vrestlEbNo0rT3JmVRfqxR4cP26uRCNgIbFF6fmwo/B/BYciGf8xUK
1446 glBgzYQCayYUWDOhwJoJBdZMKLBmQoE1EwqsB3PIQogeQoE1EwqsmfBij2ayEdhE0V3pMcKoGlPY
1447 RWTOhdEUCgXWTCiwZkKBNRMKrJlQYM2EAmsmFFgzocCaCQXWTCiwZrIRuAfoUlWRMUDbaAoNFrjJ
1448 R9nmEi7TMktInvKmD9vG5MJggddm6sEg+rmbxb5CS7hORebpukbfnHADBE5Mc5XJ9K/3bxJHDmcc
1449 sACwhfwDcF96S6PerpbPJdeG9MG2kFuBTcRnyhtMB/ADW8jaYbYVPLaQ9wIbEquD/72xWE9RrS2O
1450 2f2/HPaCuS3ko45r7QVzJcSWACYYTbGeyD9qVh9Nk0Zf2NhCPu641rOABawAprS3TmscP/H0mzWr
1451 j54ZbD/iHQlbyGbgt4lPSD9sIc8AZ4DEfHByRNtwHKyZUODUZP14sCKBjVY1fvKLWIyPsvWhqgXv
1452 U+QnrzAM/pitD0UCexeIv1mgkPhw8yrZmN4sNUoEtoX0gE+p8JUn9JiMW6zCkbKDnC3kOeLPkp1T
1453 5TNHtBpEF20Uh4a8LmI0aEl9clxrHRg3gDc9EeMC8akTOwE/03vHgBfoG6+3Jfy0EH+eugVYxcgP
1454 rg/HIeKT83v9fEwEzgIHbCGf8OErLYHmlu1xv2y284afSS46gQpbyA9GMnBc6zBwmQ+fa20hf+3D
1455 PisCHQe38/poXuWgdh4Kwwu0UQUqcJSKliDjDUfXxQmBTt4UqMAeHfmQ7hroNezwVFkzocCaCQXW
1456 TCiwZkKBNVMIAp/NdQVSUQgCx9Kb5I5CEDiv+X8UuHBPNG4RjTpmZ/V1rcKMBJtOF3ALNjzglI8C
1457 naRPunvDTw0iRZ0n01upIwddhLkhvU0vbyfyM1Jxp5/ov9+78xU/9tkSuMAe7c8DRzIw7QI+m87I
1458 FrKLzF+nvqZh97pAJ5HO4XuVrbOkvhPxCVvIv/nwtwdI9W66e20hM0jeU0vORhGeF5sB5m0MzLs9
1459 CzwAlPkRF8AWcj1QDRygb6RwHtjb3VX6yVyIGxISEhKSW/4HM4sHXFVwGCwAAAAASUVORK5CYII=
1460 "
1461 preserveAspectRatio="none"
1462 height="58.431087"
1463 width="49.441689" />
1464 <image
1465 y="902.7403"
1466 x="740"
1467 id="image4337"
1468 xlink:href="data:image/png;base64,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
1497 RU5ErkJggg==
1498 "
1499 preserveAspectRatio="none"
1500 height="55.621899"
1501 width="55.621899" />
1502 <image
1503 y="809.04144"
1504 x="665"
1505 id="image4349"
1506 xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQkAAAE6CAMAAAAyWFdVAAACRlBMVEX///+DUgTSqFHmsz9bOgVT
1507 NgbetFyCeGTovV7frELIokz24rReTjSPWgTiumRePix0TiLWrliiemSKfmQ5JiBHLx/x0xHrvAvX
1508 rAnAhQfPlAfrxAyteArhtArx1Cq2fgrRmwnivAzHigh0UweXYQWkbQXstAvrzSjlrAnBiwqlcgrz
1509 xAuzeQfXlg31vQy0kwaadwvfpQnZsw2bZgX1zTG5gwntzA7ytgz01UPQpA2IXwfJjgeRZgqUbAnz
1510 zQydagf21jDaowny2izOjgj21Q3YnAj22hR4TgR8WwqwpY+iiVz22kqWcgtqSwYpHAR0SQS4rZaq
1511 fgrJowt9UwWkdwq9nQS/taS0llSQYAfrsxbDmg6rcgWHWAVJMAa1jDW4o3i6gzC3m1xSRiypeCpl
1512 Qwe0gyBgPgV+YgyjjmHCkiesjlheSgSefkSrjkeXci72xiyKbCSqlGyjcT/ArJCdd0yceTPGtoTq
1513 xiBVOwa1fi+2iiA4JgWunGhsRQVoVDDPlBimm4e8lEecbS3BnE6YfjReVkSkhDythDqSglzy0oyi
1514 biSVazGCcjwMDAwUFBMcHBssLCw0NDQKBgQkJCREREQ8PDxQUFBsbGvMzMzk5OR0dHTd3dysrKzs
1515 7OyDg4S1tLNcXFycnJxkZGSUlJS+vbz09PRKSkx8fHyNjYzExMSkpKQCAgRWVlTU1NT+/vy2jAgC
1516 Ai/CkwushAlALAcqIgRcQwlBMgcfFgQxIwWefgSsjAT07t4UDQRKOgyKZgSOagc5KgxMNgR/PwZm
1517 AAAAAXRSTlMAQObYZgAAK/FJREFUeF7s2Nd22zgQBuB9/3NmUAt775Rsp2drebMFSIWSnDgb796B
1518 /H0j21f/dzgjED95H/9z5MiRI0eOHJnnee/9v87BcJfD4ZqD4SaHwzUHxDWHwzOLA4I7iwNiozgc
1519 rtkzBOf8lmK3EICEIICfFK+EEJQKgsA3iqc9QnCwEEwpRsWNxdMuZ0NQJXXXSUbJDcX+IJyE1EVV
1520 nKSiBDeK3Um44WDyVBlTFR2zFOAPxfy6AFLWFabvTaUVFTcDsj8JoXQV9X1ULfPhz4DMrwyQTaKQ
1521 igmCsFsJ6iQiYwptJag3FPN/kJBWwlQLxL3EvCcJjoSq5UvUQdBnh829QGwSUuvVgZAVYsuOJGA5
1522 Tyjp5kKgC8C8TwkkgjJqGQiiNeBwL7UPiI3C5vos7FWCA6ALfwHqaR8SGwV/mWo3EDMAn+F7VPu6
1523 xj0kDon/T3FIHBIHhMtuJLj/EhyA8+8juB8ERATwVYKDi6vI538LoqBMCfBRAlw4ECEo+QGKGQjT
1524 mqF/Em4sLgWlpAg/sBqQ6UqibxIA10+EWgr+ogNHQlYoELpg3BOKb7YAIRnhL/yrK0xkzEk4AU6k
1525 JD5KcCIILhRUfmsDcKEsQ5/VNllB3QQRydA7CaSnKMuMJq4gk7ffC+tHoKcq6oehLstySpJagjNT
1526 AjyTIDoayiQpM8NcQ/FVQVTWIRvqhSFvmmbqYNka6JcEKfq6TJpxbOqoc90Q79cldhZiqGvrkCf5
1527 OI5BU3bInQX4JAFdP6wQTT70jgLgTgJlFQ2DZVifh+AcjM2UsdXMJwkRZfXUjEEwjnk59BL5fH+8
1528 opUbntwqjDaBlWjyaTDEKnHwSAJ0P0yJlRjd/NdD/3xL4MlkZT6e4zgM0zRMw/A8WoleLwoeSZAq
1529 KydXNAzDOB7z8sTvJVQ0JGOctu3DJe1DGudlX4nZB4pNgquozm3RLyXbOCF3ELwo83hT2CzGuu+4
1530 XxKnoYnbxeBSM9R3EiRrNogwr6ezU7Ey5ykiXkmASUKrkAZjkK4WbXm/L6cgXB3SmiEHUZ0vLCXz
1531 SoJkgQU4FwKJmtYZCcTtYUJOF6I0QgtHBFH5KnYuwCcJ4eqHajlaYr9QpN3tS2qXBAtEm7neLIo6
1532 QZP1LwP6JKHGtm37ZXNWStTLEujXG4v12kIn62ZoXG1Oo3qoBDsvFDnxQ+JpkdB2OEJqSdiQVZQE
1533 rmFtEeBygOSFk9geFFDRMGioFokzBY8kunP6kMP69tGr2diK7YQcEQldLm2KfJFovpRmfWY4jZfx
1534 0OjRdOhz2pq1YhSdgIWLBBJCWKe1JPy0TEdrru9rJkJMFokIPJIobM9uragLQ0hjJRK0IYIqpSRd
1535 90SqtpttKgsK2bI7ap8kTue2XVsC1YbOg21dgg0HABQCxBC4bxexOgACAGVgFokSPHrx0EHaSrjc
1536 THY4GysR8et9PoBK4tZJXIM4V6nbopNnEg8VgpNAptGtzJHC7eUdGex4xOK+bNV6JyHHtO352gAp
1537 2Iqpwd8+fX7398d3f31+//4TF8WYPqTPJKJFokSPJFgTtjlcj9fm1+n3D3+8fXx88/jLm8e3P//5
1538 4SMrY7dL7lI+2ISDT9NBkrCNybyl+oe7O2tq48riAD6fotWLulsS2je0IKF9YccGbMxibMCOk3iJ
1539 k9hOvCRxZt+3lkAyiwBZmChoBKXRoJRjh0lJk28259wWamoe/Dzdh1bxrF/9z7m3b9HN5QezLM3z
1540 vJ2neSs/yNP9U798hdvQ8yWRHXippiUJ6Ru4FVVG5Ju+1onBrvMOGoEBSgcfq//056XtH89LHMi3
1541 YJuaOrTagmV0d11OReG7JOMInfgHdTorQkSbs7O+k4BOx19/9odzEEWyKS/XizktSWzU4aZ7j3yl
1542 mXF9SEzqDTxIGHVGHjrD1+nrXOd1YNH8+E0PQr4t3/1G0lQmikcl6Hn8UqsJfYhpIQRIEIoATV9v
1543 nUaNHFBY+ZO+GQKxVpHPKxqHGjvI3GmU8awhv5p2hxxslrZaBwetRIIUzRu51EOOAxvakPzybXFL
1544 PrMql/ZympLAHQWG4t8XnABh8NsBAhxAgsP6grPZBgRqIBV2cS6XjqdPfPdgLwZ0ZZiXGpPIvQKK
1545 8k29iImwowP+QCK4cDhss9kEIZIRBCFDUSmXFywMH926XC/tNl5J2pLAUFQa5asIcTJK81arbpBA
1546 cAhBCcKQMDR0MRPJZMZGRlIpyIWdHmWf//H3f/8eJbR0aIVbiu0LenfIxBogElYMBBZnxEhQwhBA
1547 QGVGiMRDl8vL6Xh7lm0mf5K0lgnp/c68GGLi7KjfjmOCOJBMUAAhZDIXL0YyQwCBlaJSqZGUS3fN
1548 n50FCm1JzKw49SJAQCTsvBWL9AaRIK2BkRgbQwoIxhhcI6mBAd21rP47SVMSb1ecbuiNYYwEQiiR
1549 sIFEJEJ6AyWwiAdqRAYeXjNMvNGSxJuYU+8WGRM7msUlVEe6g0MIORIocYYADF0LaiAy4KLnv9aQ
1550 RGHVghAMy2ZxSkD1IoHzMvMXEgmlNRCC4gKBwBcDXv/8fwraoZiJzbvdoomRxyXcceC+ChjkKSHA
1551 sOxFAjXwkzL2+1rNa5zX0HmrnZEprVqWMRLDHpQg8xJ7Q95VIQRZP0kYkAEhuMCpb2qehfHqmMhp
1552 SOL1st7tximBEjgj4OKgSCRsEaghICAMxCHMnf5w2nJkDVl/1DH9o4YkxnvzkkwJMi8xESAhoMPF
1553 MWXtRIgrvh9w5wGVzTZfg4QmKFCibxkkTKwHJYABb0M5HZmXApkSyIAOpCjjaacVZ1kDMACF47Wk
1554 HYnCtFMfCsW7zQGtgSU3B7ZGphuGbiCiviQzzCLbKFb8Y8iEJigwxt9PJPQtFrqejInBQa/ORfaX
1555 FGUbighDaIBFUVTYeNrWO0hjkEh4RpmfjjUkIb1d/TLWNAAEOsiR6I4JQeiuF1QqTIW56FQS2oiV
1556 KQDCw07NnHuRT07lEriO3vmwrxXFRICF10umBIU1RsmtQYELF/XFLNAZnl4ifjXMKHdgxYPafrGg
1557 XoruDdidS5d+0THQuGx44eLgeIaCa4wChTOH5PSS6AAIlMAaZkymc7uJ3H6ttrWeV7mEtHpp/NJ4
1558 +zoPFBAJjrMJNugMSi5gCFxJTk9YoDOURLCMyHzwfk7qSbyo4Suu8P2IapbIv+6bmBufaNKw1+Zc
1559 qZTNFgEK2cFoXJi9++Fc2s3EzyCgMUyhr27c//WrjVxPYu2gVntZ29o/zPd01Cjx4s+PVubGpxNN
1560 g717eEmFoTjjlX5ne2Il6NTjTTvUqAGKjTs+uvV4sbpY/UdROpMoHtaA4uXL2n5etRKFgrR5NPko
1561 FluZnu7rtPoXFq7QgYWF/tOWL9Y3l7bo8ThrmEUJrDjDvPe7Z9V6Fapy0AuFVDwAB8DYUf5cVXUS
1562 x7lapTqZWAomY3MrsXZyqtNut++22x0nZAGKicfRwYMWJiZ0+zePqwiBdbR23KN4gZmADoH+UKUE
1563 UhT34EtNdoJpp14/P9tc6G/29zebDig0IOXBX8AQEm/crOIzHnW0qFc2FIniFkDAtaMsp2rLhLSO
1564 Qa/eSwTTaYtFPAkYeZ6n6SjrgRAMwwUa8IkDhPv2VYzCNhaJxaYikd/Yge6AC/pDlRL4FSqE4kLC
1565 HAym55sGOJDiaVwqPVgkDVBM6Cs3BAIKM9FNxY4k9V7CnTuQI7GpbLFUJlHc2KtiLV4ImoNmy6wj
1566 mvXTfgNup4EBioESGbH1/NPLZE4SiwaROMoXlFRskoX08PwzQyobE+soUUGKtDkRtIgMjEdsDcJg
1567 Qga3WxRvP7+JXIskFY1GY7fRAIotZStVWN8/gLdFwpNy6pTATBxVuxT3E0sJ3D0wJihsCqKw7F4W
1568 ReYGBoIUBKJRKpe/Le826ntFJQC5fP7FIYFQSlWZyGMmuhafJWNL5jScbJJiQMGC5Rbfu/oYEwFe
1569 CIEO5I/vtuvyc3EKhhpfCXjck8BMYEHyJx/ElpbMQctZweQwm5dxVMpWCFGSGb6Fq9Q4yhXU/q4W
1570 JdQggc9IyhSVz/4Wg1z0ClQ+/9OtRcWhARDKo3G72/vvksgVVCQh5dYqMABligpZQx4kluQy//av
1571 n39wH/MgK/0vBKZiL/8OClhk1QKBEod1eFAUt0pntXh075OnT548ffrJp1cnF885AMTZg6NKKg6k
1572 d/6vD/VIFNbqZXwuGCWUqlQuY1W6NMDQbY1S10Gpav74HfV/H4r/Unc2uo3qQBS+T3nfagyY/OB1
1573 p9mkTLfJFJ7xdmwm2cRxyFZXWnMaFFVFVefTGY5NXMNnwdtIhM6fScT35BsxhIBISOBPAM7KGF4I
1574 CH7340hiCiWQYPhXOXhJzzFR3wJnAwQMFE3CsmafeQmJSDiZIuVxMUQAkYq2hpdqCmvARBR262hU
1575 FH1OGQ6qvgHOypZMAsBUJqJYd6j/Wu989ENqjNAYIT3vo6BfFedly03S8PimuJmZ2V9CkdyQ6QwB
1576 IefRmFG3hkWONNnqg6zsxuOoIvTH+50ROWRFNH6YJZII0RmDr375vfXpXn50U2PIV16+Bl4cCh3w
1577 ALP5oY5QU3SpISYOj0U/7OJMwRfZ9fG6QnI3KI7aGLM6Gn6soklUr3Tb8OgGuUOphvDekYCYZ0Eb
1578 uzAU/JveByGRsPBDJxoGr7OtZ7SDZZG43nblbpnCwnvnNDefFNY8o2JB8E+XMToGDMrhWW15TqWC
1579 qPr8+CBw+EPtAXhGhZI4YJ5EymFenQGeU5Eg2v5Btd8AQdjwvAoEAVuXa4DMBWJWW2uXQYJvLOHw
1580 OwXnXUQnhkWYgq/1OWCWBH2PRAeW51UYCdvuB/cgHugbIMjVDDyvskCY1W5w+L+hoCA88FMqqTeg
1581 PfQznqA/4TCNQPZgi0eR7Kv/uuseTSqI6HkOiISEX+qBS0fBCYkP2Rw0l5YU9RhAOEJToHPohIRv
1582 wZTdIHwraPanvnN3UNDkdYdzLGg8c/DTjM01bGzRKFISm7BzLoaqbzgQiqQwdYa+aEKlnpnO817W
1583 1wgL/AQomUQKwsJ2Lxtsh/ig6xLRTQqlBRTTa7yUj3qKUBim2xne4UdlLJeLghOBed2fToLCo0hw
1584 KAj1up9QTISiJfTn5/LjIrygbvA4rA3wkyqBhIEqbLAfV0M4h6iVRhgTi8hIxxYaloJBCBxVfVyj
1585 Gn4X9Q0/rQIsYUz9tp9QDNLgcdytB7kAIk0PAYXCaYg+kPrDEZFEFDUsiERVrb847KMnQtFXGULo
1586 8jcwQ1a4yEJ1WZLnkXatAqjqypSEghNBWzUC4kUcEQxxUzVKZzwcSsXAUGtcmHSOxrepfFhvNk0L
1587 JZOwlXn/ag35dMvdnXugyw/E9WqCE414wTyvLPBupAMois/NqgVbCApOZSpzkODo/IUDxUMbAOn+
1588 dIxSb6gzjnLRlFwe/VrN18ijpCqwpZKoTf226yX08KZIim/oziRIj8xwm0ivoEIi0KXLuivLplk1
1589 bT5Z/y4IaM32RTojO+0IniBFcN8PVxOw4IzoizCv+7Csgvp9VbemKBIWQJPj1GtnqBFmb23rmffP
1590 VxZ9vGjiis8CWcpdQUEkoKqricSvkPyUNr5ySW7pUo7LhYUmazcg6SeD5xBZ5TLkr6xIXtcmEDGb
1591 4Ij/mDubHjeKJo5zQpwibrwokZCQ0OYSKYcogjsSJ065cI/4Gpw5VL/32GPvJI6zPZv1eAE9hzwr
1592 gcJXw+OZWlwul9qCbJvOHnr6lP6p/vXvqm7JXPQIhqYGqhI+KIs+KuYbW17TyrcKwjve8iB0CKNY
1593 tV9tX5cmzgBnlAEXB65TaY0oBn1cETk4I/7cYmkSLq4NMlljs4qwyA0uFa6qNKLoq/0FzdK2NfCf
1594 IOErC0hiVaeUsYM8jCF0uEK2KKYbfUyBojAn1sdtSCw0TqshIAQWRCuJ/PHB1xFF/UuqEIJzgzyj
1595 OiEJ5K6qADhdEQIsIZIJw5Q3kf5NzsY/NvaBIQBKKeicF/RRWhwGp1VNdorz49NGEpZJVEznfVAg
1596 CK21ArCClZYkoULEoki/JK6R223KhgdT2oBiExTnbgsCtPfGawDVBuVOROJvD/M4nTVs1wwLpyCd
1597 RRk4FMim2h9/0gCUNzZaoxX4tYETkeBZQk9ZQhT0LifG/GiabQN0NpKwMUTrNagYVEF5cBLOLjFL
1598 wGxPGZxIElMCZyh/p37lAoZDvg0hROOh0609TVBgHFRrhdNpOvbkJIsj5U5YOGqzbYaYGKpgrVYd
1599 2MoLmaJ0SEwSCwGZxbFWIq9MOhjEEUKfKFynq6hOR0K1mCXAX7EEKJbccvqQqg8+VgDjjwVZo/vC
1600 HOLBoCgkDr9A41Azobime010NV90iONag1LeW2u8VjD80FYsbh9IAkLrMCSmRMxiTkxsVbTQxICS
1601 WQRQuh9qANFBtfSnIqGxCAW1vAUgqyDtN3NFbnyRf04AABTsXBv7SSwuDyyH0ThAv+TbOcCjrhMp
1602 tfPHS7k4WwFWYDhgXV4eGBIRjSPUBAOvOPHJduKhnwd4kMw1cwoXq9IkRtgWy3GnXlMAQjKo6yaz
1603 aQ6Eo5Cf9/u1PgkJCGGUpYrXRyg/HSDBATIrZYu4tmQk9MIXlgdaqBlB6Etpg2QzTT1vWF1GMwMv
1604 V+RYec1IwNqqoiT2VantFdm0JI8hTSChf/1++YKRcFWrXWkSrq9CsR5dNGIRjnGPCTPlLDRfsuPX
1605 nCuhKnvMHEmbpUdxrMg+REtoeJpgu5UzLqPScCW0lS5XmmNIQFgA5stakDL9SHVdk64145UO05HQ
1606 mhOT6EYSt+KAS6ILMT5SPa9J3ZFRU+7smdoDJFRJEuhY6Ofq4qikn5qehGwyOd1wi54dyBNQjgRy
1607 NpUbkaA4ch2nZj6vM3UHcR8xkeLkxX+CBISIwTGhuxENEmMC/yQoiWEQavhXXB2x3LUgYtaVx9n5
1608 MWKnMUF3/A9HWjESoTwJZ9foHHZK5MtgEHUwG5VLc/mwjZOXrqMDWtOVIoGQnW0dyqTJty2RxAEQ
1609 2RsRuZV3tU9CVcVI7N7UA3Zyc9caON2qgyQKsjGxsZckcNewTyLo4iSUvbWOVfYyJwnekTIOmjuu
1610 zVVHh6+gEIkdRcaFGyZmihgy+k9zjAnppCR8izdr9T4JI5Ho7oyEA7uAMSBrogvR/UcSZJGy4nZD
1611 lMRDqdnXQgyuDIldRdrJSGJRS3UjHT0J+m43bzc4hNq08cxEuyIkSG4ySGJSS8pPB0hIFVV+MD6c
1612 hFsbV5yEtrORxGUt2KAQE4wQM5ls8pU6mbD0RUj8RklcqmHyupHigWYDjAk+Mm6KM84n2f02piqi
1613 DkJf+8lI4lXDa3G6I+oduJapM+k4DCVFSsIGKEGCxqE3Mz1Y+HkSPI51MbckpCjgYMTMKpJoTVea
1614 BGgTJ2b318WPaUmjdQhbzfQkmEIYCaiUK0wClN+QCAOJi5TpOOBgBVgWXQZTSoGdq8qScD2IMFuO
1615 v9afhMqRno6QBH7zhMrY4RBT5x6JYF1hEqC8bavLWT9Xy+vsvebhojxJ8HCeH6ml4tBdCRI0X9p2
1616 PXvhtiSmbEs4JBJ5aGzGXZbHhG4BSpPoQNmwfKMGEuL7OU5ijuqQUwu3TflZWrNLAqKBojGBqcK2
1617 l34gkbuvQCqoDrnUzGiMsat3vUMH1RUmgT66iNuyY56Y6cnqSEk8QiSpV5PEU9fc74ZEhNOQAN2u
1618 t43ta1EcvGXVNClXtcoPljmfqd5tV/nSJHCAWQMhwbZP5C62MWVJZKvzK9j531itug4AnCtOYhuP
1619 ejnNPDxN9GQl3XTwr/w4RxLexmi8ckr7zdAAxcoOlGYAvZge81IKSYitalaY4SfLEjtrb9DTQ7RG
1620 g+uU9toba3oYUIAEuFttLmy4IgjEax/iHUxLsndSVARNmgwggtX4FhEAQBtvhveq7g6Lcrh5/s27
1621 d1++VTB4aViE80wTlnpHPvTT8U4ahqrY08LL9TBiDCEYuCsSf/z47OHXn25+Tvjs4fOxY9W+eJX4
1622 8zB+3Yd5Ar+4CjiQXOPi2vQgBg7c2my7nLS6c3fRvHv77MnnZ08f9eOTx3/+f0BRreojK8iGPZ9A
1623 ABkW0ml7BR1o0TvBVrPLysMddLY/vv/w27NH43h69r/vB4G053XmKECuwDLKYCTlB93NpG8sywdL
1624 FavZJGj3/knc/+yLsx8e4fjqwa83W1GqqkeRsurneSLb4knClcjwr46d874TB5hQrYNV753E/Sc/
1625 PX7wdCsOjIqbLXq/HgQiXHgm6qK4kAkOSRzp958/6se9j+59+N0NhoQgDxujNe+fxF+sne9v28YZ
1626 x4dlbdduGLANM0VSgiQaJk6SJZQJqIPNVIHgcH4zuzBiDGZiAQUauxiGYcCAAcmCTij2ai+sJIrr
1627 2nGUJrG1xtm8wfE8G6uALP/ZnueOdE2fTiQlP3LjyEgh8ePv8/OOp4JTJ4SRCIy8/h9D0Vrf2ZYI
1628 QVwWFcuJRLsofvS+ZZmmhw/PKnz2iwEgWg8AxKPW/YsmsWI064R6tCKigPtWX24LqhdsS9hmlWSH
1629 AH/+7SXLrJRqaCWQ5c3/yEm04P5JKCrarbX2hYLIqjNlC8IEDaEw7UN/XPF0uw+AsK9shZY7ov55
1630 /0Xmv8JbqMEH34MBjEruwUDnYIX3v3/14YWSsNWmaxHTq4QtBbGC9+iP98V5o1hZhVKoNGzK55iX
1631 LAIkrv8S7frY7/87P0ATwOE+CGLuppe/UEkYZdcC5wxrolRxiy0fxVeb4n0bYj0hCCDBrAZJuIRW
1632 SiWAATamf3Qs/0goLLeh6DpetJSx2sWxUA10DoIgwmZOven58+5nux1JL9oVpvxCUxJ5Syl/9n7Z
1633 AhR+pKhmPrs/QBIt7MLWfntTQXfKX5wk0DmICSTOicLMjbd9NW686ogFoTjHFFmI971JRoDdS2UQ
1634 JooRWehTc1JNYPfBpLGYqtXGQD8XA+IT1dBc1xIkASBKSqrgB/BW68uX2wMWwBmJjsQNYg4yL2Gw
1635 qjAUup45OGKakNtab3xKBxJj18dSFwHCAN9gkvC4IMIwlKkFHrfakMEfSz6YIDgHVHZkiTyvit7B
1636 ozaQuFy404og8fZAr+qlaq12Ee7hZLlvYOYAECKKTO7q2um5GBtfd7rS6+h0OuGmVXSDiJ/+JNCE
1637 ouQzrn10zDxTHjY/rOt6CR7gSitDhYWVLLOFu1n8/GyjqZVdQggVNMF1mjqYO22G19afdKST3S6Y
1638 QCCBn3S7PwQSlHIUmZw9Oz+YxI2cAhyUUgX+eJ2UwmTWtlW1APaJbWftFbvgNDW3jr7hS0JkUS9A
1639 rRl4yMOXMdr0MKy4W2lQEy4xGQfFS7n29HxvEIl37JSi60pFQSfOJuWwAtExV3ab5bLmOMADOWC0
1640 JMhBQiKfK659NyN58NVe/334ovYTlBP8nO53LZdrgmaI5RaKh70BojhJa2YemFFFAXh3E4IoOHjh
1641 dTD4lgO30PAJARCmnEQ1w/0j0AX06XG3BUTDCKTDDjl7FwN3hcLFESChpj8fECiOlzQrAyRMhaFL
1642 RGLBViFJgCugAQt44N+RAyZQKYmaXrcPz4bsddany24XFsryiIqCR1t2wNlPXRecFMwkluUa2WuN
1643 43bfBY52+/iq6pJMPq+YJjWVCv00WQEFHFx+7QQIwINjYFlDbtVqPrfoFzlMrWea04itI3FbUwQB
1644 9i7GCVC7CSRcTZ1cXL7XWxM8BN5E+94VVXNTGRNMMSuUknQi32AFlOURDy8ecXAOlEoVQX1RpArj
1645 4VP5vtmM0n43dsrAqmyP2fddC/2UZlATmpFNT0yftPsIYn51wga/JiSTMSlaxcsmIWE3ywCCeNRE
1646 85AFL7AHKULBQKHnob4KryA/e9IRnD1qPCc9R3LLP073fQzeJhiGCdexs+lbS43zKNrzt68s2gaS
1647 AA4KXgGIQo0P4g9Zu4nIPYoYGUovpAa5KPSantGm2+f2FTwXOjIJC+GpQGx/76wmiE9iRrUn07cm
1648 bhz20HBFtN3uzd9ZLi5C6Oep3zQxiVJ4uEnCRKFZZ8Qps4pCFeQQhQJVAaJIhYImptNHsKNA0obI
1649 55R9SX3LnQMjZtmyTEqABKm7mqPaWWBxbfrG7TuNxttG42h2aSKdTk9mVadsIQiqIAXUdCoJCaPM
1650 kDM5SQhIVKEDitxHvfOTxI094b7goc6863yniTJ6rBmQMKD6AxbpdHECrDixuJgGy9rqQc5FQVAk
1651 waVLm7FB3LWdZp29jE9R0AOVRopKrZpPFY7Ok7i//nXkjaDdqGWPEIky/rIII2ExFKoKMCYnkQaK
1652 Aey1rRa0MgfBOfA/4osiveLkWFlPAwA0tiYUCJqYSYUZwcNXYVV0E5WXwbdTEh9AmidByIQ8iijA
1653 bDDokGzsFGzVcbQcc3N+JT4M6uViOwd4B5/GSJQgI0NRFKUxyKRzfeaqOx2pEkQVSJItHouJtvtB
1654 k41TfVG4iMIxjAIqA3mohmE4mpZDEDQc4+ApuRlXE9kCOpeHKJMZBU2UsLzKnvQZtp+qQk4k6hDy
1655 rVNNGE3228KHRVy3rGnajGHAF5pjOA6A0FA3QnPgVTwrbqhYYLmDeKip5FaqjeUPptdEFOswvUlm
1656 4sHKgSb+4jg8AiALjBRumcHAh6MxDOgZ6BphDnhRZmYqbsgsaBiOqJmUBEOnlGqKVTgUC777/9gP
1657 xUB5BpXuLdvaBhJof/mTwd6jX1IgCmSBNLjlsH8kligJyvwjF1MT0HbUURNeZRirVasZCJoiikcv
1658 OxEHpkZvTPI18UWhMMO0fwYFGqBwITwwDhZLLmKC8yj1Ugcx0ygrtglKaQgrQVExpY2LbYC/Fyt2
1659 3yH+uBMEii+gW3aZKPz8gRkEaQRmoX9j2hA1wVEUYiUPu8nL+mFAKKwRy7v/utdHFDvdGMvgAwrx
1660 LiQProkVmxWPZoCCWNzqzPCJCIIbsqGm5/4xBgnbdvzpFB2OBfakuWJLTKVP466GSzbpdgP3+OK1
1661 7WhcExS/SMCiDg82RyAyEqgJFipWYmkC3YMRTWoMHWZS66BPUfGi3zL44L5UOHOcaeJ3tm3wKpNS
1662 hoISbnyWwjmAKRJZ4/9gHcQg4fhd+TB5lDJRQNC058UDtzqJljVEXN+yQLH74lPQRA5/WyRomMFF
1663 QoYcZO+fQthUqJmKziDQeAhLXYlMr1b1lPNGILEhSZDi4oekzuQkdh69SUOT6XsHNxPsDAeKKOTe
1664 7SGNGMWmbTASnGliozx/5N3CW1ETUb4RsYkEj6Hf3f2ydTVt+wUFr6UZC6QRGGWKkIuaTxu8+m+i
1665 2vKZcp0vbAxpSglnNgvz5yNmdJiU/pRrAuPEs9bJdNr3YNP0L5fDCAyeRM3YEBPKIpIE18TQKHQd
1666 /KP8phceau7HzKDyfavbm79+0eo1rk6qWpA7Kvyh0DPGxlPKoC6aBv+RQTlkRW36K8EJvQMp+5kU
1667 VvPzdSc0qYCba/tvxJYfrizGid0XrXbrcCntZ3ruBMI7SDBdUry6fPAPBVx5+IhJGQq9iv6xeMY/
1668 hGZUuNLo+xy2dp/BFtNWY/qUBFWYAIY2ZEHqP5NGzBkNKrjkcSJErjqmT6nL7aDUfrwnxMHke9V3
1669 13FjSK8BmtDY9J5LYjSjimfJdoywfRKWN8KrKIqi167ruWwDJ1at9ZebEZcZZiNJMHv8zOTW26Wi
1670 reWC3DE6Co/U5bupsNMbOmDSoKi4rC0ern+zs7sVsYcoVEjIAmn3iX+6wvzq0qLtaHUrGFGOTMKT
1671 kLADEpVhRYGFpoIo6oXie52AwWjWef4oWNE5Qk0giSB3jMQBNWFJNaGd8Q46tH9UdSi6p19FH143
1672 MGvwQLv/zennKZ3g2pbD++WRMUR7BwBHXMF4PCERylHo+ZS28OdtwTVkR2PKC9C97w4QaN+7Dd5x
1673 4K9tjZg4gCSAIK6ksEJNuHyQGUZOk+QQJa/r+mXXmP65/JZzTiN6p9HXZ84P6B3OXSmyDoyYo0ZM
1674 lBSAsCQkQBMzrsX2U4neQROMNHUUhWuP7wKDBL4hsthhnhGQaMwuAQkAgbljNAwgCM+zLMm0exJI
1675 nFZWI7ySwkSRn2oWwT9k1yz6hKiT7WehUflxY26pmGW5g7IJBB0BBfEIgCjLNpKoxoh9B3dA8A8u
1676 ihvviZIXqUhodV+d26k//3Z2egJI+MX2KEHCQw6u25SAAO9gJLgLIvORRJGZOlg8eiXESEmOEGwT
1677 DlsM2/E7s8sTkwVWYo6iCeqDKJc/kUtiJnCOUYgjCj2vZ6wDe+lob7jycv+xuBF5/s44aKKAgwOC
1678 iX74UOkxDjNZ2ZYaIKG5xByBAw1eK5PX85m6Zhfnfrwv7cLl5eU+nG0h2skqaCJr5OrEpEPmDhYA
1679 AxB3pSBQEvgqdNTyjSpmRlEyKc1OLx1ubEuEILPu1k7f4+vaJ6vj09eyBVZjDhnKKJhHkENTTcv3
1680 mzFJ0JEkEXyZmUz+spVzFq5+fry+1xXn2PLysvNEcqRG797Rx6gJvxVNPkOhaH6oBEHIV8pVVkxg
1681 +hxVEkiCZFJAYvKj5UbrwcuOkCIl5WV3+7n0aJHW4ersdBE04RIyzAzlLAcQhNROE4fCWIwWK6hJ
1682 MiRlTWn2xPLteTip4nknRjHV3Xry1YDP5z8+XP0YSBh8l4iXsAlgGAgPEMYkABhMgg+DAMKwLGgQ
1683 KAiAqLsH9q2l2R8c93Cr6mZHevM47sne3nu+IcfAC6vV2SXcXRg/d9DgW4DBdYHD4H3LtwDETNk6
1684 XfehiZpRKoiCEivl5jR78cry7Xs9trF94+mTve1u1z/HiX/rshvlNl89ffawtdb+P2vn09q6EUVx
1685 GeyloLt2TMqUQTJ+ZVwIQgSihTAieNWnhTcSI+2MP0OgmO4DbdM/iy5CS18LfW3pyot0kQ/Xe+bO
1686 yCmmyE50Ykx2ET+fe8/IMzf6Xwh8T74nEiZ9w/cd9dUJNnDLIl5JAQPVhb7rGWbAPYffZ/OrqvPM
1687 cfCRP1d9MxUXSVO22Cl1/zzk3e+/Pvz2x994Jv3PD3/+8v79T+9+oNk1UOjR0z/tZp1cgITLjj4W
1688 /iPluAAGJREYvSQU9l7tBnHNmwYOSS+KY1gfYpPuoyV5QudhOfbzKLAGnpd7f8+PuMMPBtd6xdFx
1689 a0moqd/P728M4MVxQRyUkp8GUD8JNfWmcBSA42X9E7vYS1SHjqvVFuXxaj3uJ583iU5x+HbejwIg
1690 arzQIMBB3EXBKUql4G8xecd5VtewRt0hOa9TwxMYi8iYRPs4CIlx2eRaCkuiBwQwYATB9ckFrfGa
1691 048gKqyswGLuVBMQiHF0NjkpuufWE1lqknC1mbwdgkQ7LuEJbILZ8Ojxw5zPmaBbZUI3Z0w0IEZx
1692 eI0nPIDDaTYHDY8BTHphoE3gEpQ2ebjaTdqnwUhIkdkFBQ/7H0NgDpaCPYa1KDIh8w+Ck4VD36kQ
1693 RGM6JV8sby4tj2f+qLlYgKGncRMsRAeFqNAxeaIct49DkTDSjisgPY5RXPmocG5AWWQijZvgHGlt
1694 pwJJKiuIxuKGXtAzIjN4w9kD+s/ipcsZLg4Ckan0Io6a9W47RHm8JRKjyGjexr7EggIsOvheZOAD
1695 h0IJcxYHdgUkSZiSBA8HxOPwPNCJGMbxQXeuUpCgCFWpNnkUrsvNcCRirQWnPVmU3fls7YO6YA4L
1696 UlEUSpoyOFsxyZA0lEpbKvAHgACJnxCzPGqGcVyiFgxqg3wpqTiiZrUbD0JiP9mswthIQTnKI82H
1697 YHO/keacFuCglNBV8DJVVZLnOYBoYw0iHA+l4JCpd4elUXdX4t7YIPhYkOCZsiDCEfrEMCS2ozAx
1698 /O3BEvUxYwJAwfqYOPi7LHAwo+AVqqIoIR6ehhZSsBQxAQ7rDvZH7S+lux7wIErIDaFNHBGJcjME
1699 ie9AohwlxmBx5cJ+5i/AB8bMxiZxEEJonQSvVhWBx6FYJOQK5o3KLI6lhVF3zrCqcUGuNoRhS+y2
1700 t+3jEJ6wjYJMoQXPMPoVD2iQQbq6WBRCSK1NFQyj66oic8SGcVgiDIWHSYrpM2sAgAuwWTeMow1A
1701 rEtqE/shSHwBEmESoz4yh8Id66/pzYPAlIMkO8brYDg115EtlTiPISbiaAhRkDHg0sOnAwxsiIxr
1702 I6TaKLeTdv80DIkdkUCnSO0f99mON+QFjzgoIf5t74xZ44bBMGyDNIp0yA0thkA5Q3ukQwlZ0sGE
1703 UjSlGrwcWN6EfkPA6E/0b3QpaOiSpf+s7yvZ7t7Ydzf49V247RPPPd/3mZC74JT18alYNm/K0iq0
1704 CpEwiUhV5ZsPfAg9LxV2bb7oJ09zc4PeUGWpjROy//5/32D68xeuKb+HKJ1R+yQFS8+7PXF4pA5U
1705 8Z5CHPVVsUZMa61Vaj8BaepDTTcwODA1Rjf4Z/Rkkt+V0Qhtghf9kJrjlf865CVG6XWJ+l1Cgemd
1706 Vzuej3hmDPeo3DUQYrW4J63bkoJYGsLlUlepT+AG5Eh2UM5xbHfcGmiNACUilShenSGNzCzFLapy
1707 dM/5kjDAhwouPgDEqvEuBAMg9kGpjKPOY4M88FUVOA0xwM7u7m4E4b3sh5dFSESQCFopSjHNKn70
1708 i2Fd6EAfmqM1xQkihQ8m28HdQjcOh64aVy3DcUUhEgj2RuSvrJaoHXveUQBFTSsmIckg2YDUvKdt
1709 XXGqCO9cMEaPsxQ0IMcBm6XCk4dRoxAwQkCJHwWzkBTGqn1GkWnkJB1q1IYQV8VJs8tyjJuFciAY
1710 pnit1D8hRN9fL1bzGlII1ASKaanfUkdCqQCiafZHG2Rx8vSAYUxLGFYphWZRasJAEJ4gYlywYpxR
1711 gEWTaCQgfIW3ARx0eC7Okl46Z7hU2tYCgCrHmGyElBFGLI0imNKCRUMYU9gXyrZmNuIsYrhgGK11
1712 iYdJyROiHxYFkTcp21K3ll6wJ/kDPUkfTHAJxBlh4HDcsGGkwHiRSCxeLMaZhQUNho1pWx2cF7vi
1713 zImEIUTwvBhBDnE2YukOIQty58BGZ2qjn5x77snh/EnHE2CASAkdOCvXyZBYSGiYPAzOOe/lLpe7
1714 EBhz4moY5nHBan8kMmG/sMQhDnicotKcy6CwZcuWLVu2bNnyFx8heaPLAVEHAAAAAElFTkSuQmCC
1715
1716 "
1717 preserveAspectRatio="none"
1718 height="58.320763"
1719 width="49.219746" />
1720 <image
1721 y="548.36224"
1722 x="676.33337"
1723 id="image9137-6-1-0-3"
1724 xlink:href="data:image/png;base64,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 "
1725 preserveAspectRatio="none"
1726 height="119.99996"
1727 width="106.66663" />
1728 </g>
1729 </svg>
00 # Build Snapcast
1
12 Clone the Snapcast repository. To do this, you need git.
23 For Debian derivates (e.g. Raspbian, Debian, Ubuntu, Mint):
34
4 $ sudo apt-get install git
5 ```sh
6 sudo apt-get install git
7 ```
58
69 For Arch derivates:
710
8 $ sudo pacman -S git
11 ```sh
12 sudo pacman -S git
13 ```
914
1015 For FreeBSD:
1116
12 $ sudo pkg install git
17 ```sh
18 sudo pkg install git
19 ```
1320
1421 Clone Snapcast:
1522
16 $ git clone https://github.com/badaix/snapcast.git
23 ```sh
24 git clone https://github.com/badaix/snapcast.git
25 ```
1726
1827 this creates a directory `snapcast`, in the following referred to as `<snapcast dir>`.
1928 Next clone the external submodules:
2029
21 $ cd <snapcast dir>/externals
22 $ git submodule update --init --recursive
30 ```sh
31 cd <snapcast dir>/externals
32 git submodule update --init --recursive
33 ```
2334
2435 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`.
2536 For `cmake` you must add the path to the `-DBOOST_ROOT` flag: `cmake -DBOOST_ROOT=/path/to/boost_1_7x_0`
2637
2738 ## Linux (Native)
39
2840 Install the build tools and required libs:
2941 For Debian derivates (e.g. Raspbian, Debian, Ubuntu, Mint):
3042
31 $ sudo apt-get install build-essential
32 $ sudo apt-get install libasound2-dev libvorbisidec-dev libvorbis-dev libopus-dev libflac-dev libsoxr-dev alsa-utils libavahi-client-dev avahi-daemon expat
43 ```sh
44 sudo apt-get install build-essential
45 sudo apt-get install libasound2-dev libvorbisidec-dev libvorbis-dev libopus-dev libflac-dev libsoxr-dev alsa-utils libavahi-client-dev avahi-daemon libexpat1-dev
46 ```
3347
3448 Compilation requires gcc 4.8 or higher, so it's highly recommended to use Debian (Raspbian) Jessie.
3549
3650 For Arch derivates:
3751
38 $ sudo pacman -S base-devel
39 $ sudo pacman -S alsa-lib avahi libvorbis opus-dev flac libsoxr alsa-utils boost expat
52 ```sh
53 sudo pacman -S base-devel
54 sudo pacman -S alsa-lib avahi libvorbis opus-dev flac libsoxr alsa-utils boost expat
55 ```
4056
4157 For Fedora (and probably RHEL, CentOS, & Scientific Linux, but untested):
4258
43 $ sudo dnf install @development-tools
44 $ sudo dnf install alsa-lib-devel avahi-devel libvorbis-devel opus-devel flac-devel soxr-devel libstdc++-static expat
59 ```sh
60 sudo dnf install @development-tools
61 sudo dnf install alsa-lib-devel avahi-devel libvorbis-devel opus-devel flac-devel soxr-devel libstdc++-static expat boost-devel
62 ```
4563
4664 ### Build Snapclient and Snapserver
65
4766 `cd` into the Snapcast src-root directory:
4867
49 $ cd <snapcast dir>
50 $ make
68 ```sh
69 cd <snapcast dir>
70 make
71 ```
5172
5273 Install Snapclient and/or Snapserver:
5374
54 $ sudo make installserver
55 $ sudo make installclient
75 ```sh
76 sudo make installserver
77 sudo make installclient
78 ```
5679
5780 This will copy the client and/or server binary to `/usr/bin` and update init.d/systemd to start the client/server as a daemon.
5881
5982 ### Build Snapclient
83
6084 `cd` into the Snapclient src-root directory:
6185
62 $ cd <snapcast dir>/client
63 $ make
86 ```sh
87 cd <snapcast dir>/client
88 make
89 ```
6490
6591 Install Snapclient
6692
67 $ sudo make install
93 ```sh
94 sudo make install
95 ```
6896
6997 This will copy the client binary to `/usr/bin` and update init.d/systemd to start the client as a daemon.
7098
7199 ### Build Snapserver
100
72101 `cd` into the Snapserver src-root directory:
73102
74 $ cd <snapcast dir>/server
75 $ make
103 ```sh
104 cd <snapcast dir>/server
105 make
106 ```
76107
77108 Install Snapserver
78109
79 $ sudo make install
110 ```sh
111 sudo make install
112 ```
80113
81114 This will copy the server binary to `/usr/bin` and update init.d/systemd to start the server as a daemon.
82115
84117
85118 Debian packages can be made with
86119
87 $ sudo apt-get install debhelper
88 $ cd <snapcast dir>
89 $ fakeroot make -f debian/rules binary
120 ```sh
121 sudo apt-get install debhelper
122 cd <snapcast dir>
123 fakeroot make -f debian/rules binary
124 ```
90125
91126 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
127
128 ```sh
129 fakeroot make -f debian/rules CPPFLAGS="-I/path/to/boost_1_7x_0" binary
130 ```
94131
95132 ## FreeBSD (Native)
133
96134 Install the build tools and required libs:
97135
98 $ sudo pkg install gmake gcc bash avahi libogg libvorbis libopus flac libsoxr
136 ```sh
137 sudo pkg install gmake gcc bash avahi libogg libvorbis libopus flac libsoxr
138 ```
99139
100140 ### Build Snapserver
141
101142 `cd` into the Snapserver src-root directory:
102143
103 $ cd <snapcast dir>/server
104 $ gmake TARGET=FREEBSD
144 ```sh
145 cd <snapcast dir>/server
146 gmake TARGET=FREEBSD
147 ```
105148
106149 Install Snapserver
107150
108 $ sudo gmake TARGET=FREEBSD install
151 ```sh
152 sudo gmake TARGET=FREEBSD install
153 ```
109154
110155 This will copy the server binary to `/usr/local/bin` and the startup script to `/usr/local/etc/rc.d/snapserver`. To enable the Snapserver, add this line to `/etc/rc.conf`:
111156
112 snapserver_enable="YES"
157 ```ini
158 snapserver_enable="YES"
159 ```
113160
114161 For additional command line arguments, add in `/etc/rc.conf`:
115162
116 snapserver_opts="<your custom options>"
163 ```ini
164 snapserver_opts="<your custom options>"
165 ```
117166
118167 Start and stop the server with `sudo service snapserver start` and `sudo service snapserver stop`.
119168
120
121169 ## Gentoo (native)
122170
123171 Snapcast is available under Gentoo's [Portage](https://wiki.gentoo.org/wiki/Portage) package management system. Portage utilises `USE` flags to determine what components are built on compilation. The availabe options are...
124172
125 equery u snapcast
126 [ Legend : U - final flag setting for installation]
127 [ : I - package is installed with flag ]
128 [ Colors : set, unset ]
129 * Found these USE flags for media-sound/snapcast-9999:
130 U I
131 + - avahi : Build with avahi support
132 + + client : Build and install Snapcast client component
133 + - flac : Build with FLAC compression support
134 + + server : Build and install Snapcast server component
135 - - static-libs : Build static libs
136 - - tremor : Build with TREMOR version of vorbis
137 + - vorbis : Build with libvorbis support
138
173 ```sh
174 equery u snapcast
175 [ Legend : U - final flag setting for installation]
176 [ : I - package is installed with flag ]
177 [ Colors : set, unset ]
178 * Found these USE flags for media-sound/snapcast-9999:
179 U I
180 + - avahi : Build with avahi support
181 + + client : Build and install Snapcast client component
182 + - flac : Build with FLAC compression support
183 + + server : Build and install Snapcast server component
184 - - static-libs : Build static libs
185 - - tremor : Build with TREMOR version of vorbis
186 + - vorbis : Build with libvorbis support
187 ```
139188
140189 These can be set either in the [global configuration](https://wiki.gentoo.org/wiki//etc/portage/make.conf#USE) file `/etc/portage/make.conf` or on a per-package basis (as root):
141190
142 if [ ! -d "$DIRECTORY" ]; then
143 mkdir /etc/portage/package.use/media-sound
144 fi
145 echo 'media-sound/snapcast client server flac
191 ```sh
192 if [ ! -d "$DIRECTORY" ]; then
193 mkdir /etc/portage/package.use/media-sound
194 fi
195 echo 'media-sound/snapcast client server flac
196 ```
146197
147198 If for example you only wish to build the server and *not* the client then precede the server `USE` flag with `-` i.e.
148199
149 echo 'media-sound/snapcast client -server
200 ```sh
201 echo 'media-sound/snapcast client -server
202 ```
150203
151204 Once `USE` flags are configured emerge snapcast as root:
152205
153 $ emerge -av snapcast
154
206 ```sh
207 emerge -av snapcast
208 ```
155209
156210 Starting the client or server depends on whether you are using `systemd` or `openrc`. To start using `openrc`:
157211
158 /etc/init.d/snapclient start
159 /etc/init.d/snapserver start
212 ```sh
213 /etc/init.d/snapclient start
214 /etc/init.d/snapserver start
215 ```
160216
161217 To enable the serve and client to start under the default run-level:
162218
163 rc-update add snapserver default
164 rc-update add snapclient default
165
219 ```sh
220 rc-update add snapserver default
221 rc-update add snapclient default
222 ```
166223
167224 ## macOS (Native)
168225
169 *Warning: macOS support is experimental*
226 *Warning:* macOS support is experimental
170227
171228 1. Install Xcode from the App Store
172229 2. Install [Homebrew](http://brew.sh)
173230 3. Install the required libs
174231
175 ```
176 $ brew install flac libsoxr libvorbis boost opus
232 ```ssh
233 brew install flac libsoxr libvorbis boost opus
177234 ```
178235
179236 ### Build Snapclient
237
180238 `cd` into the Snapclient src-root directory:
181239
182 $ cd <snapcast dir>/client
183 $ make TARGET=MACOS
240 ```sh
241 cd <snapcast dir>/client
242 make TARGET=MACOS
243 ```
184244
185245 Install Snapclient
186246
187 $ sudo make install TARGET=MACOS
247 ```sh
248 sudo make install TARGET=MACOS
249 ```
188250
189251 This will copy the client binary to `/usr/local/bin` and create a Launch Agent to start the client as a daemon.
190252
191253 ### Build Snapserver
254
192255 `cd` into the Snapserver src-root directory:
193256
194 $ cd <snapcast dir>/server
195 $ make TARGET=MACOS
257 ```sh
258 cd <snapcast dir>/server
259 make TARGET=MACOS
260 ```
196261
197262 Install Snapserver
198263
199 $ sudo make install TARGET=MACOS
264 ```sh
265 sudo make install TARGET=MACOS
266 ```
200267
201268 This will copy the server binary to `/usr/local/bin` and create a Launch Agent to start the server as a daemon.
202269
203270 ## Android (Cross compile)
204 Cross compilation for Android is done with the [Android NDK](http://developer.android.com/tools/sdk/ndk/index.html) on a Linux host machine.
271
272 Cross compilation for Android is done with the [Android NDK](http://developer.android.com/tools/sdk/ndk/index.html) on a Linux host machine.
205273
206274 ### Android NDK setup
207 http://developer.android.com/ndk/guides/standalone_toolchain.html
208 1. Download NDK: `https://dl.google.com/android/repository/android-ndk-r17b-linux-x86_64.zip`
209 2. Extract to: `/SOME/LOCAL/PATH/android-ndk-r17b`
210 3. Setup toolchains for arm and x86 somewhere in your home dir (`<android-ndk dir>`):
211
212 ```
213 $ cd /SOME/LOCAL/PATH/android-ndk-r17/build/tools
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
216 $ ./make_standalone_toolchain.py --arch x86 --api 16 --stl libc++ --install-dir <android-ndk dir>-x86
217 ```
275
276 Install the Android [NDK toolchain](http://developer.android.com/ndk/guides/standalone_toolchain.html)
277
278 1. Download NDK: `https://dl.google.com/android/repository/android-ndk-r21d-linux-x86_64.zip`
279 2. Extract to: `/SOME/LOCAL/PATH/android-ndk-r21d`
218280
219281 ### Build Snapclient
282
220283 Cross compile and install FLAC, opus, ogg, and tremor (only needed once):
221284
222 $ cd <snapcast dir>/externals
223 $ make NDK_DIR=<android-ndk dir>-arm ARCH=arm
224 $ make NDK_DIR=<android-ndk dir>-arm64 ARCH=aarch64
225 $ make NDK_DIR=<android-ndk dir>-x86 ARCH=x86
226
285 ```sh
286 cd <snapcast dir>/externals
287 make NDK_DIR=<android-ndk dir> ARCH=arm
288 make NDK_DIR=<android-ndk dir> ARCH=aarch64
289 make NDK_DIR=<android-ndk dir> ARCH=x86
290 ```
291
227292 Compile the Snapclient:
228293
229 $ cd <snapcast dir>/client
230 $ ./build_android_all.sh <android-ndk dir> <snapdroid jniLibs dir>
294 ```sh
295 cd <snapcast dir>/client
296 ./build_android.sh <android-ndk dir> <snapdroid jniLibs dir>
297 ```
231298
232299 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.
233300
234
235301 ## OpenWrt/LEDE (Cross compile)
236 Cross compilation for OpenWrt is done with the [OpenWrt build system](https://wiki.openwrt.org/about/toolchain) on a Linux host machine:
237 https://wiki.openwrt.org/doc/howto/build
238
239 For LEDE:
240 https://lede-project.org/docs/guide-developer/quickstart-build-images
241
242 ### OpenWrt/LEDE build system setup
243 https://wiki.openwrt.org/doc/howto/buildroot.exigence
244
245 Clone OpenWrt to some place in your home directory (`<buildroot dir>`)
246
247 $ git clone git://git.openwrt.org/15.05/openwrt.git
248
249 ...LEDE
250
251 $ git clone https://git.lede-project.org/source.git
252
253 Download and install available feeds
254
255 $ cd <buildroot dir>
256 $ ./scripts/feeds update -a
257 $ ./scripts/feeds install -a
258
259 Within the `<buildroot dir>` directory create symbolic links to the Snapcast source directory `<snapcast source>` and to the OpenWrt Makefile:
260
261 $ mkdir -p <buildroot dir>/package/sxx/snapcast
262 $ cd <buildroot dir>/package/sxx/snapcast
263 $ ln -s <snapcast source> src
264 $ ln -s <snapcast source>/openWrt/Makefile.openwrt Makefile
265
266 Build
267 in menuconfig in `sxx/snapcast` select `Compile snapserver` and/or `Compile snapclient`
268
269 $ cd <buildroot dir>
270 $ make defconfig
271 $ make menuconfig
272 $ make
273
274 Rebuild Snapcast:
275
276 $ make package/sxx/snapcast/clean
277 $ make package/sxx/snapcast/compile
278
279 The packaged `ipk` files are for OpenWrt in `<buildroot dir>/bin/ar71xx/packages/base/snap[client|server]_x.x.x_ar71xx.ipk` and for LEDE `<buildroot dir>/bin/packages/mips_24kc/base/snap[client|server]_x.x.x_mips_24kc.ipk`
302
303 To cross compile for OpenWrt, please follow the [OpenWrt flavored SnapOS guide](https://github.com/badaix/snapos/blob/master/openwrt/README.md)
280304
281305 ## Buildroot (Cross compile)
282 This example will show you how to add snapcast to [Buildroot](https://buildroot.org/).
283
284 ### Buildroot setup
285 Buildroot recommends [keeping customizations outside of the main Buildroot directory](https://buildroot.org/downloads/manual/manual.html#outside-br-custom) which is what this example will walk through.
286
287 Clone Buildroot to some place in your home directory (`<buildroot dir>`):
288
289 $ BUILDROOT_VERSION=2016.11.2
290 $ git clone --branch $BUILDROOT_VERSION --depth=1 git://git.buildroot.net/buildroot
291
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.
293
294 Now configure buildroot with the [required packages](/buildroot/configs/snapcast_defconfig) (you can also manually add them to your project's existing defconfig):
295
296 $ cd <buildroot dir> && make BR2_EXTERNAL=<snapcast dir>/buildroot snapcast_defconfig
297
298 Then use `menuconfig` to configure the rest of your project:
299
300 $ cd <buildroot dir> && make BR2_EXTERNAL=<snapcast dir>/buildroot menuconfig
301
302 And finally run the build:
303
304 $ cd <buildroot dir> && make BR2_EXTERNAL=<snapcast dir>/buildroot
305
306 ## Raspberry Pi (Cross compile)
307 This example will show you how to add snapcast to [Buildroot](https://buildroot.org/) and compile for Raspberry Pi.
308
309 * https://github.com/nickaknudson/snapcast-pi
306
307 To integrate Snapcast into [Buildroot](https://buildroot.org/), please follow the [Buildroot flavored SnapOS guide](https://github.com/badaix/snapos/blob/master/buildroot-external/README.md)
308
309 ## Windows (vcpkg)
310
311 Prerequisites:
312
313 1. CMake
314 2. Visual Studio 2017 or 2019 with C++
315
316 Set up [vcpkg](https://github.com/Microsoft/vcpkg)
317
318 Install dependencies
319
320 ```sh
321 vcpkg.exe install libflac libvorbis soxr opus boost-asio --triplet x64-windows
322 ```
323
324 Build
325
326 ```sh
327 cd <snapcast dir>
328 mkdir build
329 cd build && cmake .. -DCMAKE_TOOLCHAIN_FILE=<vcpkg_dir>/scripts/buildsystems/vcpkg.cmake
330 cmake --build . --config Release
331 ```
0 # Configuration
1
2 ## Sources
3
4 Audio sources are added to the server as `source` in the `[stream]` section of the configuration file `/etc/snapserver.conf`. Every source must be fed with a fixed sample format, that can be configured per stream (e.g. `48000:16:2`).
5
6 The following notation is used in this paragraph:
7
8 - `<angle brackets>`: the whole expression must be replaced with your specific setting
9 - `[square brackets]`: the whole expression is optional and can be left out
10 - `[key=value]`: if you leave this option out, "value" will be the default for "key"
11
12 The general format of an audio source is:
13
14 ```sh
15 TYPE://host/path?name=<name>[&codec=<codec>][&sampleformat=<sampleformat>][&chunk_ms=<chunk ms>]
16 ```
17
18 parameters have the form `key=value`, they are concatenated with an `&` character.
19 Parameter `name` is mandatory for all sources, while `codec`, `sampleformat` and `chunk_ms` are optional
20 and will override the default `codec`, `sampleformat` or `chunk_ms` settings.
21 Non blocking sources support the `dryout_ms` parameter: when no new data is read from the source, send silence to the clients
22
23 Available audio source types are:
24
25 ### pipe
26
27 Captures audio from a named pipe
28
29 ```sh
30 pipe:///<path/to/pipe>?name=<name>[&mode=create][&dryout_ms=2000]
31 ```
32
33 `mode` can be `create` or `read`. 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 `read`
34
35 ### librespot
36
37 Launches librespot and reads audio from stdout
38
39 ```sh
40 librespot:///<path/to/librespot>?name=<name>[&dryout_ms=2000][&username=<my username>&password=<my password>][&devicename=Snapcast][&bitrate=320][&wd_timeout=7800][&volume=100][&onevent=""][&normalize=false][&autoplay=false][&cache=""][&disable_audio_cache=false][&killall=true][&params=extra-params]
41 ```
42
43 Note that you need to have the librespot binary on your machine and the sampleformat will be set to `44100:16:2`
44
45 #### Available parameters
46
47 Parameters used to configure the librespot binary ([see librespot-org options](https://github.com/librespot-org/librespot/wiki/Options)):
48
49 - `username`: Username to sign in with
50 - `password`: Password
51 - `devicename`: Device name
52 - `bitrate`: Bitrate (96, 160 or 320). Defaults to 320
53 - `volume`: Initial volume in %, once connected [0-100]
54 - `onevent`: The path to a script that gets run when one of librespot's events is triggered
55 - `normalize`: Enables volume normalisation for librespot
56 - `autoplay`: Autoplay similar songs when your music ends
57 - `cache`: Path to a directory where files will be cached
58 - `disable_audio_cache`: Disable caching of the audio data
59 - `params`: Optional string appended to the librespot invocation. This allows for arbitrary flags to be passed to librespot, for instance `params=--device-type%20avr`. The value has to be properly URL-encoded.
60
61 Parameters introduced by Snapclient:
62
63 - `killall`: Kill all running librespot instances before launching librespot
64 - `wd_timeout`: Restart librespot if it doesn't create log messages for x seconds
65
66 ### airplay
67
68 Launches [shairport-sync](https://github.com/mikebrady/shairport-sync) and reads audio from stdout
69
70 ```sh
71 airplay:///<path/to/shairport-sync>?name=<name>[&dryout_ms=2000][&devicename=Snapcast][&port=5000]
72 ```
73
74 Note that you need to have the shairport-sync binary on your machine and the sampleformat will be set to `44100:16:2`
75
76 #### Available parameters
77
78 - `devicename`: Advertised name
79 - `port`: RTSP listening port
80
81 ### file
82
83 Reads PCM audio from a file
84
85 ```sh
86 file:///<path/to/PCM/file>?name=<name>
87 ```
88
89 ### process
90
91 Launches a process and reads audio from stdout
92
93 ```sh
94 process:///<path/to/process>?name=<name>[&dryout_ms=2000][&wd_timeout=0][&log_stderr=false][&params=<process arguments>]
95 ```
96
97 #### Available parameters
98
99 - `wd_timeout`: kill and restart the process if there was no message logged for x seconds to stderr (0 = disabled)
100 - `log_stderr`: Forward stderr log messages to Snapclient logging
101 - `params`: Params to start the process with
102
103 ### tcp server
104
105 Receives audio from a TCP socket (acting as server)
106
107 ```sh
108 tcp://<listen IP, e.g. 127.0.0.1>:<port>?name=<name>[&mode=server]
109 ```
110
111 default for `port` (if omitted) is 4953, default for `mode` is `server`
112
113 Mopdiy configuration would look like this (running GStreamer in [client mode](https://www.freedesktop.org/software/gstreamer-sdk/data/docs/latest/gst-plugins-base-plugins-0.10/gst-plugins-base-plugins-tcpclientsink.html))
114
115 ```sh
116 [audio]
117 output = audioresample ! audioconvert ! audio/x-raw,rate=48000,channels=2,format=S16LE ! wavenc ! tcpclientsink
118 ```
119
120 ### tcp client
121
122 Receives audio from a TCP socket (acting as client)
123
124 ```sh
125 tcp://<server IP, e.g. 127.0.0.1>:<port>?name=<name>&mode=client
126 ```
127
128 Mopdiy configuration would look like this (running GStreamer in [server mode](https://www.freedesktop.org/software/gstreamer-sdk/data/docs/latest/gst-plugins-base-plugins-0.10/gst-plugins-base-plugins-tcpserversink.html)):
129
130 ```sh
131 [audio]
132 output = audioresample ! audioconvert ! audio/x-raw,rate=48000,channels=2,format=S16LE ! wavenc ! tcpserversink
133 ```
134
135 ### alsa
136
137 Captures audio from an alsa device
138
139 ```sh
140 alsa://?name=<name>&device=<alsa device>
141 ```
142
143 `device` is an alsa device name or identifier, e.g. `default` or `hw:0,0`
144
145 The output of any audio player that uses alsa can be redirected to Snapcast by using an alsa loopback device:
146
147 1. Setup the alsa loopback device by loading the kernel module:
148
149 ```sh
150 sudo modprobe snd-aloop
151 ```
152
153 The loopback device can be created during boot by adding `snd-aloop` to `/etc/modules`
154
155 2. The loopback device should show up in `aplay -l`
156
157 ```sh
158 aplay -l
159 **** List of PLAYBACK Hardware Devices ****
160 card 0: Loopback [Loopback], device 0: Loopback PCM [Loopback PCM]
161 Subdevices: 8/8
162 Subdevice #0: subdevice #0
163 Subdevice #1: subdevice #1
164 Subdevice #2: subdevice #2
165 Subdevice #3: subdevice #3
166 Subdevice #4: subdevice #4
167 Subdevice #5: subdevice #5
168 Subdevice #6: subdevice #6
169 Subdevice #7: subdevice #7
170 card 0: Loopback [Loopback], device 1: Loopback PCM [Loopback PCM]
171 Subdevices: 8/8
172 Subdevice #0: subdevice #0
173 Subdevice #1: subdevice #1
174 Subdevice #2: subdevice #2
175 Subdevice #3: subdevice #3
176 Subdevice #4: subdevice #4
177 Subdevice #5: subdevice #5
178 Subdevice #6: subdevice #6
179 Subdevice #7: subdevice #7
180 card 1: Intel [HDA Intel], device 0: CX20561 Analog [CX20561 Analog]
181 Subdevices: 1/1
182 Subdevice #0: subdevice #0
183 card 1: Intel [HDA Intel], device 1: CX20561 Digital [CX20561 Digital]
184 Subdevices: 1/1
185 Subdevice #0: subdevice #0
186 card 2: CODEC [USB Audio CODEC], device 0: USB Audio [USB Audio]
187 Subdevices: 0/1
188 Subdevice #0: subdevice #0
189 ```
190
191 In this example the loopback device is card 0 with devices 0 and 1, each having 8 subdevices.
192 The devices are addressed with `hw:<card idx>,<device idx>,<subdevice num>`, e.g. `hw:0,0,0`.
193 If a process plays audio using `hw:0,0,x`, then the audio will be looped back to `hw:0,1,x`
194
195 3. Configure your player to use a loopback device
196
197 For mopidy (gstreamer) in `mopidy.conf`:
198
199 ```sh
200 output = audioresample ! audioconvert ! audio/x-raw,rate=48000,channels=2,format=S16LE ! alsasink device=hw:0,0,0
201 ```
202
203 For mpd: in `mpd.conf`
204
205 ```sh
206 audio_output_format "48000:16:2"
207 audio_output {
208 type "alsa"
209 name "My ALSA Device"
210 device "hw:0,0,0" # optional
211 # auto_resample "no"
212 # mixer_type "hardware" # optional
213 # mixer_device "default" # optional
214 # mixer_control "PCM" # optional
215 # mixer_index "0" # optional
216 }
217 ```
218
219 4. Configure Snapserver to capture the loopback device:
220
221 ```sh
222 [stream]
223 stream = alsa://?name=SomeName&sampleformat=48000:16:2&device=hw:0,1,0
224 ```
225
226 ### meta
227
228 Read and mix audio from other stream sources
229
230 ```sh
231 meta:///<name of source#1>/<name of source#2>/.../<name of source#N>?name=<name>
232 ```
233
234 Plays audio from the active source with the highest priority, with `source#1` having the highest priority and `source#N` the lowest.
235 Use `codec=null` for stream sources that should only serve as input for meta streams
0 # Contributions
0 # Install Linux packages
1
2 Snapcast packages are available for several Linux distributions:
3
4 - [Debian](#debian)
5 - [OpenWrt](#openwrt)
6 - [Alpine Linux](#alpine-linux)
7 - [Archlinux](#archlinux)
8 - [Void Linux](#void-linux)
9
10 ## Debian
11
12 For Debian (and Debian-based systems, such as Ubuntu, Linux Mint, ElementaryOS) download the package for your CPU architecture from the [latest release page](https://github.com/badaix/snapcast/releases/latest).
13
14 e.g. for Raspberry Pi `snapclient_0.x.x_armhf.deb`, for laptops `snapclient_0.x.x_amd64.deb`
15
16 ### using apt 1.1 or later
17
18 sudo apt install </path/to/snapclient_0.x.x_armhf.deb>
19
20 or
21
22 sudo apt install </path/to/snapserver_0.x.x_armhf.deb>
23
24 ### using dpkg
25
26 Install the package:
27
28 sudo dpkg -i snapclient_0.x.x_armhf.deb
29
30 or
31
32 sudo dpkg -i snapclient_0.x.x_amd64.deb
33
34 Install missing dependencies:
35
36 sudo apt-get -f install
37
38 ## OpenWrt
39
40 On OpenWrt do:
41
42 opkg install snapclient_0.x.x_ar71xx.ipk
43
44 ## Alpine Linux
45
46 On Alpine Linux do:
47
48 apk add snapcast
49
50 Or, for just the client:
51
52 apk add snapcast-client
53
54 Or, for just the server:
55
56 apk add snapcast-server
57
58 ## Gentoo Linux
59
60 On Gentoo Linux do:
61
62 emerge --ask media-sound/snapcast
63
64 ## Archlinux
65
66 On Archlinux, Snapcast is available through the AUR. To install, use your favorite AUR helper, or do:
67
68 git clone https://aur.archlinux.org/snapcast
69 cd snapcast
70 makepkg -si
71
72 ## Void Linux
73
74 To install the client:
75
76 # xbps-install snapclient
77
78 To install the server:
79
80 # xbps-install snapserver
00 Snapcast JSON RPC Control API
11 =============================
2 Snapcast can be controlled with a [JSON-RPC 2.0](http://www.jsonrpc.org/specification) API over a raw TCP-Socket interface on port 1705.
3
4 Single JSON Messages are new line delimited ([ndjson](http://ndjson.org/)).
5
6 For simple tests you can fire JSON commands on a telnet connection and watch Notifications coming in:
2
3 ## Raw TCP sockets
4
5 Snapcast can be controlled with a [JSON-RPC 2.0](http://www.jsonrpc.org/specification)
6 API over a raw TCP-Socket interface on port 1705.
7
8 Single JSON Messages are new line delimited ([ndjson](http://ndjson.org/)).
9
10 For simple tests you can fire JSON commands on a telnet connection and watch
11 Notifications coming in:
712
813 ```json
914 $ telnet localhost 1705
1823 {"jsonrpc":"2.0","method":"Client.OnConnect","params":{"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":1488065507,"usec":820050},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.11.0-beta-1"}},"id":"00:21:6a:7d:74:fc"}}
1924 ```
2025
21 In the following the supported Requests and Notifications are described.
26 ## HTTP
27
28 Snapcast can also be controlled on port 1780 using the HTTP protocol to (1) send
29 a single `POST` request or (2) create a long-lived `WebSocket`.
30
31 For simple tests, you can fire JSON commands directly within your browser.
32 One-shot `POST` requests receive only the immediate response of the request,
33 whereas long-lived `WebSocket`s will also receive Notifications similar to the
34 telnet connection above:
35
36 ```js
37
38 const host = '127.0.0.1:1780';
39 const request = {
40 'id': 0,
41 'jsonrpc': '2.0',
42 'method': 'Server.GetRPCVersion'
43 };
44
45 // XHR
46 const xhr = new XMLHttpRequest();
47 xhr.open('POST', `http://${host}/jsonrpc`);
48 xhr.setRequestHeader('Content-Type', 'application/json');
49 xhr.setRequestHeader('Accept', 'application/json');
50 xhr.addEventListener('load', ({ currentTarget: xhr }) => {
51 console.log(JSON.parse(xhr.responseText)); // {"id":1,"jsonrpc":"2.0","result":{"major":2,"minor":0,"patch":0}}
52 });
53 xhr.send(JSON.stringify(++request.id && request));
54
55 // Fetch
56 fetch(`http://${host}/jsonrpc`, {
57 method: 'POST',
58 headers: {
59 'Accept': 'application/json',
60 'Content-Type': 'application/json'
61 },
62 body: JSON.stringify(++request.id && request)
63 })
64 .then(response => response.json())
65 .then(content => console.log(content)); // {"id":2,"jsonrpc":"2.0","result":{"major":2,"minor":0,"patch":0}}
66
67 // Fetch with await/async
68 const response = await fetch(`http://${host}/jsonrpc`, {
69 method: 'POST',
70 headers: {
71 'Accept': 'application/json',
72 'Content-Type': 'application/json'
73 },
74 body: JSON.stringify(++request.id && request)
75 });
76 const content = await response.json();
77 console.log(content); // {"id":3,"jsonrpc":"2.0","result":{"major":2,"minor":0,"patch":0}}
78
79
80 // WebSocket
81 const ws = new WebSocket(`ws://${host}/jsonrpc`);
82 ws.addEventListener('message', (message) => {
83 console.log(JSON.parse(message.data)); // {"id":4,"jsonrpc":"2.0","result":{"major":2,"minor":0,"patch":0}}
84 });
85 ws.addEventListener('open', () => ws.send(JSON.stringify(++request.id && request)));
86
87 /*
88 WebSockets receive Notifications of events. Connect a client, and you will eventually see in your console:
89
90 {
91 "jsonrpc": "2.0",
92 "method": "Client.OnConnect",
93 "params": { ... }
94 }
95 */
96 ```
97
98 ## Requests and Notifications
2299
23100 The client that sends a "Set" command will receive a Response, while the other connected control clients will receive a Notification "On" event.
24101 Commands can be sent in a [Batch](http://www.jsonrpc.org/specification#batch). The server will reply with a Batch and send a Batch notification to the other clients. This way the volume of multiple Snapclients can be changed with a single Batch Request.
0 Setup of audio players/server
1 -----------------------------
0 # Setup of audio players/server
1
22 Snapcast can be used with a number of different audio players and servers, and so it can be integrated into your favorite audio-player solution and make it synced-multiroom capable.
33 The only requirement is that the player's audio can be redirected into the Snapserver's fifo `/tmp/snapfifo`. In the following configuration hints for [MPD](http://www.musicpd.org/) and [Mopidy](https://www.mopidy.com/) are given, which are the base of other audio player solutions, like [Volumio](https://volumio.org/) or [RuneAudio](http://www.runeaudio.com/) (both MPD) or [Pi MusicBox](http://www.pimusicbox.com/) (Mopidy).
44
55 The goal is to build the following chain:
66
7 audio player software -> snapfifo -> snapserver -> network -> snapclient -> alsa
8
9 #### Streams
7 ```plain_text
8 audio player software -> snapfifo -> snapserver -> network -> snapclient -> alsa
9 ```
10
11 ## Streams
12
1013 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.
1114 Within the config file a list of input streams can be configured in the `[stream]` section:
1215
13 ```
16 ```ini
1417 [stream]
1518 ...
1619 # stream URI of the PCM input stream, can be configured multiple times
17 # Format: TYPE://host/path?name=NAME[&codec=CODEC][&sampleformat=SAMPLEFORMAT]
20 # Format: TYPE://host/path?name=NAME[&codec=CODEC][&sampleformat=SAMPLEFORMAT]
1821 stream = pipe:///tmp/snapfifo?name=default
1922 ...
2023 ```
2124
22 #### About the notation
25 The sampleformat is a triple of `<samplerate>:<bit depth>:<channels>`, e.g. `44100:16:2`.
26 The PCM samples (bit depth) must be encoded signed, little endian in 8, 16, 24, or 32 bit, where 24 is expected to be encoded in the lower three bytes of a 32 bit word.
27
28 ## About the notation
29
2330 In this document some expressions are in brackets:
31
2432 * `<angle brackets>`: the whole expression must be replaced with your specific setting
2533 * `[square brackets]`: the whole expression is optional and can be left out
2634 * `[key=value]`: if you leave this option out, `value` will be the default for `key`
2735
2836 For example:
29 ```
37
38 ```ini
3039 stream = spotify:///librespot?name=Spotify[&username=<my username>&password=<my password>][&devicename=Snapcast][&bitrate=320]
3140 ```
41
3242 * `username` and `password` are both optional in this case. You need to specify neither or both of them.
3343 * `bitrate` is optional. If not configured, `320` will be used.
3444 * `devicename` is optional. If not configured, `Snapcast` will be used.
3545
3646 For instance, a valid usage would be:
37 ```
47
48 ```ini
3849 stream = spotify:///librespot?name=Spotify&bitrate=160
3950 ```
4051
4152 ### MPD
53
4254 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.
4355
4456 Disable alsa audio output by commenting out this section:
4557
46 #audio_output {
47 # type "alsa"
48 # name "My ALSA Device"
49 # device "hw:0,0" # optional
50 # format "48000:16:2" # optional
51 # mixer_device "default" # optional
52 # mixer_control "PCM" # optional
53 # mixer_index "0" # optional
54 #}
58 ```plain_text
59 #audio_output {
60 # type "alsa"
61 # name "My ALSA Device"
62 # device "hw:0,0" # optional
63 # format "48000:16:2" # optional
64 # mixer_device "default" # optional
65 # mixer_control "PCM" # optional
66 # mixer_index "0" # optional
67 #}
68 ```
5569
5670 Add a new audio output of the type "fifo", which will let mpd play audio into the named pipe `/tmp/snapfifo`.
5771 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).
5872
59 audio_output {
60 type "fifo"
61 name "my pipe"
62 path "/tmp/snapfifo"
63 format "48000:16:2"
64 mixer_type "software"
73 ```plain_text
74 audio_output {
75 type "fifo"
76 name "my pipe"
77 path "/tmp/snapfifo"
78 format "48000:16:2"
79 mixer_type "software"
80 }
81 ```
82
83 To test your mpd installation, you can add a radio station by
84
85 ```sh
86 sudo su
87 echo "http://wdr-1live-live.icecast.wdr.de/wdr/1live/live/mp3/128/stream.mp3" > /var/lib/mpd/playlists/einslive.m3u
88 ```
89
90 ### Mopidy
91
92 [Mopidy](https://www.mopidy.com/) can stream the audio output into the Snapserver's fifo with a `filesink` as audio output in `mopidy.conf`:
93
94 ```ini
95 [audio]
96 #output = autoaudiosink
97 output = audioresample ! audioconvert ! audio/x-raw,rate=48000,channels=2,format=S16LE ! filesink location=/tmp/snapfifo
98 ```
99
100 With newer kernels one might also have to change this sysctl-setting, as default settings have changed recently: `sudo sysctl fs.protected_fifos=0`
101
102 See [stackexchange](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.
103
104 ### FFmpeg
105
106 Pipe FFmpeg's audio output to the snapfifo:
107
108 ```sh
109 ffmpeg -y -i http://wms-15.streamsrus.com:11630 -f u16le -acodec pcm_s16le -ac 2 -ar 48000 /tmp/snapfifo
110 ```
111
112 ### mpv
113
114 Pipe mpv's audio output to the snapfifo. For version < 0.21.0:
115
116 ```sh
117 mpv http://wms-15.streamsrus.com:11630 --audio-display=no --audio-channels=stereo --audio-samplerate=48000 --audio-format=s16 --ao=pcm:file=/tmp/snapfifo
118 ```
119
120 For version >= 0.21.0:
121
122 ```sh
123 mpv http://wms-15.streamsrus.com:11630 --audio-display=no --audio-channels=stereo --audio-samplerate=48000 --audio-format=s16 --ao=pcm --ao-pcm-file=/tmp/snapfifo
124 ```
125
126 ### MPlayer
127
128 Use `-novideo` and `-ao` to pipe MPlayer's audio output to the snapfifo:
129
130 ```sh
131 mplayer http://wms-15.streamsrus.com:11630 -novideo -channels 2 -srate 48000 -af format=s16le -ao pcm:file=/tmp/snapfifo
132 ```
133
134 ### Alsa
135
136 If the player cannot be configured to route the audio stream into the snapfifo, Alsa or PulseAudio can be redirected, resulting in this chain:
137
138 ```plain_text
139 audio player software -> Alsa -> Alsa file plugin -> snapfifo -> snapserver -> network -> snapclient -> Alsa
140 ```
141
142 Edit or create your Alsa config `/etc/asound.conf` like this:
143
144 ```plain_text
145 pcm.!default {
146 type plug
147 slave.pcm rate48000Hz
148 }
149
150 pcm.rate48000Hz {
151 type rate
152 slave {
153 pcm writeFile # Direct to the plugin which will write to a file
154 format S16_LE
155 rate 48000
65156 }
66
67 To test your mpd installation, you can add a radio station by
68
69 $ sudo su
70 $ echo "http://wdr-1live-live.icecast.wdr.de/wdr/1live/live/mp3/128/stream.mp3" > /var/lib/mpd/playlists/einslive.m3u
71
72 ### Mopidy
73 [Mopidy](https://www.mopidy.com/) can stream the audio output into the Snapserver's fifo with a `filesink` as audio output in `mopidy.conf`:
74
75 [audio]
76 #output = autoaudiosink
77 output = audioresample ! audioconvert ! audio/x-raw,rate=48000,channels=2,format=S16LE ! wavenc ! filesink location=/tmp/snapfifo
78
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`
80
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.
82
83 ### FFmpeg
84 Pipe FFmpeg's audio output to the snapfifo:
85
86 ffmpeg -y -i http://wms-15.streamsrus.com:11630 -f u16le -acodec pcm_s16le -ac 2 -ar 48000 /tmp/snapfifo
87
88 ### mpv
89 Pipe mpv's audio output to the snapfifo:
90
91 mpv http://wms-15.streamsrus.com:11630 --audio-display=no --audio-channels=stereo --audio-samplerate=48000 --audio-format=s16 --ao=pcm --ao-pcm-file=/tmp/snapfifo
92
93 ### MPlayer
94 Use `-novideo` and `-ao` to pipe MPlayer's audio output to the snapfifo:
95
96 mplayer http://wms-15.streamsrus.com:11630 -novideo -channels 2 -srate 48000 -af format=s16le -ao pcm:file=/tmp/snapfifo
97
98 ### Alsa
99 If the player cannot be configured to route the audio stream into the snapfifo, Alsa or PulseAudio can be redirected, resulting in this chain:
100
101 audio player software -> Alsa -> Alsa file plugin -> snapfifo -> snapserver -> network -> snapclient -> Alsa
102
103 Edit or create your Alsa config `/etc/asound.conf` like this:
104
105 ```
106 pcm.!default {
107 type plug
108 slave.pcm rate48000Hz
109 }
110
111 pcm.rate48000Hz {
112 type rate
113 slave {
114 pcm writeFile # Direct to the plugin which will write to a file
115 format S16_LE
116 rate 48000
117 }
118157 }
119158
120159 pcm.writeFile {
121 type file
122 slave.pcm null
123 file "/tmp/snapfifo"
124 format "raw"
160 type file
161 slave.pcm null
162 file "/tmp/snapfifo"
163 format "raw"
125164 }
126165 ```
127166
128167 ### PulseAudio
168
129169 Redirect the PulseAudio stream into the snapfifo:
130170
131 audio player software -> PulseAudio -> PulseAudio pipe sink -> snapfifo -> snapserver -> network -> snapclient -> Alsa
171 ```plain_text
172 audio player software -> PulseAudio -> PulseAudio pipe sink -> snapfifo -> snapserver -> network -> snapclient -> Alsa
173 ```
132174
133175 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.
134176
135177 Load the module `pipe-sink` like this:
136178
137 pacmd load-module module-pipe-sink file=/tmp/snapfifo sink_name=Snapcast format=s16le rate=48000
138 pacmd update-sink-proplist Snapcast device.description=Snapcast
179 ```plain_text
180 pacmd load-module module-pipe-sink file=/tmp/snapfifo sink_name=Snapcast format=s16le rate=48000
181 pacmd update-sink-proplist Snapcast device.description=Snapcast
182 ```
139183
140184 It might be neccessary to set the PulseAudio latency environment variable to 60 msec: `PULSE_LATENCY_MSEC=60`
141185
142
143186 ### AirPlay
187
144188 Snapserver supports [shairport-sync](https://github.com/mikebrady/shairport-sync) with the `stdout` backend.
145 1. Build shairport-sync with `stdout` backend: `./configure --with-stdout --with-avahi --with-ssl=openssl --with-metadata`
189
190 1. Build shairport-sync (version 3.3 or later) with `stdout` backend: `./configure --with-stdout --with-avahi --with-ssl=openssl --with-metadata`
146191 2. Copy the `shairport-sync` binary somewhere to your `PATH`, e.g. `/usr/local/bin/`
147192 3. Configure snapserver with `stream = airplay:///shairport-sync?name=Airplay[&devicename=Snapcast][&port=5000]`
148
149193
150194 ### Spotify
195
151196 Snapserver supports [librespot](https://github.com/librespot-org/librespot) with the `pipe` backend.
197
152198 1. Build and copy the `librespot` binary somewhere to your `PATH`, e.g. `/usr/local/bin/`
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>][&killall=true]`
154 * Valid bitrates are 96, 160, 320
155 * `start command` and `stop command` are executed by Librespot at start/stop
156 * For example: `onstart=/usr/bin/logger -t Snapcast Starting spotify...`
157 * If `killall` is `true` (default), all running instances of Librespot will be killed. This MUST be disabled on all spotify streams by setting it to `false` if you want to use multiple spotify streams.
199 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>][&disable_audio_cache=false][&killall=true]`
200 * Valid bitrates are 96, 160, 320
201 * `start command` and `stop command` are executed by Librespot at start/stop
202 * For example: `onstart=/usr/bin/logger -t Snapcast Starting spotify...`
203 * If `killall` is `true` (default), all running instances of Librespot will be killed. This MUST be disabled on all spotify streams by setting it to `false` if you want to use multiple spotify streams.
204 * If `disable_audio_cache` is `false` (default), downloaded audio files are cached in `<cache dir>`. If set to `true` audio files will not be cached on disk.
158205
159206 ### Process
160 Snapserver can start any process and read PCM data from the stdout of the process:
207
208 Snapserver can start any process and read PCM data from the stdout of the process:
161209
162210 Configure snapserver with `stream = process:///path/to/process?name=Process[&params=<--my list --of params>][&log_stderr=false]`
163211
212 For example, you could install the minimalist **mpv** media player to pick up WebRadio from a given url ...
213
214 ```ini
215 [stream]
216 stream = process:///usr/bin/mpv?name=Webradio&sampleformat=48000:16:2&params=http://129.122.92.10:88/broadwavehigh.mp3 --no-terminal --audio-display=no --audio-channels=stereo --audio-samplerate=48000 --audio-format=s16 --ao=pcm:file=/dev/stdout
217 ```
164218
165219 ### Line-in
220
166221 Audio captured from line-in can be redirected to the snapserver's pipe, e.g. by using:
167222
168223 #### cpiped
224
169225 [cpipe](https://github.com/b-fitzpatrick/cpiped)
170226
171227 #### PulseAudio
228
172229 `parec >/tmp/snapfifo` (defaults to 44.1kHz, 16bit, stereo)
230
231 ### VLC
232
233 Use `--aout afile` and `--audiofile-file` to pipe VLC's audio output to the snapfifo:
234
235 ```sh
236 vlc --no-video --aout afile --audiofile-file /tmp/snapfifo
237 ```
Binary diff not shown
3333 ifndef ARCH
3434 $(error ARCH is not set ("arm" or "aarch64" or "x86"))
3535 endif
36
37 $(eval TOOLCHAIN:=$(NDK_DIR)/toolchains/llvm/prebuilt/linux-x86_64)
38
3639 ifeq ($(ARCH), x86)
3740 $(eval CPPFLAGS:=-DLITTLE_ENDIAN=1234 -DBIG_ENDIAN=4321 -DBYTE_ORDER=LITTLE_ENDIAN)
38 $(eval PROGRAM_PREFIX:=$(NDK_DIR)/bin/i686-linux-android-)
41 $(eval TARGET:=x86_64-linux-android)
42 $(eval API:=21)
3943 else ifeq ($(ARCH), arm)
4044 $(eval CPPFLAGS:=-U_ARM_ASSEM_)
41 $(eval PROGRAM_PREFIX:=$(NDK_DIR)/bin/arm-linux-androideabi-)
45 $(eval TARGET:=armv7a-linux-androideabi)
46 $(eval API:=16)
4247 else ifeq ($(ARCH), aarch64)
4348 $(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-)
49 $(eval TARGET:=aarch64-linux-android)
50 $(eval API:=21)
4551 else
4652 $(error ARCH must be "arm" or "aarch64" or "x86")
4753 endif
48 $(eval CC:=$(PROGRAM_PREFIX)clang)
49 $(eval CXX:=$(PROGRAM_PREFIX)clang++)
50 $(eval CPPFLAGS:=-I$(NDK_DIR)/include $(CPPFLAGS))
54 $(eval CC:=$(TOOLCHAIN)/bin/$(TARGET)$(API)-clang)
55 $(eval CXX:=$(TOOLCHAIN)/bin/$(TARGET)$(API)-clang++)
56 $(eval SYSROOT:=$(TOOLCHAIN)/sysroot/$(TARGET))
57 $(eval CPPFLAGS:=-I$(TOOLCHAIN)/sysroot/usr/include -I$(SYSROOT)/usr/local/include $(CPPFLAGS))
5158
5259 flac: check-env
5360 @cd flac; \
5562 export CXX="$(CXX)"; \
5663 export CPPFLAGS="$(CPPFLAGS)"; \
5764 ./autogen.sh; \
58 ./configure --host=$(ARCH) --disable-ogg --prefix=$(NDK_DIR)/usr/local/; \
65 ./configure --host=$(ARCH) --disable-ogg --prefix=$(SYSROOT)/usr/local/; \
5966 make; \
6067 make install; \
6168 make clean;
6673 export CXX="$(CXX)"; \
6774 export CPPFLAGS="$(CPPFLAGS)"; \
6875 ./autogen.sh; \
69 ./configure --host=$(ARCH) --prefix=$(NDK_DIR)/usr/local/; \
76 ./configure --host=$(ARCH) --prefix=$(SYSROOT)/usr/local/; \
7077 make; \
7178 make install; \
7279 make clean;
7784 export CXX="$(CXX)"; \
7885 export CPPFLAGS="$(CPPFLAGS)"; \
7986 ./autogen.sh; \
80 ./configure --host=$(ARCH) --prefix=$(NDK_DIR)/usr/local/; \
87 ./configure --host=$(ARCH) --prefix=$(SYSROOT)/usr/local/; \
8188 make; \
8289 make install; \
8390 make clean;
8895 export CXX="$(CXX)"; \
8996 export CPPFLAGS="$(CPPFLAGS)"; \
9097 ./autogen.sh; \
91 ./configure --host=$(ARCH) --prefix=$(NDK_DIR)/usr/local/ --with-ogg=$(NDK_DIR)/usr/local/; \
98 ./configure --host=$(ARCH) --prefix=$(SYSROOT)/usr/local/ --with-ogg=$(SYSROOT)/usr/local/; \
9299 make; \
93100 make install; \
94101 make clean; \
123130 cd build; \
124131 cmake ..; \
125132 make; \
126 make DESTDIR=$(NDK_DIR) install; \
133 make DESTDIR=$(SYSROOT) install; \
127134 make clean; \
128135 cd ..; \
129136 rm -rf build;
130137
131138 soxr: check-env
132 @cd /home/johannes/Develop/soxr; \
139 @cd soxr; \
133140 export CC="$(CC)"; \
134141 export CXX="$(CXX)"; \
135142 export CPPFLAGS="$(CPPFLAGS)"; \
137144 cd build; \
138145 cmake -DBUILD_SHARED_LIBS=OFF -DBUILD_TESTS=OFF -DWITH_OPENMP=OFF ..; \
139146 make; \
140 make DESTDIR=$(NDK_DIR) install; \
147 make DESTDIR=$(SYSROOT) install; \
141148 make clean; \
142149 cd ..; \
143150 rm -rf build;
22 control_server.cpp
33 control_session_tcp.cpp
44 control_session_http.cpp
5 control_session_ws.cpp
56 snapserver.cpp
7 server.cpp
68 stream_server.cpp
79 stream_session.cpp
10 stream_session_tcp.cpp
11 stream_session_ws.cpp
812 encoder/encoder_factory.cpp
913 encoder/pcm_encoder.cpp
14 encoder/null_encoder.cpp
1015 streamreader/base64.cpp
1116 streamreader/stream_uri.cpp
1217 streamreader/stream_manager.cpp
1722 streamreader/file_stream.cpp
1823 streamreader/airplay_stream.cpp
1924 streamreader/librespot_stream.cpp
25 streamreader/meta_stream.cpp
2026 streamreader/watchdog.cpp
2127 streamreader/process_stream.cpp)
2228
6773 list(APPEND SERVER_INCLUDE ${OPUS_INCLUDE_DIRS})
6874 endif (OPUS_FOUND)
6975
76 if (ALSA_FOUND)
77 list(APPEND SERVER_SOURCES streamreader/alsa_stream.cpp)
78 list(APPEND SERVER_LIBRARIES ${ALSA_LIBRARIES})
79 list(APPEND SERVER_INCLUDE ${ALSA_INCLUDE_DIRS})
80 endif (ALSA_FOUND)
81
7082 if (EXPAT_FOUND)
7183 list(APPEND SERVER_LIBRARIES ${EXPAT_LIBRARIES})
7284 list(APPEND SERVER_INCLUDE ${EXPAT_INCLUDE_DIRS})
7890 add_executable(snapserver ${SERVER_SOURCES})
7991 target_link_libraries(snapserver ${SERVER_LIBRARIES})
8092
81 install(TARGETS snapserver COMPONENT server DESTINATION "${CMAKE_INSTALL_BINDIR}")
93 install(TARGETS snapserver COMPONENT server DESTINATION ${CMAKE_INSTALL_BINDIR})
94 install(FILES snapserver.1 COMPONENT server DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
95 install(FILES etc/snapserver.conf COMPONENT server DESTINATION ${CMAKE_INSTALL_SYSCONFDIR})
96 install(DIRECTORY etc/snapweb/ DESTINATION ${CMAKE_INSTALL_DATADIR}/snapserver/snapweb)
97 #install(FILES ../debian/snapserver.service DESTINATION ${SYSTEMD_SERVICES_INSTALL_DIR})
1313 # You should have received a copy of the GNU General Public License
1414 # along with this program. If not, see <http://www.gnu.org/licenses/>.
1515
16 VERSION = 0.19.0
16 VERSION = 0.22.0
1717 BIN = snapserver
1818
1919 ifeq ($(TARGET), FREEBSD)
4141 LDFLAGS += -fsanitize=$(SANITIZE)
4242 endif
4343
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
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 -DHAS_SOXR -DVERSION=\"$(VERSION)\" -I. -I.. -I../common
45 LDFLAGS += $(ADD_LDFLAGS) -lvorbis -lvorbisenc -logg -lFLAC -lopus -lsoxr
46 OBJ = snapserver.o server.o config.o control_server.o control_session_tcp.o control_session_http.o control_session_ws.o stream_server.o stream_session.o stream_session_tcp.o stream_session_ws.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/meta_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/null_encoder.o encoder/ogg_encoder.o ../common/sample_format.o ../common/resampler.o
4747
4848 ifneq (,$(TARGET))
4949 CXXFLAGS += -D$(TARGET)
6161
6262 else ifeq ($(TARGET), OPENWRT)
6363
64 CXXFLAGS += -DNO_CPP11_STRING -DHAS_AVAHI -DHAS_DAEMON -pthread
64 CXXFLAGS += -DNO_CPP11_STRING -DHAS_AVAHI -DHAS_DAEMON -DHAS_ALSA -pthread
6565 LDFLAGS += -lavahi-client -lavahi-common -latomic
66 OBJ += ../common/daemon.o publishZeroConf/publish_avahi.o
66 OBJ += ../common/daemon.o publishZeroConf/publish_avahi.o streamreader/alsa_stream.o
6767
6868 else ifeq ($(TARGET), BUILDROOT)
6969
70 CXXFLAGS += -DHAS_AVAHI -DHAS_DAEMON -pthread
70 CXXFLAGS += -DHAS_AVAHI -DHAS_DAEMON -DHAS_ALSA -pthread
7171 LDFLAGS += -lrt -lavahi-client -lavahi-common
72 OBJ += publishZeroConf/publish_avahi.o
72 OBJ += publishZeroConf/publish_avahi.o streamreader/alsa_stream.o
7373
7474 else ifeq ($(TARGET), FREEBSD)
7575
8181 else ifeq ($(TARGET), MACOS)
8282
8383 CXX = g++
84 CXXFLAGS += -DFREEBSD -DHAS_BONJOUR -DHAS_DAEMON -Wno-deprecated -I/usr/local/include
84 CXXFLAGS += -DFREEBSD -DMACOS -DHAS_BONJOUR -DHAS_DAEMON -Wno-deprecated -I/usr/local/include
8585 LDFLAGS += -L/usr/local/lib -framework CoreFoundation -framework IOKit
8686 OBJ += ../common/daemon.o publishZeroConf/publish_bonjour.o
8787
8888 else
8989
9090 CXX = g++
91 CXXFLAGS += -DHAS_AVAHI -DHAS_DAEMON -pthread
92 LDFLAGS += -lrt -lavahi-client -lavahi-common -latomic
93 OBJ += ../common/daemon.o publishZeroConf/publish_avahi.o
91 CXXFLAGS += -DHAS_AVAHI -DHAS_DAEMON -DHAS_ALSA -pthread
92 LDFLAGS += -lrt -lasound -lavahi-client -lavahi-common -latomic
93 OBJ += ../common/daemon.o publishZeroConf/publish_avahi.o streamreader/alsa_stream.o
9494
9595 endif
9696
122122 echo BSD
123123 install -s -g wheel -o root -m 555 $(BIN) $(TARGET_DIR)/local/bin/$(BIN)
124124 install -g wheel -o root -m 555 $(BIN).1 $(TARGET_DIR)/local/man/man1/$(BIN).1
125 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 etc/$(BIN).bsd $(TARGET_DIR)/local/etc/rc.d/$(BIN)
126 install -g wheel -o root etc/$(BIN).conf /etc/$(BIN).conf
127 for file in etc/snapweb/*\.*; do install -g wheel -o root -m 644 $${file} -Dt "/usr/share/snapserver/snapweb/"; done
128 for file in etc/snapweb/3rd-party/*\.*; do install -g wheel -o root -m 644 $${file} -Dt "/usr/share/snapserver/snapweb/3rd-party/"; done
126129
127130 else ifeq ($(TARGET), MACOS)
128131
132135 install -g wheel -o root $(BIN).1 $(TARGET_DIR)/local/share/man/man1/$(BIN).1
133136 install -g wheel -o root etc/$(BIN).plist /Library/LaunchAgents/de.badaix.snapcast.$(BIN).plist
134137 install -g wheel -o root etc/$(BIN).conf /etc/$(BIN).conf
138 for file in etc/snapweb/*\.*; do install -g wheel -o root -m 644 $${file} -Dt "/usr/share/snapserver/snapweb/"; done
139 for file in etc/snapweb/3rd-party/*\.*; do install -g wheel -o root -m 644 $${file} -Dt "/usr/share/snapserver/snapweb/3rd-party/"; done
135140 launchctl load /Library/LaunchAgents/de.badaix.snapcast.$(BIN).plist
136141
137142 else
156161 installfiles:
157162 install -s -D -g root -o root $(BIN) $(TARGET_DIR)/bin/$(BIN)
158163 install -D -g root -o root $(BIN).1 $(TARGET_DIR)/share/man/man1/$(BIN).1
164 install -g root -o root etc/$(BIN).conf /etc/$(BIN).conf;
165 for file in etc/snapweb/*\.*; do install -g root -o root -m 644 $${file} -Dt "/usr/share/snapserver/snapweb/"; done
166 for file in etc/snapweb/3rd-party/*\.*; do install -g root -o root -m 644 $${file} -Dt "/usr/share/snapserver/snapweb/3rd-party/"; done
159167
160168 installsystemd:
161169 @echo using systemd; \
162170 cp ../debian/$(BIN).service /lib/systemd/system/$(BIN).service; \
163171 cp -n ../debian/$(BIN).default /etc/default/$(BIN); \
164 cp -n etc/$(BIN).conf /etc/$(BIN).conf; \
165172 systemctl daemon-reload; \
166173 systemctl enable $(BIN); \
167174 systemctl start $(BIN); \
170177 @echo using sysv; \
171178 cp ../debian/$(BIN).init /etc/init.d/$(BIN); \
172179 cp -n ../debian/$(BIN).default /etc/default/$(BIN); \
173 cp -n etc/$(BIN).conf /etc/$(BIN).conf; \
174180 update-rc.d $(BIN) defaults; \
175181 /etc/init.d/$(BIN) start; \
176182
177 installbsd:
178 @echo using bsd; \
179 cp ../debian/$(BIN).bsd /usr/local/etc/rc.d/$(BIN); \
180
181183 adduser:
182 @if ! getent passwd snapserver >/dev/null; then \
183 useradd --user-group --system --home-dir /var/lib/snapserver snapserver; \
184 fi; \
185
184 sh ../debian/snapserver.postinst configure
186185
187186 ifeq ($(TARGET), FREEBSD)
188187
193192 rm -f $(TARGET_DIR)/local/man/man1/$(BIN).1; \
194193 rm -f $(TARGET_DIR)/local/etc/rc.d/$(BIN); \
195194 rm -f /etc/$(BIN).conf; \
196 rm -f /var/lib/snapserver; \
195 rm -rf /var/lib/snapserver; \
196 rm -rf /usr/share/snapserver; \
197197
198198 else ifeq ($(TARGET), MACOS)
199199
204204 rm -f $(TARGET_DIR)/local/share/man/man1/$(BIN).1; \
205205 rm -f /Library/LaunchAgents/de.badaix.snapcast.$(BIN).plist; \
206206 rm -f /etc/$(BIN).conf; \
207 rm -f /var/lib/snapserver; \
207 rm -rf /var/lib/snapserver; \
208 rm -rf /usr/share/snapserver; \
208209
209210 else
210211
220221 echo cannot tell; \
221222 fi; \
222223 rm -f /etc/$(BIN).conf; \
223 rm -f /var/run/$(BIN); \
224 rm -f /var/lib/snapserver; \
224 rm -rf /var/run/$(BIN); \
225 rm -rf /var/lib/snapserver; \
226 rm -rf /usr/share/snapserver; \
225227 $(MAKE) deluser
226228
227229 endif
244246 systemctl daemon-reload; \
245247
246248 deluser:
247 @userdel --force snapserver > /dev/null || true; \
248 groupdel snapserver > /dev/null || true; \
249
249 sh ../debian/snapserver.postrm purge
250
6161 throw SnapException("failed to create settings directory: \"" + dir + "\": " + cpt::to_string(errno));
6262
6363 filename_ = dir + "server.json";
64 SLOG(NOTICE) << "Settings file: \"" << filename_ << "\"\n";
64 LOG(NOTICE) << "Settings file: \"" << filename_ << "\"\n";
6565
6666 int fd;
6767 if ((fd = open(filename_.c_str(), O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1)
8282 }
8383 catch (const std::exception& e)
8484 {
85 SLOG(ERROR) << "Exception in chown: " << e.what() << "\n";
85 LOG(ERROR) << "Exception in chown: " << e.what() << "\n";
8686 }
8787 }
8888
8989 try
9090 {
9191 ifstream ifs(filename_, std::ifstream::in);
92 if (ifs.good())
92 if (ifs.good() && (ifs.peek() != std::ifstream::traits_type::eof()))
9393 {
9494 json j;
9595 ifs >> j;
100100 {
101101 GroupPtr group = make_shared<Group>();
102102 group->fromJson(jGroup);
103 // if (client->id.empty() || getClientInfo(client->id))
104 // continue;
103 // if (client->id.empty() || getClientInfo(client->id))
104 // continue;
105105 groups.push_back(group);
106106 }
107107 }
3434 struct ClientInfo;
3535 struct Group;
3636
37 typedef std::shared_ptr<ClientInfo> ClientInfoPtr;
38 typedef std::shared_ptr<Group> GroupPtr;
37 using ClientInfoPtr = std::shared_ptr<ClientInfo>;
38 using GroupPtr = std::shared_ptr<Group>;
3939
4040
4141 template <typename T>
3030 using namespace std;
3131 using json = nlohmann::json;
3232
33
34 ControlServer::ControlServer(boost::asio::io_context& io_context, const ServerSettings::TcpSettings& tcp_settings,
35 const ServerSettings::HttpSettings& http_settings, ControlMessageReceiver* controlMessageReceiver)
33 static constexpr auto LOG_TAG = "ControlServer";
34
35
36 ControlServer::ControlServer(boost::asio::io_context& io_context, const ServerSettings::Tcp& tcp_settings, const ServerSettings::Http& http_settings,
37 ControlMessageReceiver* controlMessageReceiver)
3638 : io_context_(io_context), tcp_settings_(tcp_settings), http_settings_(http_settings), controlMessageReceiver_(controlMessageReceiver)
3739 {
3840 }
5052 auto count = distance(new_end, sessions_.end());
5153 if (count > 0)
5254 {
53 SLOG(ERROR) << "Removing " << count << " inactive session(s), active sessions: " << sessions_.size() - count << "\n";
55 LOG(ERROR, LOG_TAG) << "Removing " << count << " inactive session(s), active sessions: " << sessions_.size() - count << "\n";
5456 sessions_.erase(new_end, sessions_.end());
5557 }
5658 }
7173 }
7274
7375
74 std::string ControlServer::onMessageReceived(ControlSession* connection, const std::string& message)
75 {
76 // LOG(DEBUG) << "received: \"" << message << "\"\n";
76 std::string ControlServer::onMessageReceived(ControlSession* session, const std::string& message)
77 {
78 // LOG(DEBUG, LOG_TAG) << "received: \"" << message << "\"\n";
7779 if (controlMessageReceiver_ != nullptr)
78 return controlMessageReceiver_->onMessageReceived(connection, message);
80 return controlMessageReceiver_->onMessageReceived(session, message);
7981 return "";
82 }
83
84
85 void ControlServer::onNewSession(const shared_ptr<ControlSession>& session)
86 {
87 std::lock_guard<std::recursive_mutex> mlock(session_mutex_);
88 session->start();
89 sessions_.emplace_back(session);
90 cleanup();
91 }
92
93
94 void ControlServer::onNewSession(const std::shared_ptr<StreamSession>& session)
95 {
96 if (controlMessageReceiver_ != nullptr)
97 controlMessageReceiver_->onNewSession(session);
8098 }
8199
82100
86104 if (!ec)
87105 handleAccept<ControlSessionTcp>(std::move(socket));
88106 else
89 LOG(ERROR) << "Error while accepting socket connection: " << ec.message() << "\n";
107 LOG(ERROR, LOG_TAG) << "Error while accepting socket connection: " << ec.message() << "\n";
90108 };
91109
92110 auto accept_handler_http = [this](error_code ec, tcp::socket socket) {
93111 if (!ec)
94112 handleAccept<ControlSessionHttp>(std::move(socket), http_settings_);
95113 else
96 LOG(ERROR) << "Error while accepting socket connection: " << ec.message() << "\n";
114 LOG(ERROR, LOG_TAG) << "Error while accepting socket connection: " << ec.message() << "\n";
97115 };
98116
99117 for (auto& acceptor : acceptor_tcp_)
115133 setsockopt(socket.native_handle(), SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
116134 setsockopt(socket.native_handle(), SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
117135 // socket->set_option(boost::asio::ip::tcp::no_delay(false));
118 SLOG(NOTICE) << "ControlServer::NewConnection: " << socket.remote_endpoint().address().to_string() << endl;
136 LOG(NOTICE, LOG_TAG) << "ControlServer::NewConnection: " << socket.remote_endpoint().address().to_string() << endl;
119137 shared_ptr<SessionType> session = make_shared<SessionType>(this, io_context_, std::move(socket), std::forward<Args>(args)...);
120 {
121 std::lock_guard<std::recursive_mutex> mlock(session_mutex_);
122 session->start();
123 sessions_.emplace_back(session);
124 cleanup();
125 }
138 onNewSession(session);
126139 }
127140 catch (const std::exception& e)
128141 {
129 SLOG(ERROR) << "Exception in ControlServer::handleAccept: " << e.what() << endl;
142 LOG(ERROR, LOG_TAG) << "Exception in ControlServer::handleAccept: " << e.what() << endl;
130143 }
131144 startAccept();
132145 }
141154 {
142155 try
143156 {
144 LOG(INFO) << "Creating TCP acceptor for address: " << address << ", port: " << tcp_settings_.port << "\n";
157 LOG(INFO, LOG_TAG) << "Creating TCP acceptor for address: " << address << ", port: " << tcp_settings_.port << "\n";
145158 acceptor_tcp_.emplace_back(
146159 make_unique<tcp::acceptor>(io_context_, tcp::endpoint(boost::asio::ip::address::from_string(address), tcp_settings_.port)));
147160 }
148161 catch (const boost::system::system_error& e)
149162 {
150 LOG(ERROR) << "error creating TCP acceptor: " << e.what() << ", code: " << e.code() << "\n";
163 LOG(ERROR, LOG_TAG) << "error creating TCP acceptor: " << e.what() << ", code: " << e.code() << "\n";
151164 }
152165 }
153166 }
157170 {
158171 try
159172 {
160 LOG(INFO) << "Creating HTTP acceptor for address: " << address << ", port: " << http_settings_.port << "\n";
173 LOG(INFO, LOG_TAG) << "Creating HTTP acceptor for address: " << address << ", port: " << http_settings_.port << "\n";
161174 acceptor_http_.emplace_back(
162175 make_unique<tcp::acceptor>(io_context_, tcp::endpoint(boost::asio::ip::address::from_string(address), http_settings_.port)));
163176 }
164177 catch (const boost::system::system_error& e)
165178 {
166 LOG(ERROR) << "error creating HTTP acceptor: " << e.what() << ", code: " << e.code() << "\n";
179 LOG(ERROR, LOG_TAG) << "error creating HTTP acceptor: " << e.what() << ", code: " << e.code() << "\n";
167180 }
168181 }
169182 }
4242 class ControlServer : public ControlMessageReceiver
4343 {
4444 public:
45 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::Tcp& tcp_settings, const ServerSettings::Http& http_settings,
4646 ControlMessageReceiver* controlMessageReceiver = nullptr);
4747 virtual ~ControlServer();
4848
4949 void start();
5050 void stop();
5151
52 /// Send a message to all connceted clients
52 /// Send a message to all connected clients
5353 void send(const std::string& message, const ControlSession* excludeSession = nullptr);
54
55 /// Clients call this when they receive a message. Implementation of MessageReceiver::onMessageReceived
56 std::string onMessageReceived(ControlSession* connection, const std::string& message) override;
5754
5855 private:
5956 void startAccept();
6259 void handleAccept(tcp::socket socket, Args&&... args);
6360 void cleanup();
6461
62 /// Implementation of ControlMessageReceiver
63 std::string onMessageReceived(ControlSession* session, const std::string& message) override;
64 void onNewSession(const std::shared_ptr<ControlSession>& session) override;
65 void onNewSession(const std::shared_ptr<StreamSession>& session) override;
66
6567 mutable std::recursive_mutex session_mutex_;
6668 std::vector<std::weak_ptr<ControlSession>> sessions_;
6769
6971 std::vector<acceptor_ptr> acceptor_http_;
7072
7173 boost::asio::io_context& io_context_;
72 ServerSettings::TcpSettings tcp_settings_;
73 ServerSettings::HttpSettings http_settings_;
74 ServerSettings::Tcp tcp_settings_;
75 ServerSettings::Http http_settings_;
7476 ControlMessageReceiver* controlMessageReceiver_;
7577 };
7678
3232
3333
3434 class ControlSession;
35
35 class StreamSession;
3636
3737 /// Interface: callback for a received message.
3838 class ControlMessageReceiver
3939 {
4040 public:
4141 // TODO: rename, error handling
42 virtual std::string onMessageReceived(ControlSession* connection, const std::string& message) = 0;
42 virtual std::string onMessageReceived(ControlSession* session, const std::string& message) = 0;
43 virtual void onNewSession(const std::shared_ptr<ControlSession>& session) = 0;
44 virtual void onNewSession(const std::shared_ptr<StreamSession>& session) = 0;
4345 };
4446
4547
4951 * Messages are sent to the client with the "send" method.
5052 * Received messages from the client are passed to the ControlMessageReceiver callback
5153 */
52 class ControlSession
54 class ControlSession : public std::enable_shared_from_this<ControlSession>
5355 {
5456 public:
55 /// ctor. Received message from the client are passed to MessageReceiver
57 /// ctor. Received message from the client are passed to ControlMessageReceiver
5658 ControlSession(ControlMessageReceiver* receiver) : message_receiver_(receiver)
5759 {
5860 }
5961 virtual ~ControlSession() = default;
6062 virtual void start() = 0;
6163 virtual void stop() = 0;
62
63 /// Sends a message to the client (synchronous)
64 virtual bool send(const std::string& message) = 0;
6564
6665 /// Sends a message to the client (asynchronous)
6766 virtual void sendAsync(const std::string& message) = 0;
1717
1818 #include "control_session_http.hpp"
1919 #include "common/aixlog.hpp"
20 #include "control_session_ws.hpp"
2021 #include "message/pcm_chunk.hpp"
22 #include "stream_session_ws.hpp"
2123 #include <boost/beast/http/file_body.hpp>
2224 #include <iostream>
2325
2426 using namespace std;
27
28 static constexpr auto LOG_TAG = "ControlSessionHTTP";
29
2530
2631 static constexpr const char* HTTP_SERVER_NAME = "Snapcast";
2732
98103 } // namespace
99104
100105 ControlSessionHttp::ControlSessionHttp(ControlMessageReceiver* receiver, boost::asio::io_context& ioc, tcp::socket&& socket,
101 const ServerSettings::HttpSettings& settings)
106 const ServerSettings::Http& settings)
102107 : ControlSession(receiver), socket_(std::move(socket)), settings_(settings), strand_(ioc)
103108 {
104 LOG(DEBUG) << "ControlSessionHttp\n";
109 LOG(DEBUG, LOG_TAG) << "ControlSessionHttp\n";
105110 }
106111
107112
108113 ControlSessionHttp::~ControlSessionHttp()
109114 {
110 LOG(DEBUG) << "ControlSessionHttp::~ControlSessionHttp()\n";
115 LOG(DEBUG, LOG_TAG) << "ControlSessionHttp::~ControlSessionHttp()\n";
111116 stop();
112117 }
113118
192197 if (req.target().back() == '/')
193198 path.append("index.html");
194199
195 LOG(DEBUG) << "path: " << path << "\n";
200 LOG(DEBUG, LOG_TAG) << "path: " << path << "\n";
196201 // Attempt to open the file
197202 beast::error_code ec;
198203 http::file_body::value_type body;
241246 // Handle the error, if any
242247 if (ec)
243248 {
244 LOG(ERROR) << "ControlSessionHttp::on_read error: " << ec.message() << "\n";
245 return;
246 }
247
248 LOG(DEBUG) << "read: " << bytes_transferred << ", method: " << req_.method_string() << ", content type: " << req_[beast::http::field::content_type]
249 << ", target: " << req_.target() << ", body: " << req_.body() << "\n";
249 LOG(ERROR, LOG_TAG) << "ControlSessionHttp::on_read error: " << ec.message() << "\n";
250 return;
251 }
252
253 LOG(DEBUG, LOG_TAG) << "read: " << bytes_transferred << ", method: " << req_.method_string() << ", content type: " << req_[beast::http::field::content_type]
254 << ", target: " << req_.target() << ", body: " << req_.body() << "\n";
250255
251256 // See if it is a WebSocket Upgrade
252 if (websocket::is_upgrade(req_) && (req_.target() == "/jsonrpc"))
253 {
254 // Create a WebSocket session by transferring the socket
255 // std::make_shared<websocket_session>(std::move(socket_), state_)->run(std::move(req_));
256 ws_ = make_unique<websocket::stream<beast::tcp_stream>>(std::move(socket_));
257 ws_->async_accept(req_, [ this, self = shared_from_this() ](beast::error_code ec) { on_accept_ws(ec); });
258 LOG(DEBUG) << "websocket upgrade\n";
257 if (websocket::is_upgrade(req_))
258 {
259 LOG(DEBUG, LOG_TAG) << "websocket upgrade, target: " << req_.target() << "\n";
260 if (req_.target() == "/jsonrpc")
261 {
262 // Create a WebSocket session by transferring the socket
263 // std::make_shared<websocket_session>(std::move(socket_), state_)->run(std::move(req_));
264 auto ws = std::make_shared<websocket::stream<beast::tcp_stream>>(std::move(socket_));
265 ws->async_accept(req_, [ this, ws, self = shared_from_this() ](beast::error_code ec) {
266 if (ec)
267 {
268 LOG(ERROR, LOG_TAG) << "Error during WebSocket handshake (control): " << ec.message() << "\n";
269 }
270 else
271 {
272 auto ws_session = make_shared<ControlSessionWebsocket>(message_receiver_, strand_.context(), std::move(*ws));
273 message_receiver_->onNewSession(ws_session);
274 }
275 });
276 }
277 else if (req_.target() == "/stream")
278 {
279 // Create a WebSocket session by transferring the socket
280 // std::make_shared<websocket_session>(std::move(socket_), state_)->run(std::move(req_));
281 auto ws = std::make_shared<websocket::stream<beast::tcp_stream>>(std::move(socket_));
282 ws->async_accept(req_, [ this, ws, self = shared_from_this() ](beast::error_code ec) {
283 if (ec)
284 {
285 LOG(ERROR, LOG_TAG) << "Error during WebSocket handshake (stream): " << ec.message() << "\n";
286 }
287 else
288 {
289 auto ws_session = make_shared<StreamSessionWebsocket>(strand_.context(), nullptr, std::move(*ws));
290 message_receiver_->onNewSession(ws_session);
291 }
292 });
293 }
259294 return;
260295 }
261296
281316 // Handle the error, if any
282317 if (ec)
283318 {
284 LOG(ERROR) << "ControlSessionHttp::on_write, error: " << ec.message() << "\n";
319 LOG(ERROR, LOG_TAG) << "ControlSessionHttp::on_write, error: " << ec.message() << "\n";
285320 return;
286321 }
287322
307342 {
308343 }
309344
310 void ControlSessionHttp::sendAsync(const std::string& message)
311 {
312 if (!ws_)
313 return;
314
315 strand_.post([ this, self = shared_from_this(), message ]() {
316 messages_.emplace_back(message);
317 if (messages_.size() > 1)
318 {
319 LOG(DEBUG) << "HTTP session outstanding async_writes: " << messages_.size() << "\n";
320 return;
321 }
322 send_next();
323 });
324 }
325
326 void ControlSessionHttp::send_next()
327 {
328 if (!ws_)
329 return;
330
331 auto message = messages_.front();
332 ws_->async_write(boost::asio::buffer(message),
333 boost::asio::bind_executor(strand_, [ this, self = shared_from_this() ](std::error_code ec, std::size_t length) {
334 messages_.pop_front();
335 if (ec)
336 {
337 LOG(ERROR) << "Error while writing to web socket: " << ec.message() << "\n";
338 }
339 else
340 {
341 LOG(DEBUG) << "Wrote " << length << " bytes to web socket\n";
342 }
343 if (!messages_.empty())
344 send_next();
345 }));
346 }
347
348
349 bool ControlSessionHttp::send(const std::string& message)
350 {
351 if (!ws_)
352 return false;
353
354 boost::system::error_code ec;
355 ws_->write(boost::asio::buffer(message), ec);
356 return !ec;
357 }
358
359 void ControlSessionHttp::on_accept_ws(beast::error_code ec)
360 {
361 if (ec)
362 {
363 LOG(ERROR) << "ControlSessionWs::on_accept, error: " << ec.message() << "\n";
364 return;
365 }
366
367 // Read a message
368 do_read_ws();
369 }
370
371 void ControlSessionHttp::do_read_ws()
372 {
373 // Read a message into our buffer
374 ws_->async_read(buffer_, boost::asio::bind_executor(strand_, [ this, self = shared_from_this() ](beast::error_code ec, std::size_t bytes_transferred) {
375 on_read_ws(ec, bytes_transferred);
376 }));
377 }
378
379
380 void ControlSessionHttp::on_read_ws(beast::error_code ec, std::size_t bytes_transferred)
381 {
382 boost::ignore_unused(bytes_transferred);
383
384 // This indicates that the session was closed
385 if (ec == websocket::error::closed)
386 return;
387
388 if (ec)
389 {
390 LOG(ERROR) << "ControlSessionHttp::on_read_ws error: " << ec.message() << "\n";
391 return;
392 }
393
394 std::string line{boost::beast::buffers_to_string(buffer_.data())};
395 if (!line.empty())
396 {
397 // LOG(DEBUG) << "received: " << line << "\n";
398 if ((message_receiver_ != nullptr) && !line.empty())
399 {
400 string response = message_receiver_->onMessageReceived(this, line);
401 if (!response.empty())
402 {
403 sendAsync(response);
404 }
405 }
406 }
407 buffer_.consume(bytes_transferred);
408 do_read_ws();
409 }
345
346 void ControlSessionHttp::sendAsync(const std::string& /*message*/)
347 {
348 }
3535 * Messages are sent to the client with the "send" method.
3636 * Received messages from the client are passed to the ControlMessageReceiver callback
3737 */
38 class ControlSessionHttp : public ControlSession, public std::enable_shared_from_this<ControlSession>
38 class ControlSessionHttp : public ControlSession
3939 {
4040 public:
41 /// ctor. Received message from the client are passed to MessageReceiver
42 ControlSessionHttp(ControlMessageReceiver* receiver, boost::asio::io_context& ioc, tcp::socket&& socket, const ServerSettings::HttpSettings& settings);
41 /// ctor. Received message from the client are passed to ControlMessageReceiver
42 ControlSessionHttp(ControlMessageReceiver* receiver, boost::asio::io_context& ioc, tcp::socket&& socket, const ServerSettings::Http& settings);
4343 ~ControlSessionHttp() override;
4444 void start() override;
4545 void stop() override;
46
47 /// Sends a message to the client (synchronous)
48 bool send(const std::string& message) override;
4946
5047 /// Sends a message to the client (asynchronous)
5148 void sendAsync(const std::string& message) override;
5855 template <class Body, class Allocator, class Send>
5956 void handle_request(http::request<Body, http::basic_fields<Allocator>>&& req, Send&& send);
6057
61 void send_next();
62
6358 http::request<http::string_body> req_;
64
65 protected:
66 // Websocket methods
67 void on_accept_ws(beast::error_code ec);
68 void on_read_ws(beast::error_code ec, std::size_t bytes_transferred);
69 void do_read_ws();
70
71 std::unique_ptr<websocket::stream<beast::tcp_stream>> ws_;
7259
7360 protected:
7461 tcp::socket socket_;
7562 beast::flat_buffer buffer_;
76 ServerSettings::HttpSettings settings_;
63 ServerSettings::Http settings_;
7764 boost::asio::io_context::strand strand_;
7865 std::deque<std::string> messages_;
7966 };
2121
2222 using namespace std;
2323
24 static constexpr auto LOG_TAG = "ControlSessionTCP";
25
2426 // https://stackoverflow.com/questions/7754695/boost-asio-async-write-how-to-not-interleaving-async-write-calls/7756894
2527
2628
3234
3335 ControlSessionTcp::~ControlSessionTcp()
3436 {
35 LOG(DEBUG) << "ControlSessionTcp::~ControlSessionTcp()\n";
37 LOG(DEBUG, LOG_TAG) << "ControlSessionTcp::~ControlSessionTcp()\n";
3638 stop();
3739 }
3840
4547 boost::asio::bind_executor(strand_, [ this, self = shared_from_this(), delimiter ](const std::error_code& ec, std::size_t bytes_transferred) {
4648 if (ec)
4749 {
48 LOG(ERROR) << "Error while reading from control socket: " << ec.message() << "\n";
50 LOG(ERROR, LOG_TAG) << "Error while reading from control socket: " << ec.message() << "\n";
4951 return;
5052 }
5153
5557 {
5658 if (line.back() == '\r')
5759 line.resize(line.size() - 1);
58 // LOG(DEBUG) << "received: " << line << "\n";
60 // LOG(DEBUG, LOG_TAG) << "received: " << line << "\n";
5961 if ((message_receiver_ != nullptr) && !line.empty())
6062 {
6163 string response = message_receiver_->onMessageReceived(this, line);
7779
7880 void ControlSessionTcp::stop()
7981 {
80 LOG(DEBUG) << "ControlSession::stop\n";
82 LOG(DEBUG, LOG_TAG) << "ControlSession::stop\n";
8183 boost::system::error_code ec;
8284 socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
8385 if (ec)
84 LOG(ERROR) << "Error in socket shutdown: " << ec.message() << "\n";
86 LOG(ERROR, LOG_TAG) << "Error in socket shutdown: " << ec.message() << "\n";
8587 socket_.close(ec);
8688 if (ec)
87 LOG(ERROR) << "Error in socket close: " << ec.message() << "\n";
88 LOG(DEBUG) << "ControlSession ControlSession stopped\n";
89 LOG(ERROR, LOG_TAG) << "Error in socket close: " << ec.message() << "\n";
90 LOG(DEBUG, LOG_TAG) << "ControlSession ControlSession stopped\n";
8991 }
9092
9193
9597 messages_.emplace_back(message + "\r\n");
9698 if (messages_.size() > 1)
9799 {
98 LOG(DEBUG) << "TCP session outstanding async_writes: " << messages_.size() << "\n";
100 LOG(DEBUG, LOG_TAG) << "TCP session outstanding async_writes: " << messages_.size() << "\n";
99101 return;
100102 }
101103 send_next();
109111 messages_.pop_front();
110112 if (ec)
111113 {
112 LOG(ERROR) << "Error while writing to control socket: " << ec.message() << "\n";
114 LOG(ERROR, LOG_TAG) << "Error while writing to control socket: " << ec.message() << "\n";
113115 }
114116 else
115117 {
116 LOG(DEBUG) << "Wrote " << length << " bytes to control socket\n";
118 LOG(DEBUG, LOG_TAG) << "Wrote " << length << " bytes to control socket\n";
117119 }
118120 if (!messages_.empty())
119121 send_next();
120122 }));
121123 }
122
123 bool ControlSessionTcp::send(const std::string& message)
124 {
125 boost::system::error_code ec;
126 boost::asio::write(socket_, boost::asio::buffer(message + "\r\n"), ec);
127 return !ec;
128 }
2727 * Messages are sent to the client with the "send" method.
2828 * Received messages from the client are passed to the ControlMessageReceiver callback
2929 */
30 class ControlSessionTcp : public ControlSession, public std::enable_shared_from_this<ControlSession>
30 class ControlSessionTcp : public ControlSession
3131 {
3232 public:
33 /// ctor. Received message from the client are passed to MessageReceiver
33 /// ctor. Received message from the client are passed to ControlMessageReceiver
3434 ControlSessionTcp(ControlMessageReceiver* receiver, boost::asio::io_context& ioc, tcp::socket&& socket);
3535 ~ControlSessionTcp() override;
3636 void start() override;
3737 void stop() override;
38
39 /// Sends a message to the client (synchronous)
40 bool send(const std::string& message) override;
4138
4239 /// Sends a message to the client (asynchronous)
4340 void sendAsync(const std::string& message) override;
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 "control_session_ws.hpp"
19 #include "common/aixlog.hpp"
20 #include "message/pcm_chunk.hpp"
21 #include <iostream>
22
23 using namespace std;
24
25 static constexpr auto LOG_TAG = "ControlSessionWS";
26
27
28 ControlSessionWebsocket::ControlSessionWebsocket(ControlMessageReceiver* receiver, boost::asio::io_context& ioc, websocket::stream<beast::tcp_stream>&& socket)
29 : ControlSession(receiver), ws_(std::move(socket)), strand_(ioc)
30 {
31 LOG(DEBUG, LOG_TAG) << "ControlSessionWebsocket\n";
32 }
33
34
35 ControlSessionWebsocket::~ControlSessionWebsocket()
36 {
37 LOG(DEBUG, LOG_TAG) << "ControlSessionWebsocket::~ControlSessionWebsocket()\n";
38 stop();
39 }
40
41
42 void ControlSessionWebsocket::start()
43 {
44 // Read a message
45 do_read_ws();
46 }
47
48
49 void ControlSessionWebsocket::stop()
50 {
51 // if (ws_.is_open())
52 // {
53 // boost::beast::error_code ec;
54 // ws_.close(beast::websocket::close_code::normal, ec);
55 // if (ec)
56 // LOG(ERROR, LOG_TAG) << "Error in socket close: " << ec.message() << "\n";
57 // }
58 }
59
60
61 void ControlSessionWebsocket::sendAsync(const std::string& message)
62 {
63 strand_.post([ this, self = shared_from_this(), msg = message ]() {
64 messages_.push_back(std::move(msg));
65 if (messages_.size() > 1)
66 {
67 LOG(DEBUG, LOG_TAG) << "HTTP session outstanding async_writes: " << messages_.size() << "\n";
68 return;
69 }
70 send_next();
71 });
72 }
73
74
75 void ControlSessionWebsocket::send_next()
76 {
77 const std::string& message = messages_.front();
78 ws_.async_write(boost::asio::buffer(message),
79 boost::asio::bind_executor(strand_, [ this, self = shared_from_this() ](std::error_code ec, std::size_t length) {
80 messages_.pop_front();
81 if (ec)
82 {
83 LOG(ERROR, LOG_TAG) << "Error while writing to web socket: " << ec.message() << "\n";
84 }
85 else
86 {
87 LOG(DEBUG, LOG_TAG) << "Wrote " << length << " bytes to web socket\n";
88 }
89 if (!messages_.empty())
90 send_next();
91 }));
92 }
93
94
95 void ControlSessionWebsocket::do_read_ws()
96 {
97 // Read a message into our buffer
98 ws_.async_read(buffer_, boost::asio::bind_executor(strand_, [ this, self = shared_from_this() ](beast::error_code ec, std::size_t bytes_transferred) {
99 on_read_ws(ec, bytes_transferred);
100 }));
101 }
102
103
104 void ControlSessionWebsocket::on_read_ws(beast::error_code ec, std::size_t bytes_transferred)
105 {
106 boost::ignore_unused(bytes_transferred);
107
108 // This indicates that the session was closed
109 if (ec == websocket::error::closed)
110 return;
111
112 if (ec)
113 {
114 LOG(ERROR, LOG_TAG) << "ControlSessionWebsocket::on_read_ws error: " << ec.message() << "\n";
115 return;
116 }
117
118 std::string line{boost::beast::buffers_to_string(buffer_.data())};
119 if (!line.empty())
120 {
121 // LOG(DEBUG, LOG_TAG) << "received: " << line << "\n";
122 if ((message_receiver_ != nullptr) && !line.empty())
123 {
124 string response = message_receiver_->onMessageReceived(this, line);
125 if (!response.empty())
126 {
127 sendAsync(response);
128 }
129 }
130 }
131 buffer_.consume(bytes_transferred);
132 do_read_ws();
133 }
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 CONTROL_SESSION_WS_HPP
19 #define CONTROL_SESSION_WS_HPP
20
21 #include "control_session.hpp"
22 #include <boost/beast/core.hpp>
23 #include <boost/beast/websocket.hpp>
24 #include <deque>
25
26 namespace beast = boost::beast; // from <boost/beast.hpp>
27 namespace http = beast::http; // from <boost/beast/http.hpp>
28 namespace websocket = beast::websocket; // from <boost/beast/websocket.hpp>
29 namespace net = boost::asio; // from <boost/asio.hpp>
30
31
32 /// Endpoint for a connected control client.
33 /**
34 * Endpoint for a connected control client.
35 * Messages are sent to the client with the "send" method.
36 * Received messages from the client are passed to the ControlMessageReceiver callback
37 */
38 class ControlSessionWebsocket : public ControlSession
39 {
40 public:
41 /// ctor. Received message from the client are passed to ControlMessageReceiver
42 ControlSessionWebsocket(ControlMessageReceiver* receiver, boost::asio::io_context& ioc, websocket::stream<beast::tcp_stream>&& socket);
43 ~ControlSessionWebsocket() override;
44 void start() override;
45 void stop() override;
46
47 /// Sends a message to the client (asynchronous)
48 void sendAsync(const std::string& message) override;
49
50 protected:
51 // Websocket methods
52 void on_read_ws(beast::error_code ec, std::size_t bytes_transferred);
53 void do_read_ws();
54 void send_next();
55
56 websocket::stream<beast::tcp_stream> ws_;
57
58 protected:
59 beast::flat_buffer buffer_;
60 boost::asio::io_context::strand strand_;
61 std::deque<std::string> messages_;
62 };
63
64
65
66 #endif
1515 along with this program. If not, see <http://www.gnu.org/licenses/>.
1616 ***/
1717
18 #ifndef ENCODER_H
19 #define ENCODER_H
18 #ifndef ENCODER_HPP
19 #define ENCODER_HPP
2020
21 #include <functional>
2122 #include <memory>
2223 #include <string>
2324
2829 namespace encoder
2930 {
3031
31 class Encoder;
32
33 /// Callback interface for users of Encoder
34 /**
35 * Users of Encoder should implement this to get the encoded PCM data
36 */
37 class EncoderListener
38 {
39 public:
40 virtual void onChunkEncoded(const Encoder* encoder, std::shared_ptr<msg::PcmChunk> chunk, double duration) = 0;
41 };
42
43
44
4532 /// Abstract Encoder class
4633 /**
4734 * Stream encoder. PCM chunks are fed into the encoder.
5037 class Encoder
5138 {
5239 public:
40 using OnEncodedCallback = std::function<void(const Encoder&, std::shared_ptr<msg::PcmChunk>, double)>;
41
5342 /// ctor. Codec options (E.g. compression level) are passed as string and are codec dependend
5443 Encoder(const std::string& codecOptions = "") : headerChunk_(nullptr), codecOptions_(codecOptions)
5544 {
5847 virtual ~Encoder() = default;
5948
6049 /// The listener will receive the encoded stream
61 virtual void init(EncoderListener* listener, const SampleFormat& format)
50 virtual void init(OnEncodedCallback callback, const SampleFormat& format)
6251 {
6352 if (codecOptions_ == "")
6453 codecOptions_ = getDefaultOptions();
65 listener_ = listener;
54 encoded_callback_ = callback;
6655 sampleFormat_ = format;
6756 initEncoder();
6857 }
6958
7059 /// Here the work is done. Encoded data is passed to the EncoderListener.
71 virtual void encode(const msg::PcmChunk* chunk) = 0;
60 virtual void encode(const msg::PcmChunk& chunk) = 0;
7261
7362 virtual std::string name() const = 0;
7463
9382
9483 SampleFormat sampleFormat_;
9584 std::shared_ptr<msg::CodecHeader> headerChunk_;
96 EncoderListener* listener_;
9785 std::string codecOptions_;
86 OnEncodedCallback encoded_callback_;
9887 };
9988
10089 } // namespace encoder
1616 ***/
1717
1818 #include "encoder_factory.hpp"
19 #include "null_encoder.hpp"
1920 #include "pcm_encoder.hpp"
2021 #if defined(HAS_OGG) && defined(HAS_VORBIS) && defined(HAS_VORBIS_ENC)
2122 #include "ogg_encoder.hpp"
4748 }
4849 if (codec == "pcm")
4950 return std::make_unique<PcmEncoder>(codecOptions);
51 else if (codec == "null")
52 return std::make_unique<NullEncoder>(codecOptions);
5053 #if defined(HAS_OGG) && defined(HAS_VORBIS) && defined(HAS_VORBIS_ENC)
5154 else if (codec == "ogg")
5255 return std::make_unique<OggEncoder>(codecOptions);
2727 namespace encoder
2828 {
2929
30 // static constexpr auto LOG_TAG = "FlacEnc";
31
3032 FlacEncoder::FlacEncoder(const std::string& codecOptions) : Encoder(codecOptions), encoder_(nullptr), pcmBufferSize_(0), encodedSamples_(0), flacChunk_(nullptr)
3133 {
3234 headerChunk_.reset(new msg::CodecHeader("flac"));
6668 }
6769
6870
69 void FlacEncoder::encode(const msg::PcmChunk* chunk)
71 void FlacEncoder::encode(const msg::PcmChunk& chunk)
7072 {
7173 if (flacChunk_ == nullptr)
72 flacChunk_ = make_shared<msg::PcmChunk>(chunk->format, 0);
73
74 int samples = chunk->getSampleCount();
75 int frames = chunk->getFrameCount();
76 // LOG(INFO) << "payload: " << chunk->payloadSize << "\tframes: " << frames << "\tsamples: " << samples << "\tduration: " <<
74 flacChunk_ = make_shared<msg::PcmChunk>(chunk.format, 0);
75
76 int samples = chunk.getSampleCount();
77 int frames = chunk.getFrameCount();
78 // LOG(INFO, LOG_TAG) << "payload: " << chunk->payloadSize << "\tframes: " << frames << "\tsamples: " << samples << "\tduration: " <<
7779 // chunk->duration<chronos::msec>().count() << "\n";
7880
7981 if (pcmBufferSize_ < samples)
8486
8587 if (sampleFormat_.sampleSize() == 1)
8688 {
87 FLAC__int8* buffer = (FLAC__int8*)chunk->payload;
89 FLAC__int8* buffer = (FLAC__int8*)chunk.payload;
8890 for (int i = 0; i < samples; i++)
8991 pcmBuffer_[i] = (FLAC__int32)(buffer[i]);
9092 }
9193 else if (sampleFormat_.sampleSize() == 2)
9294 {
93 FLAC__int16* buffer = (FLAC__int16*)chunk->payload;
95 FLAC__int16* buffer = (FLAC__int16*)chunk.payload;
9496 for (int i = 0; i < samples; i++)
9597 pcmBuffer_[i] = (FLAC__int32)(buffer[i]);
9698 }
9799 else if (sampleFormat_.sampleSize() == 4)
98100 {
99 FLAC__int32* buffer = (FLAC__int32*)chunk->payload;
101 FLAC__int32* buffer = (FLAC__int32*)chunk.payload;
100102 for (int i = 0; i < samples; i++)
101103 pcmBuffer_[i] = (FLAC__int32)(buffer[i]);
102104 }
107109 if (encodedSamples_ > 0)
108110 {
109111 double resMs = static_cast<double>(encodedSamples_) / sampleFormat_.msRate();
110 // LOG(INFO) << "encoded: " << chunk->payloadSize << "\tframes: " << encodedSamples_ << "\tres: " << resMs << "\n";
112 // LOG(INFO, LOG_TAG) << "encoded: " << chunk->payloadSize << "\tframes: " << encodedSamples_ << "\tres: " << resMs << "\n";
111113 encodedSamples_ = 0;
112 listener_->onChunkEncoded(this, flacChunk_, resMs);
113 flacChunk_ = make_shared<msg::PcmChunk>(chunk->format, 0);
114 encoded_callback_(*this, flacChunk_, resMs);
115 flacChunk_ = make_shared<msg::PcmChunk>(chunk.format, 0);
114116 }
115117 }
116118
118120 FLAC__StreamEncoderWriteStatus FlacEncoder::write_callback(const FLAC__StreamEncoder* /*encoder*/, const FLAC__byte buffer[], size_t bytes, unsigned samples,
119121 unsigned current_frame)
120122 {
121 // LOG(INFO) << "write_callback: " << bytes << ", " << samples << ", " << current_frame << "\n";
123 // LOG(INFO, LOG_TAG) << "write_callback: " << bytes << ", " << samples << ", " << current_frame << "\n";
122124 if ((current_frame == 0) && (bytes > 0) && (samples == 0))
123125 {
124126 headerChunk_->payload = (char*)realloc(headerChunk_->payload, headerChunk_->payloadSize + bytes);
1515 along with this program. If not, see <http://www.gnu.org/licenses/>.
1616 ***/
1717
18 #ifndef FLAC_ENCODER_H
19 #define FLAC_ENCODER_H
18 #ifndef FLAC_ENCODER_HPP
19 #define FLAC_ENCODER_HPP
2020 #include "encoder.hpp"
2121 #include <stdio.h>
2222 #include <stdlib.h>
3333 public:
3434 FlacEncoder(const std::string& codecOptions = "");
3535 ~FlacEncoder() override;
36 void encode(const msg::PcmChunk* chunk) override;
36 void encode(const msg::PcmChunk& chunk) override;
3737 std::string getAvailableOptions() const override;
3838 std::string getDefaultOptions() const override;
3939 std::string name() const override;
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 "null_encoder.hpp"
19
20
21 namespace encoder
22 {
23
24
25 NullEncoder::NullEncoder(const std::string& codecOptions) : Encoder(codecOptions)
26 {
27 headerChunk_.reset(new msg::CodecHeader("null"));
28 }
29
30
31 void NullEncoder::encode(const msg::PcmChunk& chunk)
32 {
33 std::ignore = chunk;
34 }
35
36
37 void NullEncoder::initEncoder()
38 {
39 }
40
41
42 std::string NullEncoder::name() const
43 {
44 return "null";
45 }
46
47 } // namespace encoder
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 NULL_ENCODER_HPP
19 #define NULL_ENCODER_HPP
20 #include "encoder.hpp"
21
22 namespace encoder
23 {
24
25 class NullEncoder : public Encoder
26 {
27 public:
28 NullEncoder(const std::string& codecOptions = "");
29 void encode(const msg::PcmChunk& chunk) override;
30 std::string name() const override;
31
32 protected:
33 void initEncoder() override;
34 };
35
36 } // namespace encoder
37
38 #endif
3030 namespace encoder
3131 {
3232
33 static constexpr auto LOG_TAG = "OggEnc";
34
3335 OggEncoder::OggEncoder(const std::string& codecOptions) : Encoder(codecOptions), lastGranulepos_(0)
3436 {
3537 }
6365 }
6466
6567
66 void OggEncoder::encode(const msg::PcmChunk* chunk)
68 void OggEncoder::encode(const msg::PcmChunk& chunk)
6769 {
6870 double res = 0;
69 // LOG(TRACE) << "payload: " << chunk->payloadSize << "\tframes: " << chunk->getFrameCount() << "\tduration: " << chunk->duration<chronos::msec>().count()
71 // LOG(TRACE, LOG_TAG) << "payload: " << chunk->payloadSize << "\tframes: " << chunk->getFrameCount() << "\tduration: " <<
72 // chunk->duration<chronos::msec>().count()
7073 // << "\n";
71 int frames = chunk->getFrameCount();
74 int frames = chunk.getFrameCount();
7275 float** buffer = vorbis_analysis_buffer(&vd_, frames);
7376
7477 /* uninterleave samples */
7679 {
7780 if (sampleFormat_.sampleSize() == 1)
7881 {
79 int8_t* chunkBuffer = (int8_t*)chunk->payload;
82 int8_t* chunkBuffer = (int8_t*)chunk.payload;
8083 for (int i = 0; i < frames; i++)
8184 buffer[channel][i] = chunkBuffer[sampleFormat_.channels() * i + channel] / 128.f;
8285 }
8386 else if (sampleFormat_.sampleSize() == 2)
8487 {
85 int16_t* chunkBuffer = (int16_t*)chunk->payload;
88 int16_t* chunkBuffer = (int16_t*)chunk.payload;
8689 for (int i = 0; i < frames; i++)
8790 buffer[channel][i] = chunkBuffer[sampleFormat_.channels() * i + channel] / 32768.f;
8891 }
8992 else if (sampleFormat_.sampleSize() == 4)
9093 {
91 int32_t* chunkBuffer = (int32_t*)chunk->payload;
94 int32_t* chunkBuffer = (int32_t*)chunk.payload;
9295 for (int i = 0; i < frames; i++)
9396 buffer[channel][i] = chunkBuffer[sampleFormat_.channels() * i + channel] / 2147483648.f;
9497 }
97100 /* tell the library how much we actually submitted */
98101 vorbis_analysis_wrote(&vd_, frames);
99102
100 auto oggChunk = make_shared<msg::PcmChunk>(chunk->format, 0);
103 auto oggChunk = make_shared<msg::PcmChunk>(chunk.format, 0);
101104
102105 /* vorbis does some data preanalysis, then divvies up blocks for
103106 more involved (potentially parallel) processing. Get a single
141144 if (res > 0)
142145 {
143146 res /= sampleFormat_.msRate();
144 // LOG(INFO) << "res: " << res << "\n";
147 // LOG(INFO, LOG_TAG) << "res: " << res << "\n";
145148 lastGranulepos_ = os_.granulepos;
146149 // make oggChunk smaller
147150 oggChunk->payload = (char*)realloc(oggChunk->payload, pos);
148151 oggChunk->payloadSize = pos;
149 listener_->onChunkEncoded(this, oggChunk, res);
152 encoded_callback_(*this, oggChunk, res);
150153 }
151154 }
152155
219222 vorbis_comment_init(&vc_);
220223 vorbis_comment_add_tag(&vc_, "TITLE", "SnapStream");
221224 vorbis_comment_add_tag(&vc_, "VERSION", VERSION);
222 vorbis_comment_add_tag(&vc_, "SAMPLE_FORMAT", sampleFormat_.getFormat().c_str());
225 vorbis_comment_add_tag(&vc_, "SAMPLE_FORMAT", sampleFormat_.toString().c_str());
223226
224227 /* set up the analysis state and auxiliary encoding storage */
225228 vorbis_analysis_init(&vd_, &vi_);
259262 break;
260263 headerChunk_->payloadSize += og_.header_len + og_.body_len;
261264 headerChunk_->payload = (char*)realloc(headerChunk_->payload, headerChunk_->payloadSize);
262 LOG(DEBUG) << "HeadLen: " << og_.header_len << ", bodyLen: " << og_.body_len << ", result: " << result << "\n";
265 LOG(DEBUG, LOG_TAG) << "HeadLen: " << og_.header_len << ", bodyLen: " << og_.body_len << ", result: " << result << "\n";
263266 memcpy(headerChunk_->payload + pos, og_.header, og_.header_len);
264267 pos += og_.header_len;
265268 memcpy(headerChunk_->payload + pos, og_.body, og_.body_len);
1515 along with this program. If not, see <http://www.gnu.org/licenses/>.
1616 ***/
1717
18 #ifndef OGG_ENCODER_H
19 #define OGG_ENCODER_H
18 #ifndef OGG_ENCODER_HPP
19 #define OGG_ENCODER_HPP
2020 #include "encoder.hpp"
2121 #include <ogg/ogg.h>
2222 #include <vorbis/vorbisenc.h>
3030 OggEncoder(const std::string& codecOptions = "");
3131 ~OggEncoder() override;
3232
33 void encode(const msg::PcmChunk* chunk) override;
33 void encode(const msg::PcmChunk& chunk) override;
3434 std::string getAvailableOptions() const override;
3535 std::string getDefaultOptions() const override;
3636 std::string name() const override;
2929 #define ID_OPUS 0x4F505553
3030 static constexpr opus_int32 const_min_bitrate = 6000;
3131 static constexpr opus_int32 const_max_bitrate = 512000;
32 static constexpr int min_chunk_size = 10;
33
34 static constexpr auto LOG_TAG = "OpusEnc";
3235
3336 namespace
3437 {
7679 {
7780 // Opus is quite restrictive in sample rate and bit depth
7881 // 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");
82 // if ((sampleFormat_.rate() != 48000) || (sampleFormat_.bits() != 16) || (sampleFormat_.channels() != 2))
83 // throw SnapException("Opus sampleformat must be 48000:16:2");
84 if (sampleFormat_.channels() != 2)
85 throw SnapException("Opus requires a stereo signal");
86 SampleFormat out{48000, 16, 2};
87 if ((sampleFormat_.rate() != 48000) || (sampleFormat_.bits() != 16))
88 LOG(INFO, LOG_TAG) << "Resampling input from " << sampleFormat_.toString() << " to " << out.toString() << " as required by Opus\n";
89
90 resampler_ = make_unique<Resampler>(sampleFormat_, out);
91 sampleFormat_ = out;
8192
8293 opus_int32 bitrate = 192000;
8394 opus_int32 complexity = 10;
131142 throw SnapException("Opus error parsing options: " + codecOptions_);
132143 }
133144
134 LOG(INFO) << "Opus bitrate: " << bitrate << " bps, complexity: " << complexity << "\n";
145 LOG(INFO, LOG_TAG) << "Opus bitrate: " << bitrate << " bps, complexity: " << complexity << "\n";
135146
136147 int error;
137148 enc_ = opus_encoder_create(sampleFormat_.rate(), sampleFormat_.channels(), OPUS_APPLICATION_RESTRICTED_LOWDELAY, &error);
152163 assign(payload + 8, SWAP_16(sampleFormat_.bits()));
153164 assign(payload + 10, SWAP_16(sampleFormat_.channels()));
154165
155 remainder_ = std::make_unique<msg::PcmChunk>(sampleFormat_, 10);
166 remainder_ = std::make_unique<msg::PcmChunk>(sampleFormat_, min_chunk_size);
156167 remainder_max_size_ = remainder_->payloadSize;
157168 remainder_->payloadSize = 0;
158169 }
163174 // 240, 480, 960, 1920, 2880 frames
164175 // We will split the chunk into encodable sizes and store any remaining data in the remainder_ buffer
165176 // and encode the buffer content in the next iteration
166 void OpusEncoder::encode(const msg::PcmChunk* chunk)
167 {
168 // LOG(TRACE) << "encode " << chunk->duration<std::chrono::milliseconds>().count() << "ms\n";
177 void OpusEncoder::encode(const msg::PcmChunk& chunk)
178 {
179 // chunk =
180 // resampler_->resample(std::make_shared<msg::PcmChunk>(chunk)).get();
181 auto in = std::make_shared<msg::PcmChunk>(chunk);
182 auto out = resampler_->resample(in); //, std::chrono::milliseconds(20));
183 if (out == nullptr)
184 return;
185 // chunk = out.get();
186
187 // LOG(TRACE, LOG_TAG) << "encode " << chunk->duration<std::chrono::milliseconds>().count() << "ms\n";
169188 uint32_t offset = 0;
170189
171190 // check if there is something left from the last call to encode and fill the remainder buffer to
172 // an encodable size of 10ms
191 // an encodable size of 10ms (min_chunk_size)
173192 if (remainder_->payloadSize > 0)
174193 {
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(TRACE) << "remainder buffer size: " << remainder_->payloadSize << "/" << remainder_max_size_ << ", appending " << offset << " bytes\n";
194 offset = std::min(static_cast<uint32_t>(remainder_max_size_ - remainder_->payloadSize), out->payloadSize);
195 memcpy(remainder_->payload + remainder_->payloadSize, out->payload, offset);
196 // LOG(TRACE, LOG_TAG) << "remainder buffer size: " << remainder_->payloadSize << "/" << remainder_max_size_ << ", appending " << offset << " bytes\n";
178197 remainder_->payloadSize += offset;
179198
180199 if (remainder_->payloadSize < remainder_max_size_)
181200 {
182 LOG(DEBUG) << "not enough data to encode (" << remainder_->payloadSize << " of " << remainder_max_size_ << " bytes)"
183 << "\n";
201 LOG(DEBUG, LOG_TAG) << "not enough data to encode (" << remainder_->payloadSize << " of " << remainder_max_size_ << " bytes)"
202 << "\n";
184203 return;
185204 }
186 encode(chunk->format, remainder_->payload, remainder_->payloadSize);
205 encode(out->format, remainder_->payload, remainder_->payloadSize);
187206 remainder_->payloadSize = 0;
188207 }
189208
190209 // encode greedy 60ms, 40ms, 20ms, 10ms chunks
191 std::vector<size_t> chunk_durations{60, 40, 20, 10};
210 std::vector<size_t> chunk_durations{60, 40, 20, min_chunk_size};
192211 for (const auto duration : chunk_durations)
193212 {
194213 auto ms2bytes = [this](size_t ms) { return (ms * sampleFormat_.msRate() * sampleFormat_.frameSize()); };
195214 uint32_t bytes = ms2bytes(duration);
196 while (chunk->payloadSize - offset >= bytes)
215 while (out->payloadSize - offset >= bytes)
197216 {
198 // LOG(TRACE) << "encoding " << duration << "ms (" << bytes << "), offset: " << offset << ", chunk size: " << chunk->payloadSize - offset << "\n";
199 encode(chunk->format, chunk->payload + offset, bytes);
217 // LOG(TRACE, LOG_TAG) << "encoding " << duration << "ms (" << bytes << "), offset: " << offset << ", chunk size: " << chunk->payloadSize - offset
218 // << "\n";
219 encode(out->format, out->payload + offset, bytes);
200220 offset += bytes;
201221 }
202 if (chunk->payloadSize == offset)
222 if (out->payloadSize == offset)
203223 break;
204224 }
205225
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;
226 // something is left (must be less than min_chunk_size (10ms))
227 if (out->payloadSize > offset)
228 {
229 memcpy(remainder_->payload + remainder_->payloadSize, out->payload + offset, out->payloadSize - offset);
230 remainder_->payloadSize = out->payloadSize - offset;
211231 }
212232 }
213233
215235 void OpusEncoder::encode(const SampleFormat& format, const char* data, size_t size)
216236 {
217237 // void* buffer;
218 // LOG(INFO) << "frames: " << chunk->readFrames(buffer, std::chrono::milliseconds(10)) << "\n";
238 // LOG(INFO, LOG_TAG) << "frames: " << chunk->readFrames(buffer, std::chrono::milliseconds(10)) << "\n";
219239 int samples_per_channel = size / format.frameSize();
220240 if (encoded_.size() < size)
221241 encoded_.resize(size);
222242
223243 opus_int32 len = opus_encode(enc_, (opus_int16*)data, samples_per_channel, encoded_.data(), size);
224 // LOG(TRACE) << "Encode " << samples_per_channel << " frames, size " << size << " bytes, encoded: " << len << " bytes" << '\n';
244 LOG(TRACE, LOG_TAG) << "Encode " << samples_per_channel << " frames, size " << size << " bytes, encoded: " << len << " bytes" << '\n';
225245
226246 if (len > 0)
227247 {
230250 opusChunk->payloadSize = len;
231251 opusChunk->payload = (char*)realloc(opusChunk->payload, opusChunk->payloadSize);
232252 memcpy(opusChunk->payload, encoded_.data(), len);
233 listener_->onChunkEncoded(this, opusChunk, (double)samples_per_channel / sampleFormat_.msRate());
253 encoded_callback_(*this, opusChunk, (double)samples_per_channel / sampleFormat_.msRate());
234254 }
235255 else
236256 {
237 LOG(ERROR) << "Failed to encode chunk: " << opus_strerror(len) << ", samples / channel: " << samples_per_channel << ", bytes: " << size << '\n';
257 LOG(ERROR, LOG_TAG) << "Failed to encode chunk: " << opus_strerror(len) << ", samples / channel: " << samples_per_channel << ", bytes: " << size
258 << '\n';
238259 }
239260 }
240261
1515 along with this program. If not, see <http://www.gnu.org/licenses/>.
1616 ***/
1717
18 #pragma once
18 #ifndef OPUS_ENCODER_HPP
19 #define OPUS_ENCODER_HPP
1920
21 #include "common/resampler.hpp"
2022 #include "encoder.hpp"
2123 #include <opus/opus.h>
2224
3032 OpusEncoder(const std::string& codecOptions = "");
3133 ~OpusEncoder() override;
3234
33 void encode(const msg::PcmChunk* chunk) override;
35 void encode(const msg::PcmChunk& chunk) override;
3436 std::string getAvailableOptions() const override;
3537 std::string getDefaultOptions() const override;
3638 std::string name() const override;
4244 std::vector<unsigned char> encoded_;
4345 std::unique_ptr<msg::PcmChunk> remainder_;
4446 size_t remainder_max_size_;
47 std::unique_ptr<Resampler> resampler_;
4548 };
4649
4750 } // namespace encoder
51
52 #endif
4646 }
4747
4848
49 void PcmEncoder::encode(const msg::PcmChunk* chunk)
49 void PcmEncoder::encode(const msg::PcmChunk& chunk)
5050 {
51 auto pcmChunk = std::make_shared<msg::PcmChunk>(*chunk);
52 listener_->onChunkEncoded(this, pcmChunk, pcmChunk->durationMs());
51 // copy the chunk into a shared_ptr
52 auto pcmChunk = std::make_shared<msg::PcmChunk>(chunk);
53 encoded_callback_(*this, pcmChunk, pcmChunk->durationMs());
5354 }
5455
5556
1515 along with this program. If not, see <http://www.gnu.org/licenses/>.
1616 ***/
1717
18 #ifndef PCM_ENCODER_H
19 #define PCM_ENCODER_H
18 #ifndef PCM_ENCODER_HPP
19 #define PCM_ENCODER_HPP
2020 #include "encoder.hpp"
2121
2222 namespace encoder
2626 {
2727 public:
2828 PcmEncoder(const std::string& codecOptions = "");
29 void encode(const msg::PcmChunk* chunk) override;
29 void encode(const msg::PcmChunk& chunk) override;
3030 std::string name() const override;
3131
3232 protected:
0 #!/bin/sh
1
2 # PROVIDE: snapserver
3 # REQUIRE: DAEMON
4 # KEYWORD: Snapserver
5 #
6 # Snapserver - Snapcast server
7 #
8 # Add the following line to /etc/rc.conf to enable snapserver:
9 # snapserver_enable=YES
10 # Add snapserver_opts=<options> to configure command line arguments
11
12 snapserver_opts="-d --logging.sink=system"
13
14 . /etc/rc.subr
15
16 name=snapserver
17 rcvar=snapserver_enable
18 desc="Snapserver - Snapcast server"
19
20 load_rc_config $name
21
22 : ${snapserver_enable:=NO}
23
24 command=/usr/local/bin/${name}
25 pidfile="/var/run/${name}/pid"
26
27 start_cmd=snapserver_start
28
29
30 snapserver_start() {
31 checkyesno snapserver_enable && echo "Starting snapserver." && ${command} ${snapserver_opts}
32 }
33
34 run_rc_command $1
35
3131 # the pid file when running as daemon
3232 #pidfile = /var/run/snapserver/pid
3333
34 # the user to run as when daemonized
35 #user = snapserver
36 # the group to run as when daemonized
37 #group = snapserver
38
3439 # directory where persistent data is stored (server.json)
3540 # if empty, data dir will be
3641 # - "/var/lib/snapserver/" when running as daemon
5863 #port = 1780
5964
6065 # serve a website from the doc_root location
61 #doc_root =
66 # disabled if commented or empty
67 doc_root = /usr/share/snapserver/snapweb
6268 #
6369 ###############################################################################
6470
95101 # which port the server should listen to
96102 #port = 1704
97103
98 # stream URI of the PCM input stream, can be configured multiple times
104 # source URI of the PCM input stream, can be configured multiple times
99105 # The following notation is used in this paragraph:
100106 # <angle brackets>: the whole expression must be replaced with your specific setting
101107 # [square brackets]: the whole expression is optional and can be left out
103109 #
104110 # Format: TYPE://host/path?name=<name>[&codec=<codec>][&sampleformat=<sampleformat>][&chunk_ms=<chunk ms>]
105111 # parameters have the form "key=value", they are concatenated with an "&" character
106 # parameter "name" is mandatory for all streams, while codec, sampleformat and chunk_ms are optional
112 # parameter "name" is mandatory for all sources, while codec, sampleformat and chunk_ms are optional
107113 # and will override the default codec, sampleformat or chunk_ms settings
108 # Non blocking streams support the dryout_ms parameter: when no new data is read from the stream, send silence to the clients
114 # Non blocking sources support the dryout_ms parameter: when no new data is read from the source, send silence to the clients
109115 # Available types are:
110116 # pipe: pipe:///<path/to/pipe>?name=<name>[&mode=create][&dryout_ms=2000], mode can be "create" or "read"
111117 # librespot: librespot:///<path/to/librespot>?name=<name>[&dryout_ms=2000][&username=<my username>&password=<my password>][&devicename=Snapcast][&bitrate=320][&wd_timeout=7800][&volume=100][&onevent=""][&nomalize=false][&autoplay=false]
118124 # sampleformat will be set to "44100:16:2"
119125 # tcp server: tcp://<listen IP, e.g. 127.0.0.1>:<port>?name=<name>[&mode=server]
120126 # tcp client: tcp://<server IP, e.g. 127.0.0.1>:<port>?name=<name>&mode=client
121 stream = pipe:///tmp/snapfifo?name=default
122 #stream = tcp://127.0.0.1?name=mopidy_tcp
127 # alsa: alsa://?name=<name>&device=<alsa device>
128 # meta: meta:///<name of source#1>/<name of source#2>/.../<name of source#N>?name=<name>
129 source = pipe:///tmp/snapfifo?name=default
130 #source = tcp://127.0.0.1?name=mopidy_tcp
123131
124132 # Default sample format
125133 #sampleformat = 48000:16:2
129137 # Type codec:? to get codec specific options
130138 #codec = flac
131139
132 # Default stream read chunk size [ms]
140 # Default source stream read chunk size [ms]
133141 #chunk_ms = 20
134142
135143 # Buffer [ms]
145153 #
146154 [logging]
147155
148 # enable debug logging
149 #debug = false
156 # log sink [null,system,stdout,stderr,file:<filename>]
157 # when left empty: if running as daemon "system" else "stdout"
158 #sink =
150159
151 # log file name for the debug logs (debug must be enabled)
152 #debug_logfile =
160 # log filter <tag>:<level>[,<tag>:<level>]*
161 # with tag = * or <log tag> and level = [trace,debug,info,notice,warning,error,fatal]
162 #filter = *:info
153163 #
154164 ###############################################################################
66 <key>ProgramArguments</key>
77 <array>
88 <string>/usr/local/bin/snapserver</string>
9 <string>--logging.sink=system</string>
910 <!-- <string>-d</string> -->
1011 </array>
1112 <key>RunAtLoad</key>
0 <html>
1
2 <head>
3 <meta name="viewport" content="width=device-width, initial-scale=1">
4 <meta name="theme-color" content="#455A64">
5 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
6 <link rel="manifest" href="manifest.json">
7 <link rel="stylesheet" href="styles.css">
8 <link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
9 <title>Snapweb</title>
10 <script src="3rd-party/libflac.js"></script>
11 <script src="snapstream.js"></script>
12 <script src="snapcontrol.js"></script>
13 </head>
14
15 <body>
16 <div id="show"></div>
17 </body>
18
19 </html>
0 {
1 "short_name": "Snapweb",
2 "name": "Snapcast WebApp",
3 "icons": [
4 {
5 "src": "launcher-icon.png",
6 "sizes": "192x192",
7 "type": "image/png"
8 }
9 ],
10 "start_url": "/index.html",
11 "display": "standalone",
12 "categories": ["music"],
13 "description": "Snapcast web client",
14 "theme_color": "#455A64"
15 }
0 "use strict";
1 class Host {
2 constructor(json) {
3 this.arch = "";
4 this.ip = "";
5 this.mac = "";
6 this.name = "";
7 this.os = "";
8 this.fromJson(json);
9 }
10 fromJson(json) {
11 this.arch = json.arch;
12 this.ip = json.ip;
13 this.mac = json.mac;
14 this.name = json.name;
15 this.os = json.os;
16 }
17 }
18 class Client {
19 constructor(json) {
20 this.id = "";
21 this.connected = false;
22 this.fromJson(json);
23 }
24 fromJson(json) {
25 this.id = json.id;
26 this.host = new Host(json.host);
27 let jsnapclient = json.snapclient;
28 this.snapclient = { name: jsnapclient.name, protocolVersion: jsnapclient.protocolVersion, version: jsnapclient.version };
29 let jconfig = json.config;
30 this.config = { instance: jconfig.instance, latency: jconfig.latency, name: jconfig.name, volume: { muted: jconfig.volume.muted, percent: jconfig.volume.percent } };
31 this.lastSeen = { sec: json.lastSeen.sec, usec: json.lastSeen.usec };
32 this.connected = Boolean(json.connected);
33 }
34 }
35 class Group {
36 constructor(json) {
37 this.name = "";
38 this.id = "";
39 this.stream_id = "";
40 this.muted = false;
41 this.clients = [];
42 this.fromJson(json);
43 }
44 fromJson(json) {
45 this.name = json.name;
46 this.id = json.id;
47 this.stream_id = json.stream_id;
48 this.muted = Boolean(json.muted);
49 for (let client of json.clients)
50 this.clients.push(new Client(client));
51 }
52 getClient(id) {
53 for (let client of this.clients) {
54 if (client.id == id)
55 return client;
56 }
57 return null;
58 }
59 }
60 class Stream {
61 constructor(json) {
62 this.id = "";
63 this.status = "";
64 this.fromJson(json);
65 }
66 fromJson(json) {
67 this.id = json.id;
68 this.status = json.status;
69 let juri = json.uri;
70 this.uri = { raw: juri.raw, scheme: juri.scheme, host: juri.host, path: juri.path, fragment: juri.fragment, query: juri.query };
71 }
72 }
73 class Server {
74 constructor(json) {
75 this.groups = [];
76 this.streams = [];
77 if (json)
78 this.fromJson(json);
79 }
80 fromJson(json) {
81 this.groups = [];
82 for (let jgroup of json.groups)
83 this.groups.push(new Group(jgroup));
84 let jsnapserver = json.server.snapserver;
85 this.server = { host: new Host(json.server.host), snapserver: { controlProtocolVersion: jsnapserver.controlProtocolVersion, name: jsnapserver.name, protocolVersion: jsnapserver.protocolVersion, version: jsnapserver.version } };
86 this.streams = [];
87 for (let jstream of json.streams) {
88 this.streams.push(new Stream(jstream));
89 }
90 }
91 getClient(id) {
92 for (let group of this.groups) {
93 let client = group.getClient(id);
94 if (client)
95 return client;
96 }
97 return null;
98 }
99 getGroup(id) {
100 for (let group of this.groups) {
101 if (group.id == id)
102 return group;
103 }
104 return null;
105 }
106 getStream(id) {
107 for (let stream of this.streams) {
108 if (stream.id == id)
109 return stream;
110 }
111 return null;
112 }
113 }
114 class SnapControl {
115 constructor(host, port) {
116 this.server = new Server();
117 this.connection = new WebSocket('ws://' + host + ':' + port + '/jsonrpc');
118 this.msg_id = 0;
119 this.status_req_id = -1;
120 // console.log(navigator);
121 this.connection.onmessage = (msg) => this.onMessage(msg.data);
122 this.connection.onopen = (ev) => { this.status_req_id = this.sendRequest('Server.GetStatus'); };
123 this.connection.onerror = (ev) => { alert("error: " + ev.type); }; //this.onError(ev);
124 }
125 action(answer) {
126 switch (answer.method) {
127 case 'Client.OnVolumeChanged':
128 let client = this.getClient(answer.params.id);
129 client.config.volume = answer.params.volume;
130 updateGroupVolume(this.getGroupFromClient(client.id));
131 break;
132 case 'Client.OnLatencyChanged':
133 this.getClient(answer.params.id).config.latency = answer.params.latency;
134 break;
135 case 'Client.OnNameChanged':
136 this.getClient(answer.params.id).config.name = answer.params.name;
137 break;
138 case 'Client.OnConnect':
139 case 'Client.OnDisconnect':
140 this.getClient(answer.params.client.id).fromJson(answer.params.client);
141 break;
142 case 'Group.OnMute':
143 this.getGroup(answer.params.id).muted = Boolean(answer.params.mute);
144 break;
145 case 'Group.OnStreamChanged':
146 this.getGroup(answer.params.id).stream_id = answer.params.stream_id;
147 break;
148 case 'Stream.OnUpdate':
149 this.getStream(answer.params.id).fromJson(answer.params.stream);
150 break;
151 case 'Server.OnUpdate':
152 this.server.fromJson(answer.params.server);
153 break;
154 default:
155 break;
156 }
157 }
158 getClient(client_id) {
159 return this.server.getClient(client_id);
160 }
161 getGroup(group_id) {
162 return this.server.getGroup(group_id);
163 }
164 getGroupVolume(group, online) {
165 if (group.clients.length == 0)
166 return 0;
167 let group_vol = 0;
168 let client_count = 0;
169 for (let client of group.clients) {
170 if (online && !client.connected)
171 continue;
172 group_vol += client.config.volume.percent;
173 ++client_count;
174 }
175 if (client_count == 0)
176 return 0;
177 return group_vol / client_count;
178 }
179 getGroupFromClient(client_id) {
180 for (let group of this.server.groups)
181 for (let client of group.clients)
182 if (client.id == client_id)
183 return group;
184 return null;
185 }
186 getStream(stream_id) {
187 return this.server.getStream(stream_id);
188 }
189 setVolume(client_id, percent, mute) {
190 percent = Math.max(0, Math.min(100, percent));
191 let client = this.getClient(client_id);
192 client.config.volume.percent = percent;
193 if (mute != undefined)
194 client.config.volume.muted = mute;
195 this.sendRequest('Client.SetVolume', '{"id":"' + client_id + '","volume":{"muted":' + (client.config.volume.muted ? "true" : "false") + ',"percent":' + client.config.volume.percent + '}}');
196 }
197 setClientName(client_id, name) {
198 let client = this.getClient(client_id);
199 let current_name = (client.config.name != "") ? client.config.name : client.host.name;
200 if (name != current_name) {
201 this.sendRequest('Client.SetName', '{"id":"' + client_id + '","name":"' + name + '"}');
202 client.config.name = name;
203 }
204 }
205 setClientLatency(client_id, latency) {
206 let client = this.getClient(client_id);
207 let current_latency = client.config.latency;
208 if (latency != current_latency) {
209 this.sendRequest('Client.SetLatency', '{"id":"' + client_id + '","latency":' + latency + '}');
210 client.config.latency = latency;
211 }
212 }
213 deleteClient(client_id) {
214 let client = this.getClient(client_id);
215 this.sendRequest('Server.DeleteClient', '{"id": "' + client_id + '"}');
216 this.server.groups.forEach((g, gi) => {
217 g.clients.forEach((c, ci) => {
218 if (c.id == client_id) {
219 this.server.groups[gi].clients.splice(ci, 1);
220 }
221 });
222 });
223 this.server.groups.forEach((g, gi) => {
224 if (g.clients.length == 0) {
225 this.server.groups.splice(gi, 1);
226 }
227 });
228 show();
229 }
230 setStream(group_id, stream_id) {
231 this.getGroup(group_id).stream_id = stream_id;
232 this.sendRequest('Group.SetStream', '{"id":"' + group_id + '","stream_id":"' + stream_id + '"}');
233 }
234 setClients(group_id, clients) {
235 this.status_req_id = this.sendRequest('Group.SetClients', '{"clients":' + JSON.stringify(clients) + ',"id":"' + group_id + '"}');
236 }
237 muteGroup(group_id, mute) {
238 this.getGroup(group_id).muted = mute;
239 this.sendRequest('Group.SetMute', '{"id":"' + group_id + '","mute":' + (mute ? "true" : "false") + '}');
240 }
241 sendRequest(method, params) {
242 let msg = '{"id": ' + (++this.msg_id) + ',"jsonrpc":"2.0","method":"' + method + '"';
243 if (params)
244 msg += ',"params": ' + params;
245 msg += '}';
246 console.log("Sending: " + msg);
247 this.connection.send(msg);
248 return this.msg_id;
249 }
250 onMessage(msg) {
251 let answer = JSON.parse(msg);
252 let is_response = (answer.id != undefined);
253 console.log("Received " + (is_response ? "response" : "notification") + ", json: " + JSON.stringify(answer));
254 if (is_response) {
255 if (answer.id == this.status_req_id) {
256 this.server = new Server(answer.result.server);
257 show();
258 }
259 }
260 else {
261 if (Array.isArray(answer)) {
262 for (let a of answer) {
263 this.action(a);
264 }
265 }
266 else {
267 this.action(answer);
268 }
269 // TODO: don't update everything, but only the changed,
270 // e.g. update the values for the volume sliders
271 show();
272 }
273 }
274 }
275 let snapcontrol;
276 let snapstream = null;
277 let hide_offline = true;
278 let autoplay_done = false;
279 function autoplayRequested() {
280 return document.location.hash.match(/autoplay/) !== null;
281 }
282 function show() {
283 // Render the page
284 let play_img;
285 if (snapstream) {
286 play_img = 'stop.png';
287 }
288 else {
289 play_img = 'play.png';
290 }
291 let content = "";
292 content += "<div class='navbar'>Snapcast";
293 let serverVersion = snapcontrol.server.server.snapserver.version.split('.');
294 if ((serverVersion.length >= 2) && (+serverVersion[1] >= 21)) {
295 content += " <a href=\"javascript:play();\"><img src='" + play_img + "' class='play-button'></a>";
296 // Stream became ready and was not playing. If autoplay is requested, start playing.
297 if (!snapstream && !autoplay_done && autoplayRequested()) {
298 autoplay_done = true;
299 play();
300 }
301 }
302 content += "</div>";
303 content += "<div class='content'>";
304 let server = snapcontrol.server;
305 for (let group of server.groups) {
306 if (hide_offline) {
307 let groupActive = false;
308 for (let client of group.clients) {
309 if (client.connected) {
310 groupActive = true;
311 break;
312 }
313 }
314 if (!groupActive)
315 continue;
316 }
317 // Set mute variables
318 let classgroup;
319 let muted;
320 let mute_img;
321 if (group.muted == true) {
322 classgroup = 'group muted';
323 muted = true;
324 mute_img = 'mute_icon.png';
325 }
326 else {
327 classgroup = 'group';
328 muted = false;
329 mute_img = 'speaker_icon.png';
330 }
331 // Start group div
332 content += "<div id='g_" + group.id + "' class='" + classgroup + "'>";
333 // Create stream selection dropdown
334 let streamselect = "<select id='stream_" + group.id + "' onchange='setStream(\"" + group.id + "\")' class='stream'>";
335 for (let i_stream = 0; i_stream < server.streams.length; i_stream++) {
336 let streamselected = "";
337 if (group.stream_id == server.streams[i_stream].id) {
338 streamselected = 'selected';
339 }
340 streamselect += "<option value='" + server.streams[i_stream].id + "' " + streamselected + ">" + server.streams[i_stream].id + ": " + server.streams[i_stream].status + "</option>";
341 }
342 streamselect += "</select>";
343 // Group mute and refresh button
344 content += "<div class='groupheader'>";
345 content += streamselect;
346 let clientCount = 0;
347 for (let client of group.clients)
348 if (!hide_offline || client.connected)
349 clientCount++;
350 if (clientCount > 1) {
351 let volume = snapcontrol.getGroupVolume(group, hide_offline);
352 content += "<a href=\"javascript:setMuteGroup('" + group.id + "'," + !muted + ");\"><img src='" + mute_img + "' class='mute-button'></a>";
353 content += "<div class='slidergroupdiv'>";
354 content += " <input type='range' draggable='false' min=0 max=100 step=1 id='vol_" + group.id + "' oninput='javascript:setGroupVolume(\"" + group.id + "\")' value=" + volume + " class='slider'>";
355 // content += " <input type='range' min=0 max=100 step=1 id='vol_" + group.id + "' oninput='javascript:setVolume(\"" + client.id + "\"," + client.config.volume.muted + ")' value=" + client.config.volume.percent + " class='" + sliderclass + "'>";
356 content += "</div>";
357 }
358 // transparent placeholder edit icon
359 content += "<div class='edit-group-icon'>&#9998</div>";
360 content += "</div>";
361 content += "<hr class='groupheader-separator'>";
362 // Create clients in group
363 for (let client of group.clients) {
364 if (!client.connected && hide_offline)
365 continue;
366 // Set name and connection state vars, start client div
367 let name;
368 let clas = 'client';
369 if (client.config.name != "") {
370 name = client.config.name;
371 }
372 else {
373 name = client.host.name;
374 }
375 if (client.connected == false) {
376 clas = 'client disconnected';
377 }
378 content += "<div id='c_" + client.id + "' class='" + clas + "'>";
379 // Client mute status vars
380 let muted;
381 let mute_img;
382 let sliderclass;
383 if (client.config.volume.muted == true) {
384 muted = true;
385 sliderclass = 'slider muted';
386 mute_img = 'mute_icon.png';
387 }
388 else {
389 sliderclass = 'slider';
390 muted = false;
391 mute_img = 'speaker_icon.png';
392 }
393 // Populate client div
394 content += "<a href=\"javascript:setVolume('" + client.id + "'," + !muted + ");\"><img src='" + mute_img + "' class='mute-button'></a>";
395 content += " <div class='sliderdiv'>";
396 content += " <input type='range' min=0 max=100 step=1 id='vol_" + client.id + "' oninput='javascript:setVolume(\"" + client.id + "\"," + client.config.volume.muted + ")' value=" + client.config.volume.percent + " class='" + sliderclass + "'>";
397 content += " </div>";
398 content += " <span class='edit-icons'>";
399 content += " <a href=\"javascript:openClientSettings('" + client.id + "');\" class='edit-icon'>&#9998</a>";
400 if (client.connected == false) {
401 content += " <a href=\"javascript:deleteClient('" + client.id + "');\" class='delete-icon'>&#128465</a>";
402 content += " </span>";
403 }
404 else {
405 content += "</span>";
406 }
407 content += " <div class='name'>" + name + "</div>";
408 content += "</div>";
409 }
410 content += "</div>";
411 }
412 content += "</div>"; // content
413 content += "<div id='client-settings' class='client-settings'>";
414 content += " <div class='client-setting-content'>";
415 content += " <form action='javascript:closeClientSettings()'>";
416 content += " <label for='client-name'>Name</label>";
417 content += " <input type='text' class='client-input' id='client-name' name='client-name' placeholder='Client name..'>";
418 content += " <label for='client-latency'>Latency</label>";
419 content += " <input type='number' class='client-input' min='-10000' max='10000' id='client-latency' name='client-latency' placeholder='Latency in ms..'>";
420 content += " <label for='client-group'>Group</label>";
421 content += " <select id='client-group' class='client-input' name='client-group'>";
422 content += " </select>";
423 content += " <input type='submit' value='Submit'>";
424 content += " </form>";
425 content += " </div>";
426 content += "</div>";
427 // Pad then update page
428 content = content + "<br><br>";
429 document.getElementById('show').innerHTML = content;
430 for (let group of snapcontrol.server.groups) {
431 if (group.clients.length > 1) {
432 let slider = document.getElementById("vol_" + group.id);
433 if (slider == null)
434 continue;
435 slider.addEventListener('pointerdown', function (ev) {
436 groupVolumeEnter(group.id);
437 });
438 slider.addEventListener('touchstart', function () {
439 groupVolumeEnter(group.id);
440 });
441 }
442 }
443 }
444 function updateGroupVolume(group) {
445 let group_vol = snapcontrol.getGroupVolume(group, hide_offline);
446 let slider = document.getElementById("vol_" + group.id);
447 if (slider == null)
448 return;
449 console.log("updateGroupVolume group: " + group.id + ", volume: " + group_vol + ", slider: " + (slider != null));
450 slider.value = String(group_vol);
451 }
452 let client_volumes;
453 let group_volume;
454 function setGroupVolume(group_id) {
455 let group = snapcontrol.getGroup(group_id);
456 let percent = document.getElementById('vol_' + group.id).valueAsNumber;
457 console.log("setGroupVolume id: " + group.id + ", volume: " + percent);
458 // show()
459 let delta = percent - group_volume;
460 let ratio;
461 if (delta < 0)
462 ratio = (group_volume - percent) / group_volume;
463 else
464 ratio = (percent - group_volume) / (100 - group_volume);
465 for (let i = 0; i < group.clients.length; ++i) {
466 let new_volume = client_volumes[i];
467 if (delta < 0)
468 new_volume -= ratio * client_volumes[i];
469 else
470 new_volume += ratio * (100 - client_volumes[i]);
471 let client_id = group.clients[i].id;
472 // TODO: use batch request to update all client volumes at once
473 snapcontrol.setVolume(client_id, new_volume);
474 let slider = document.getElementById('vol_' + client_id);
475 if (slider)
476 slider.value = String(new_volume);
477 }
478 }
479 function groupVolumeEnter(group_id) {
480 let group = snapcontrol.getGroup(group_id);
481 let percent = document.getElementById('vol_' + group.id).valueAsNumber;
482 console.log("groupVolumeEnter id: " + group.id + ", volume: " + percent);
483 group_volume = percent;
484 client_volumes = [];
485 for (let i = 0; i < group.clients.length; ++i) {
486 client_volumes.push(group.clients[i].config.volume.percent);
487 }
488 // show()
489 }
490 function setVolume(id, mute) {
491 console.log("setVolume id: " + id + ", mute: " + mute);
492 let percent = document.getElementById('vol_' + id).valueAsNumber;
493 let client = snapcontrol.getClient(id);
494 let needs_update = (mute != client.config.volume.muted);
495 snapcontrol.setVolume(id, percent, mute);
496 let group = snapcontrol.getGroupFromClient(id);
497 updateGroupVolume(group);
498 if (needs_update)
499 show();
500 }
501 function play() {
502 if (snapstream) {
503 snapstream.stop();
504 snapstream = null;
505 }
506 else {
507 snapstream = new SnapStream(window.location.hostname, 1780);
508 }
509 show();
510 }
511 function setMuteGroup(id, mute) {
512 snapcontrol.muteGroup(id, mute);
513 show();
514 }
515 function setStream(id) {
516 snapcontrol.setStream(id, document.getElementById('stream_' + id).value);
517 show();
518 }
519 function setGroup(client_id, group_id) {
520 console.log("setGroup id: " + client_id + ", group: " + group_id);
521 let server = snapcontrol.server;
522 // Get client group id
523 let current_group = snapcontrol.getGroupFromClient(client_id);
524 // Get
525 // List of target group's clients
526 // OR
527 // List of current group's other clients
528 let send_clients = [];
529 for (let i_group = 0; i_group < server.groups.length; i_group++) {
530 if (server.groups[i_group].id == group_id || (group_id == "new" && server.groups[i_group].id == current_group.id)) {
531 for (let i_client = 0; i_client < server.groups[i_group].clients.length; i_client++) {
532 if (group_id == "new" && server.groups[i_group].clients[i_client].id == client_id) { }
533 else {
534 send_clients[send_clients.length] = server.groups[i_group].clients[i_client].id;
535 }
536 }
537 }
538 }
539 if (group_id == "new")
540 group_id = current_group.id;
541 else
542 send_clients[send_clients.length] = client_id;
543 snapcontrol.setClients(group_id, send_clients);
544 }
545 function setName(id) {
546 // Get current name and lacency
547 let client = snapcontrol.getClient(id);
548 let current_name = (client.config.name != "") ? client.config.name : client.host.name;
549 let current_latency = client.config.latency;
550 let new_name = window.prompt("New Name", current_name);
551 let new_latency = Number(window.prompt("New Latency", String(current_latency)));
552 if (new_name != null)
553 snapcontrol.setClientName(id, new_name);
554 if (new_latency != null)
555 snapcontrol.setClientLatency(id, new_latency);
556 show();
557 }
558 function openClientSettings(id) {
559 let modal = document.getElementById("client-settings");
560 let client = snapcontrol.getClient(id);
561 let current_name = (client.config.name != "") ? client.config.name : client.host.name;
562 let name = document.getElementById("client-name");
563 name.name = id;
564 name.value = current_name;
565 let latency = document.getElementById("client-latency");
566 latency.valueAsNumber = client.config.latency;
567 let group = snapcontrol.getGroupFromClient(id);
568 let group_input = document.getElementById("client-group");
569 while (group_input.length > 0)
570 group_input.remove(0);
571 let group_num = 0;
572 for (let ogroup of snapcontrol.server.groups) {
573 let option = document.createElement('option');
574 option.value = ogroup.id;
575 option.text = "Group " + (group_num + 1) + " (" + ogroup.clients.length + " Clients)";
576 group_input.add(option);
577 if (ogroup == group) {
578 console.log("Selected: " + group_num);
579 group_input.selectedIndex = group_num;
580 }
581 ++group_num;
582 }
583 let option = document.createElement('option');
584 option.value = option.text = "new";
585 group_input.add(option);
586 modal.style.display = "block";
587 }
588 function closeClientSettings() {
589 let name = document.getElementById("client-name");
590 let id = name.name;
591 console.log("onclose " + id + ", value: " + name.value);
592 snapcontrol.setClientName(id, name.value);
593 let latency = document.getElementById("client-latency");
594 snapcontrol.setClientLatency(id, latency.valueAsNumber);
595 let group_input = document.getElementById("client-group");
596 let option = group_input.options[group_input.selectedIndex];
597 setGroup(id, option.value);
598 let modal = document.getElementById("client-settings");
599 modal.style.display = "none";
600 show();
601 }
602 function deleteClient(id) {
603 if (confirm('Are you sure?')) {
604 snapcontrol.deleteClient(id);
605 }
606 }
607 window.onload = function (event) {
608 snapcontrol = new SnapControl(window.location.hostname, 1780);
609 };
610 // When the user clicks anywhere outside of the modal, close it
611 window.onclick = function (event) {
612 let modal = document.getElementById("client-settings");
613 if (event.target == modal) {
614 modal.style.display = "none";
615 }
616 };
617 //# sourceMappingURL=snapcontrol.js.map
0 "use strict";
1 function setCookie(key, value, exdays = -1) {
2 let d = new Date();
3 if (exdays < 0)
4 exdays = 10 * 365;
5 d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
6 let expires = "expires=" + d.toUTCString();
7 document.cookie = key + "=" + value + ";" + expires + ";sameSite=Strict;path=/";
8 }
9 function getPersistentValue(key, defaultValue = "") {
10 if (!!window.localStorage) {
11 const value = window.localStorage.getItem(key);
12 if (value !== null) {
13 return value;
14 }
15 window.localStorage.setItem(key, defaultValue);
16 return defaultValue;
17 }
18 // Fallback to cookies if localStorage is not available.
19 let name = key + "=";
20 let decodedCookie = decodeURIComponent(document.cookie);
21 let ca = decodedCookie.split(';');
22 for (let c of ca) {
23 c = c.trimLeft();
24 if (c.indexOf(name) == 0) {
25 return c.substring(name.length, c.length);
26 }
27 }
28 setCookie(key, defaultValue);
29 return defaultValue;
30 }
31 function getChromeVersion() {
32 const raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
33 return raw ? parseInt(raw[2]) : null;
34 }
35 function uuidv4() {
36 return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
37 var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
38 return v.toString(16);
39 });
40 }
41 class Tv {
42 constructor(sec, usec) {
43 this.sec = 0;
44 this.usec = 0;
45 this.sec = sec;
46 this.usec = usec;
47 }
48 setMilliseconds(ms) {
49 this.sec = Math.floor(ms / 1000);
50 this.usec = Math.floor(ms * 1000) % 1000000;
51 }
52 getMilliseconds() {
53 return this.sec * 1000 + this.usec / 1000;
54 }
55 }
56 class BaseMessage {
57 constructor(buffer) {
58 this.type = 0;
59 this.id = 0;
60 this.refersTo = 0;
61 this.received = new Tv(0, 0);
62 this.sent = new Tv(0, 0);
63 this.size = 0;
64 }
65 deserialize(buffer) {
66 let view = new DataView(buffer);
67 this.type = view.getUint16(0, true);
68 this.id = view.getUint16(2, true);
69 this.refersTo = view.getUint16(4, true);
70 this.received = new Tv(view.getInt32(6, true), view.getInt32(10, true));
71 this.sent = new Tv(view.getInt32(14, true), view.getInt32(18, true));
72 this.size = view.getUint32(22, true);
73 }
74 serialize() {
75 this.size = 26 + this.getSize();
76 let buffer = new ArrayBuffer(this.size);
77 let view = new DataView(buffer);
78 view.setUint16(0, this.type, true);
79 view.setUint16(2, this.id, true);
80 view.setUint16(4, this.refersTo, true);
81 view.setInt32(6, this.sent.sec, true);
82 view.setInt32(10, this.sent.usec, true);
83 view.setInt32(14, this.received.sec, true);
84 view.setInt32(18, this.received.usec, true);
85 view.setUint32(22, this.size, true);
86 return buffer;
87 }
88 getSize() {
89 return 0;
90 }
91 }
92 class CodecMessage extends BaseMessage {
93 constructor(buffer) {
94 super(buffer);
95 this.codec = "";
96 this.payload = new ArrayBuffer(0);
97 if (buffer) {
98 this.deserialize(buffer);
99 }
100 this.type = 1;
101 }
102 deserialize(buffer) {
103 super.deserialize(buffer);
104 let view = new DataView(buffer);
105 let codecSize = view.getInt32(26, true);
106 let decoder = new TextDecoder("utf-8");
107 this.codec = decoder.decode(buffer.slice(30, 30 + codecSize));
108 let payloadSize = view.getInt32(30 + codecSize, true);
109 console.log("payload size: " + payloadSize);
110 this.payload = buffer.slice(34 + codecSize, 34 + codecSize + payloadSize);
111 console.log("payload: " + this.payload);
112 }
113 }
114 class TimeMessage extends BaseMessage {
115 constructor(buffer) {
116 super(buffer);
117 this.latency = new Tv(0, 0);
118 if (buffer) {
119 this.deserialize(buffer);
120 }
121 this.type = 4;
122 }
123 deserialize(buffer) {
124 super.deserialize(buffer);
125 let view = new DataView(buffer);
126 this.latency = new Tv(view.getInt32(26, true), view.getInt32(30, true));
127 }
128 serialize() {
129 let buffer = super.serialize();
130 let view = new DataView(buffer);
131 view.setInt32(26, this.latency.sec, true);
132 view.setInt32(30, this.latency.usec, true);
133 return buffer;
134 }
135 getSize() {
136 return 8;
137 }
138 }
139 class JsonMessage extends BaseMessage {
140 constructor(buffer) {
141 super(buffer);
142 if (buffer) {
143 this.deserialize(buffer);
144 }
145 }
146 deserialize(buffer) {
147 super.deserialize(buffer);
148 let view = new DataView(buffer);
149 let size = view.getUint32(26, true);
150 let decoder = new TextDecoder();
151 this.json = JSON.parse(decoder.decode(buffer.slice(30, 30 + size)));
152 }
153 serialize() {
154 let buffer = super.serialize();
155 let view = new DataView(buffer);
156 let jsonStr = JSON.stringify(this.json);
157 view.setUint32(26, jsonStr.length, true);
158 let encoder = new TextEncoder();
159 let encoded = encoder.encode(jsonStr);
160 for (let i = 0; i < encoded.length; ++i)
161 view.setUint8(30 + i, encoded[i]);
162 return buffer;
163 }
164 getSize() {
165 let encoder = new TextEncoder();
166 let encoded = encoder.encode(JSON.stringify(this.json));
167 return encoded.length + 4;
168 // return JSON.stringify(this.json).length;
169 }
170 }
171 class HelloMessage extends JsonMessage {
172 constructor(buffer) {
173 super(buffer);
174 this.mac = "";
175 this.hostname = "";
176 this.version = "0.1.0";
177 this.clientName = "Snapweb";
178 this.os = "";
179 this.arch = "web";
180 this.instance = 1;
181 this.uniqueId = "";
182 this.snapStreamProtocolVersion = 2;
183 if (buffer) {
184 this.deserialize(buffer);
185 }
186 this.type = 5;
187 }
188 deserialize(buffer) {
189 super.deserialize(buffer);
190 this.mac = this.json["MAC"];
191 this.hostname = this.json["HostName"];
192 this.version = this.json["Version"];
193 this.clientName = this.json["ClientName"];
194 this.os = this.json["OS"];
195 this.arch = this.json["Arch"];
196 this.instance = this.json["Instance"];
197 this.uniqueId = this.json["ID"];
198 this.snapStreamProtocolVersion = this.json["SnapStreamProtocolVersion"];
199 }
200 serialize() {
201 this.json = { "MAC": this.mac, "HostName": this.hostname, "Version": this.version, "ClientName": this.clientName, "OS": this.os, "Arch": this.arch, "Instance": this.instance, "ID": this.uniqueId, "SnapStreamProtocolVersion": this.snapStreamProtocolVersion };
202 return super.serialize();
203 }
204 }
205 class ServerSettingsMessage extends JsonMessage {
206 constructor(buffer) {
207 super(buffer);
208 this.bufferMs = 0;
209 this.latency = 0;
210 this.volumePercent = 0;
211 this.muted = false;
212 if (buffer) {
213 this.deserialize(buffer);
214 }
215 this.type = 3;
216 }
217 deserialize(buffer) {
218 super.deserialize(buffer);
219 this.bufferMs = this.json["bufferMs"];
220 this.latency = this.json["latency"];
221 this.volumePercent = this.json["volume"];
222 this.muted = this.json["muted"];
223 }
224 serialize() {
225 this.json = { "bufferMs": this.bufferMs, "latency": this.latency, "volume": this.volumePercent, "muted": this.muted };
226 return super.serialize();
227 }
228 }
229 class PcmChunkMessage extends BaseMessage {
230 constructor(buffer, sampleFormat) {
231 super(buffer);
232 this.timestamp = new Tv(0, 0);
233 // payloadSize: number = 0;
234 this.payload = new ArrayBuffer(0);
235 this.idx = 0;
236 this.deserialize(buffer);
237 this.sampleFormat = sampleFormat;
238 this.type = 2;
239 }
240 deserialize(buffer) {
241 super.deserialize(buffer);
242 let view = new DataView(buffer);
243 this.timestamp = new Tv(view.getInt32(26, true), view.getInt32(30, true));
244 // this.payloadSize = view.getUint32(34, true);
245 this.payload = buffer.slice(38); //, this.payloadSize + 38));// , this.payloadSize);
246 // console.log("ts: " + this.timestamp.sec + " " + this.timestamp.usec + ", payload: " + this.payloadSize + ", len: " + this.payload.byteLength);
247 }
248 readFrames(frames) {
249 let frameCnt = frames;
250 let frameSize = this.sampleFormat.frameSize();
251 if (this.idx + frames > this.payloadSize() / frameSize)
252 frameCnt = (this.payloadSize() / frameSize) - this.idx;
253 let begin = this.idx * frameSize;
254 this.idx += frameCnt;
255 let end = begin + frameCnt * frameSize;
256 // console.log("readFrames: " + frames + ", result: " + frameCnt + ", begin: " + begin + ", end: " + end + ", payload: " + this.payload.byteLength);
257 return this.payload.slice(begin, end);
258 }
259 getFrameCount() {
260 return (this.payloadSize() / this.sampleFormat.frameSize());
261 }
262 isEndOfChunk() {
263 return this.idx >= this.getFrameCount();
264 }
265 startMs() {
266 return this.timestamp.getMilliseconds() + 1000 * (this.idx / this.sampleFormat.rate);
267 }
268 duration() {
269 return 1000 * ((this.getFrameCount() - this.idx) / this.sampleFormat.rate);
270 }
271 payloadSize() {
272 return this.payload.byteLength;
273 }
274 clearPayload() {
275 this.payload = new ArrayBuffer(0);
276 }
277 addPayload(buffer) {
278 let payload = new ArrayBuffer(this.payload.byteLength + buffer.byteLength);
279 let view = new DataView(payload);
280 let viewOld = new DataView(this.payload);
281 let viewNew = new DataView(buffer);
282 for (let i = 0; i < viewOld.byteLength; ++i) {
283 view.setInt8(i, viewOld.getInt8(i));
284 }
285 for (let i = 0; i < viewNew.byteLength; ++i) {
286 view.setInt8(i + viewOld.byteLength, viewNew.getInt8(i));
287 }
288 this.payload = payload;
289 }
290 }
291 class AudioStream {
292 constructor(timeProvider, sampleFormat, bufferMs) {
293 this.timeProvider = timeProvider;
294 this.sampleFormat = sampleFormat;
295 this.bufferMs = bufferMs;
296 this.chunks = new Array();
297 // setRealSampleRate(sampleRate: number) {
298 // if (sampleRate == this.sampleFormat.rate) {
299 // this.correctAfterXFrames = 0;
300 // }
301 // else {
302 // this.correctAfterXFrames = Math.ceil((this.sampleFormat.rate / sampleRate) / (this.sampleFormat.rate / sampleRate - 1.));
303 // console.debug("setRealSampleRate: " + sampleRate + ", correct after X: " + this.correctAfterXFrames);
304 // }
305 // }
306 this.chunk = undefined;
307 this.volume = 1;
308 this.muted = false;
309 this.lastLog = 0;
310 }
311 setVolume(percent, muted) {
312 let base = 10;
313 this.volume = percent / 100; // (Math.pow(base, percent / 100) - 1) / (base - 1);
314 console.log("setVolume: " + percent + " => " + this.volume + ", muted: " + this.muted);
315 this.muted = muted;
316 }
317 addChunk(chunk) {
318 this.chunks.push(chunk);
319 // let oldest = this.timeProvider.serverNow() - this.chunks[0].timestamp.getMilliseconds();
320 // let newest = this.timeProvider.serverNow() - this.chunks[this.chunks.length - 1].timestamp.getMilliseconds();
321 // console.debug("chunks: " + this.chunks.length + ", oldest: " + oldest.toFixed(2) + ", newest: " + newest.toFixed(2));
322 while (this.chunks.length > 0) {
323 let age = this.timeProvider.serverNow() - this.chunks[0].timestamp.getMilliseconds();
324 // todo: consider buffer ms
325 if (age > 5000 + this.bufferMs) {
326 this.chunks.shift();
327 console.log("Dropping old chunk: " + age.toFixed(2) + ", left: " + this.chunks.length);
328 }
329 else
330 break;
331 }
332 }
333 getNextBuffer(buffer, playTimeMs) {
334 if (!this.chunk) {
335 this.chunk = this.chunks.shift();
336 }
337 // let age = this.timeProvider.serverTime(this.playTime * 1000) - startMs;
338 let frames = buffer.length;
339 // console.debug("getNextBuffer: " + frames + ", play time: " + playTimeMs.toFixed(2));
340 let left = new Float32Array(frames);
341 let right = new Float32Array(frames);
342 let read = 0;
343 let pos = 0;
344 // let volume = this.muted ? 0 : this.volume;
345 let serverPlayTimeMs = this.timeProvider.serverTime(playTimeMs);
346 if (this.chunk) {
347 let age = serverPlayTimeMs - this.chunk.startMs(); // - 500;
348 let reqChunkDuration = frames / this.sampleFormat.msRate();
349 let secs = Math.floor(Date.now() / 1000);
350 if (this.lastLog != secs) {
351 this.lastLog = secs;
352 console.log("age: " + age.toFixed(2) + ", req: " + reqChunkDuration);
353 }
354 if (age < -reqChunkDuration) {
355 console.log("age: " + age.toFixed(2) + " < req: " + reqChunkDuration * -1 + ", chunk.startMs: " + this.chunk.startMs().toFixed(2) + ", timestamp: " + this.chunk.timestamp.getMilliseconds().toFixed(2));
356 console.log("Chunk too young, returning silence");
357 }
358 else {
359 if (Math.abs(age) > 5) {
360 // We are 5ms apart, do a hard sync, i.e. don't play faster/slower,
361 // but seek to the desired position instead
362 while (this.chunk && age > this.chunk.duration()) {
363 console.log("Chunk too old, dropping (age: " + age.toFixed(2) + " > " + this.chunk.duration().toFixed(2) + ")");
364 this.chunk = this.chunks.shift();
365 if (!this.chunk)
366 break;
367 age = serverPlayTimeMs - this.chunk.startMs();
368 }
369 if (this.chunk) {
370 if (age > 0) {
371 console.log("Fast forwarding " + age.toFixed(2) + "ms");
372 this.chunk.readFrames(Math.floor(age * this.chunk.sampleFormat.msRate()));
373 }
374 else if (age < 0) {
375 console.log("Playing silence " + -age.toFixed(2) + "ms");
376 let silentFrames = Math.floor(-age * this.chunk.sampleFormat.msRate());
377 left.fill(0, 0, silentFrames);
378 right.fill(0, 0, silentFrames);
379 read = silentFrames;
380 pos = silentFrames;
381 }
382 age = 0;
383 }
384 }
385 // else if (age > 0.1) {
386 // let rate = age * 0.0005;
387 // rate = 1.0 - Math.min(rate, 0.0005);
388 // console.debug("Age > 0, rate: " + rate);
389 // // we are late (age > 0), this means we are not playing fast enough
390 // // => the real sample rate seems to be lower, we have to drop some frames
391 // this.setRealSampleRate(this.sampleFormat.rate * rate); // 0.9999);
392 // }
393 // else if (age < -0.1) {
394 // let rate = -age * 0.0005;
395 // rate = 1.0 + Math.min(rate, 0.0005);
396 // console.debug("Age < 0, rate: " + rate);
397 // // we are early (age > 0), this means we are playing too fast
398 // // => the real sample rate seems to be higher, we have to insert some frames
399 // this.setRealSampleRate(this.sampleFormat.rate * rate); // 0.9999);
400 // }
401 // else {
402 // this.setRealSampleRate(this.sampleFormat.rate);
403 // }
404 let addFrames = 0;
405 let everyN = 0;
406 if (age > 0.1) {
407 addFrames = Math.ceil(age); // / 5);
408 }
409 else if (age < -0.1) {
410 addFrames = Math.floor(age); // / 5);
411 }
412 // addFrames = -2;
413 let readFrames = frames + addFrames - read;
414 if (addFrames != 0)
415 everyN = Math.ceil((frames + addFrames - read) / (Math.abs(addFrames) + 1));
416 // addFrames = 0;
417 // console.debug("frames: " + frames + ", readFrames: " + readFrames + ", addFrames: " + addFrames + ", everyN: " + everyN);
418 while ((read < readFrames) && this.chunk) {
419 let pcmChunk = this.chunk;
420 let pcmBuffer = pcmChunk.readFrames(readFrames - read);
421 let payload = new Int16Array(pcmBuffer);
422 // console.debug("readFrames: " + (frames - read) + ", read: " + pcmBuffer.byteLength + ", payload: " + payload.length);
423 // read += (pcmBuffer.byteLength / this.sampleFormat.frameSize());
424 for (let i = 0; i < payload.length; i += 2) {
425 read++;
426 left[pos] = (payload[i] / 32768); // * volume;
427 right[pos] = (payload[i + 1] / 32768); // * volume;
428 if ((everyN != 0) && (read % everyN == 0)) {
429 if (addFrames > 0) {
430 pos--;
431 }
432 else {
433 left[pos + 1] = left[pos];
434 right[pos + 1] = right[pos];
435 pos++;
436 // console.log("Add: " + pos);
437 }
438 }
439 pos++;
440 }
441 if (pcmChunk.isEndOfChunk()) {
442 this.chunk = this.chunks.shift();
443 }
444 }
445 if (addFrames != 0)
446 console.debug("Pos: " + pos + ", frames: " + frames + ", add: " + addFrames + ", everyN: " + everyN);
447 if (read == readFrames)
448 read = frames;
449 }
450 }
451 if (read < frames) {
452 console.log("Failed to get chunk, read: " + read + "/" + frames + ", chunks left: " + this.chunks.length);
453 left.fill(0, pos);
454 right.fill(0, pos);
455 }
456 buffer.copyToChannel(left, 0, 0);
457 buffer.copyToChannel(right, 1, 0);
458 }
459 }
460 class TimeProvider {
461 constructor(ctx = undefined) {
462 this.diffBuffer = new Array();
463 this.diff = 0;
464 if (ctx) {
465 this.setAudioContext(ctx);
466 }
467 }
468 setAudioContext(ctx) {
469 this.ctx = ctx;
470 this.reset();
471 }
472 reset() {
473 this.diffBuffer.length = 0;
474 this.diff = 0;
475 }
476 setDiff(c2s, s2c) {
477 if (this.now() == 0) {
478 this.reset();
479 }
480 else {
481 if (this.diffBuffer.push((c2s - s2c) / 2) > 100)
482 this.diffBuffer.shift();
483 let sorted = [...this.diffBuffer];
484 sorted.sort();
485 this.diff = sorted[Math.floor(sorted.length / 2)];
486 }
487 // console.debug("c2s: " + c2s.toFixed(2) + ", s2c: " + s2c.toFixed(2) + ", diff: " + this.diff.toFixed(2) + ", now: " + this.now().toFixed(2) + ", server.now: " + this.serverNow().toFixed(2) + ", win.now: " + window.performance.now().toFixed(2));
488 // console.log("now: " + this.now() + "\t" + this.now() + "\t" + this.now());
489 }
490 now() {
491 if (!this.ctx) {
492 return window.performance.now();
493 }
494 else {
495 // Use the more accurate getOutputTimestamp if available, fallback to ctx.currentTime otherwise.
496 const contextTime = !!this.ctx.getOutputTimestamp ? this.ctx.getOutputTimestamp().contextTime : undefined;
497 return (contextTime !== undefined ? contextTime : this.ctx.currentTime) * 1000;
498 }
499 }
500 nowSec() {
501 return this.now() / 1000;
502 }
503 serverNow() {
504 return this.serverTime(this.now());
505 }
506 serverTime(localTimeMs) {
507 return localTimeMs + this.diff;
508 }
509 }
510 class SampleFormat {
511 constructor() {
512 this.rate = 48000;
513 this.channels = 2;
514 this.bits = 16;
515 }
516 msRate() {
517 return this.rate / 1000;
518 }
519 toString() {
520 return this.rate + ":" + this.bits + ":" + this.channels;
521 }
522 sampleSize() {
523 if (this.bits == 24) {
524 return 4;
525 }
526 return this.bits / 8;
527 }
528 frameSize() {
529 return this.channels * this.sampleSize();
530 }
531 durationMs(bytes) {
532 return (bytes / this.frameSize()) * this.msRate();
533 }
534 }
535 class Decoder {
536 setHeader(buffer) {
537 return new SampleFormat();
538 }
539 decode(chunk) {
540 return null;
541 }
542 }
543 class OpusDecoder extends Decoder {
544 constructor() {
545 super();
546 }
547 setHeader(buffer) {
548 let view = new DataView(buffer);
549 let ID_OPUS = 0x4F505553;
550 if (buffer.byteLength < 12) {
551 console.error("Opus header too small: " + buffer.byteLength);
552 return null;
553 }
554 else if (view.getUint32(0, true) != ID_OPUS) {
555 console.error("Opus header too small: " + buffer.byteLength);
556 return null;
557 }
558 let format = new SampleFormat();
559 format.rate = view.getUint32(4, true);
560 format.bits = view.getUint16(8, true);
561 format.channels = view.getUint16(10, true);
562 console.log("Opus samplerate: " + format.toString());
563 return format;
564 }
565 decode(chunk) {
566 return null;
567 }
568 }
569 class FlacDecoder extends Decoder {
570 constructor() {
571 super();
572 this.header = null;
573 this.cacheInfo = { isCachedChunk: false, cachedBlocks: 0 };
574 this.decoder = Flac.create_libflac_decoder(true);
575 if (this.decoder) {
576 let init_status = Flac.init_decoder_stream(this.decoder, this.read_callback_fn.bind(this), this.write_callback_fn.bind(this), this.error_callback_fn.bind(this), this.metadata_callback_fn.bind(this), false);
577 console.log("Flac init: " + init_status);
578 Flac.setOptions(this.decoder, { analyseSubframes: true, analyseResiduals: true });
579 }
580 this.sampleFormat = new SampleFormat();
581 this.flacChunk = new ArrayBuffer(0);
582 // this.pcmChunk = new PcmChunkMessage();
583 // Flac.setOptions(this.decoder, {analyseSubframes: analyse_frames, analyseResiduals: analyse_residuals});
584 // flac_ok &= init_status == 0;
585 // console.log("flac init : " + flac_ok);//DEBUG
586 }
587 decode(chunk) {
588 // console.log("Flac decode: " + chunk.payload.byteLength);
589 this.flacChunk = chunk.payload.slice(0);
590 this.pcmChunk = chunk;
591 this.pcmChunk.clearPayload();
592 this.cacheInfo = { cachedBlocks: 0, isCachedChunk: true };
593 // console.log("Flac len: " + this.flacChunk.byteLength);
594 while (this.flacChunk.byteLength && Flac.FLAC__stream_decoder_process_single(this.decoder)) {
595 let state = Flac.FLAC__stream_decoder_get_state(this.decoder);
596 // console.log("State: " + state);
597 }
598 // console.log("Pcm payload: " + this.pcmChunk!.payloadSize());
599 if (this.cacheInfo.cachedBlocks > 0) {
600 let diffMs = this.cacheInfo.cachedBlocks / this.sampleFormat.msRate();
601 // console.log("Cached: " + this.cacheInfo.cachedBlocks + ", " + diffMs + "ms");
602 this.pcmChunk.timestamp.setMilliseconds(this.pcmChunk.timestamp.getMilliseconds() - diffMs);
603 }
604 return this.pcmChunk;
605 }
606 read_callback_fn(bufferSize) {
607 // console.log(' decode read callback, buffer bytes max=', bufferSize);
608 if (this.header) {
609 console.log(" header: " + this.header.byteLength);
610 let data = new Uint8Array(this.header);
611 this.header = null;
612 return { buffer: data, readDataLength: data.byteLength, error: false };
613 }
614 else if (this.flacChunk) {
615 // console.log(" flacChunk: " + this.flacChunk.byteLength);
616 // a fresh read => next call to write will not be from cached data
617 this.cacheInfo.isCachedChunk = false;
618 let data = new Uint8Array(this.flacChunk.slice(0, Math.min(bufferSize, this.flacChunk.byteLength)));
619 this.flacChunk = this.flacChunk.slice(data.byteLength);
620 return { buffer: data, readDataLength: data.byteLength, error: false };
621 }
622 return { buffer: new Uint8Array(0), readDataLength: 0, error: false };
623 }
624 write_callback_fn(data, frameInfo) {
625 // console.log(" write frame metadata: " + frameInfo + ", len: " + data.length);
626 if (this.cacheInfo.isCachedChunk) {
627 // there was no call to read, so it's some cached data
628 this.cacheInfo.cachedBlocks += frameInfo.blocksize;
629 }
630 let payload = new ArrayBuffer((frameInfo.bitsPerSample / 8) * frameInfo.channels * frameInfo.blocksize);
631 let view = new DataView(payload);
632 for (let channel = 0; channel < frameInfo.channels; ++channel) {
633 let channelData = new DataView(data[channel].buffer, 0, data[channel].buffer.byteLength);
634 // console.log("channelData: " + channelData.byteLength + ", blocksize: " + frameInfo.blocksize);
635 for (let i = 0; i < frameInfo.blocksize; ++i) {
636 view.setInt16(2 * (frameInfo.channels * i + channel), channelData.getInt16(2 * i, true), true);
637 }
638 }
639 this.pcmChunk.addPayload(payload);
640 // console.log("write: " + payload.byteLength + ", len: " + this.pcmChunk!.payloadSize());
641 }
642 /** @memberOf decode */
643 metadata_callback_fn(data) {
644 console.info('meta data: ', data);
645 // let view = new DataView(data);
646 this.sampleFormat.rate = data.sampleRate;
647 this.sampleFormat.channels = data.channels;
648 this.sampleFormat.bits = data.bitsPerSample;
649 console.log("metadata_callback_fn, sampleformat: " + this.sampleFormat.toString());
650 }
651 /** @memberOf decode */
652 error_callback_fn(err, errMsg) {
653 console.error('decode error callback', err, errMsg);
654 }
655 setHeader(buffer) {
656 this.header = buffer.slice(0);
657 Flac.FLAC__stream_decoder_process_until_end_of_metadata(this.decoder);
658 return this.sampleFormat;
659 }
660 }
661 class PlayBuffer {
662 constructor(buffer, playTime, source, destination) {
663 this.num = 0;
664 this.buffer = buffer;
665 this.playTime = playTime;
666 this.source = source;
667 this.source.buffer = this.buffer;
668 this.source.connect(destination);
669 this.onended = (playBuffer) => { };
670 }
671 start() {
672 this.source.onended = (ev) => {
673 this.onended(this);
674 };
675 this.source.start(this.playTime);
676 }
677 }
678 class PcmDecoder extends Decoder {
679 setHeader(buffer) {
680 let sampleFormat = new SampleFormat();
681 let view = new DataView(buffer);
682 sampleFormat.channels = view.getUint16(22, true);
683 sampleFormat.rate = view.getUint32(24, true);
684 sampleFormat.bits = view.getUint16(34, true);
685 return sampleFormat;
686 }
687 decode(chunk) {
688 return chunk;
689 }
690 }
691 class SnapStream {
692 constructor(host, port) {
693 this.playTime = 0;
694 this.msgId = 0;
695 this.bufferDurationMs = 80; // 0;
696 this.bufferFrameCount = 3844; // 9600; // 2400;//8192;
697 this.syncHandle = -1;
698 // ageBuffer: Array<number>;
699 this.audioBuffers = new Array();
700 this.freeBuffers = new Array();
701 // median: number = 0;
702 this.audioBufferCount = 3;
703 this.bufferMs = 1000;
704 this.bufferNum = 0;
705 this.streamsocket = new WebSocket('ws://' + host + ':' + port + '/stream');
706 this.streamsocket.binaryType = "arraybuffer";
707 this.streamsocket.onmessage = (msg) => {
708 let view = new DataView(msg.data);
709 let type = view.getUint16(0, true);
710 if (type == 1) {
711 let codec = new CodecMessage(msg.data);
712 console.log("Codec: " + codec.codec);
713 if (codec.codec == "flac") {
714 this.decoder = new FlacDecoder();
715 }
716 else if (codec.codec == "pcm") {
717 this.decoder = new PcmDecoder();
718 }
719 else if (codec.codec == "opus") {
720 this.decoder = new OpusDecoder();
721 alert("Codec not supported: " + codec.codec);
722 }
723 else {
724 alert("Codec not supported: " + codec.codec);
725 }
726 if (this.decoder) {
727 this.sampleFormat = this.decoder.setHeader(codec.payload);
728 console.log("Sampleformat: " + this.sampleFormat.toString());
729 if ((this.sampleFormat.channels != 2) || (this.sampleFormat.bits != 16)) {
730 alert("Stream must be stereo with 16 bit depth, actual format: " + this.sampleFormat.toString());
731 }
732 else {
733 if (this.bufferDurationMs != 0) {
734 this.bufferFrameCount = Math.floor(this.bufferDurationMs * this.sampleFormat.msRate());
735 }
736 this.stopAudio();
737 let options = { latencyHint: "playback", sampleRate: this.sampleFormat.rate };
738 const chromeVersion = getChromeVersion();
739 if (chromeVersion !== null && chromeVersion < 55) {
740 // Some older browsers won't decode the stream if options are provided.
741 options = undefined;
742 }
743 this.ctx = new AudioContext(options);
744 this.timeProvider.setAudioContext(this.ctx);
745 this.gainNode = this.ctx.createGain();
746 this.gainNode.connect(this.ctx.destination);
747 this.gainNode.gain.value = this.serverSettings.muted ? 0 : this.serverSettings.volumePercent / 100;
748 // this.timeProvider = new TimeProvider(this.ctx);
749 this.stream = new AudioStream(this.timeProvider, this.sampleFormat, this.bufferMs);
750 console.log("Base latency: " + this.ctx.baseLatency + ", output latency: " + this.ctx.outputLatency);
751 this.play();
752 }
753 }
754 }
755 else if (type == 2) {
756 let pcmChunk = new PcmChunkMessage(msg.data, this.sampleFormat);
757 if (this.decoder) {
758 let decoded = this.decoder.decode(pcmChunk);
759 if (decoded) {
760 this.stream.addChunk(decoded);
761 }
762 }
763 }
764 else if (type == 3) {
765 this.serverSettings = new ServerSettingsMessage(msg.data);
766 if (this.gainNode) {
767 this.gainNode.gain.value = this.serverSettings.muted ? 0 : this.serverSettings.volumePercent / 100;
768 }
769 this.bufferMs = this.serverSettings.bufferMs - this.serverSettings.latency;
770 console.log("ServerSettings bufferMs: " + this.serverSettings.bufferMs + ", latency: " + this.serverSettings.latency + ", volume: " + this.serverSettings.volumePercent + ", muted: " + this.serverSettings.muted);
771 }
772 else if (type == 4) {
773 if (this.timeProvider) {
774 let time = new TimeMessage(msg.data);
775 this.timeProvider.setDiff(time.latency.getMilliseconds(), this.timeProvider.now() - time.sent.getMilliseconds());
776 }
777 // console.log("Time sec: " + time.latency.sec + ", usec: " + time.latency.usec + ", diff: " + this.timeProvider.diff);
778 }
779 else {
780 console.info("Message not handled, type: " + type);
781 }
782 };
783 this.streamsocket.onopen = (ev) => {
784 console.log("on open");
785 let hello = new HelloMessage();
786 hello.mac = "00:00:00:00:00:00";
787 hello.arch = "web";
788 hello.os = navigator.platform;
789 hello.hostname = "Snapweb client";
790 hello.uniqueId = getPersistentValue("uniqueId", uuidv4());
791 this.sendMessage(hello);
792 this.syncTime();
793 this.syncHandle = window.setInterval(() => this.syncTime(), 1000);
794 };
795 this.streamsocket.onerror = (ev) => { alert("error: " + ev.type); }; //this.onError(ev);
796 this.streamsocket.onclose = (ev) => {
797 stop();
798 };
799 // this.ageBuffer = new Array<number>();
800 this.timeProvider = new TimeProvider();
801 }
802 sendMessage(msg) {
803 msg.sent = new Tv(0, 0);
804 msg.sent.setMilliseconds(this.timeProvider.now());
805 msg.id = ++this.msgId;
806 if (this.streamsocket.readyState != this.streamsocket.OPEN) {
807 stop();
808 }
809 else {
810 this.streamsocket.send(msg.serialize());
811 }
812 }
813 syncTime() {
814 let t = new TimeMessage();
815 t.latency.setMilliseconds(this.timeProvider.now());
816 this.sendMessage(t);
817 // console.log("prepareSource median: " + Math.round(this.median * 10) / 10);
818 }
819 stopAudio() {
820 if (this.ctx) {
821 this.ctx.close();
822 }
823 while (this.audioBuffers.length > 0) {
824 let buffer = this.audioBuffers.pop();
825 buffer.onended = (playBuffer) => { };
826 buffer.source.stop();
827 }
828 while (this.freeBuffers.length > 0) {
829 this.freeBuffers.pop();
830 }
831 }
832 stop() {
833 window.clearInterval(this.syncHandle);
834 this.stopAudio();
835 if ([WebSocket.OPEN, WebSocket.CONNECTING].includes(this.streamsocket.readyState)) {
836 this.streamsocket.close();
837 }
838 }
839 play() {
840 this.playTime = this.timeProvider.nowSec() + 0.1;
841 for (let i = 1; i <= this.audioBufferCount; ++i) {
842 this.playNext();
843 }
844 }
845 playNext() {
846 let buffer = this.freeBuffers.pop() || this.ctx.createBuffer(this.sampleFormat.channels, this.bufferFrameCount, this.sampleFormat.rate);
847 let playTimeMs = (this.playTime + this.ctx.baseLatency) * 1000 - this.bufferMs;
848 this.stream.getNextBuffer(buffer, playTimeMs);
849 let source = this.ctx.createBufferSource();
850 let playBuffer = new PlayBuffer(buffer, this.playTime, source, this.gainNode);
851 this.audioBuffers.push(playBuffer);
852 playBuffer.num = ++this.bufferNum;
853 playBuffer.onended = (buffer) => {
854 let diff = this.timeProvider.nowSec() - buffer.playTime;
855 this.freeBuffers.push(this.audioBuffers.splice(this.audioBuffers.indexOf(buffer), 1)[0].buffer);
856 // console.debug("PlayBuffer " + playBuffer.num + " ended after: " + (diff * 1000) + ", in flight: " + this.audioBuffers.length);
857 this.playNext();
858 };
859 playBuffer.start();
860 this.playTime += this.bufferFrameCount / this.sampleFormat.rate;
861 }
862 }
863 //# sourceMappingURL=snapstream.js.map
0 body {
1 background-color: rgb(246, 246, 246);
2 color: rgb(255, 255, 255);
3 font-family: 'Arial', sans-serif;
4 width: 100%;
5 margin: 0;
6 font-size: 20px;
7 overscroll-behavior: contain;
8 }
9
10 /* width */
11 ::-webkit-scrollbar {
12 width: 10px;
13 }
14
15 /* Track */
16 ::-webkit-scrollbar-track {
17 background: #1f1f1f;
18 }
19
20 /* Handle */
21 ::-webkit-scrollbar-thumb {
22 background: #333;
23 }
24
25 /* Handle on hover */
26 ::-webkit-scrollbar-thumb:hover {
27 background: #555;
28 }
29
30 .navbar {
31 overflow: hidden;
32 background-color: #607d8b;
33 z-index: 1; /* Sit on top */
34 padding: 13px;
35 color: white;
36 position: fixed; /* Set the navbar to fixed position */
37 top: 0; /* Position the navbar at the top of the page */
38 width: 100%; /* Full width */
39 font-size: 21px;
40 font-weight: 500;
41 user-select: none;
42 }
43
44 .play-button {
45 display: block;
46 position: absolute;
47 right: 34px;
48 top: 5px;
49 height: 40px;
50 width: 40px;
51 }
52
53 .content {
54 margin-top: 62px
55 }
56
57 .group {
58 float: none;
59 background-color: white;
60 box-shadow: 0px 3px 5px 0px rgba(0,0,0,0.2);
61 clear: both;
62 padding: 8px;
63 margin: 10px 15px 10px 15px;
64 overflow: auto;
65 width: auto;
66 border-radius: 3px;
67 user-select: none;
68 }
69
70 .group.muted {
71 opacity: 0.27;
72 }
73
74 .groupheader {
75 /* margin: 10px; */
76 width: auto;
77 height: fit-content;
78 /* padding: 10px; */
79 padding-bottom: 0px;
80 display: grid;
81 grid-template-columns: min-content auto min-content;
82 grid-template-rows: min-content min-content;
83 grid-gap: 0px;
84 }
85
86 .groupheader-separator {
87 height: 1px;
88 margin: 8px 0px;
89 border-width: 0px;
90 color: lightgray;
91 background-color: lightgray;
92 }
93
94 .stream {
95 color: #686868;
96 grid-row: 1;
97 grid-column: 1/3;
98 width: fit-content;
99 }
100
101 select {
102 background-color: transparent;
103 border: 0px;
104 width: 150px;
105 font-size: 20px;
106 color: #e3e3e3;
107 -moz-appearance: none;
108 -webkit-appearance: none;
109 appearance: none;
110 }
111
112 .slidergroupdiv {
113 /* background: greenyellow; */
114 display: flex;
115 justify-content: center;
116 align-items: center;
117 grid-row: 2;
118 grid-column: 2;
119 }
120
121 .client {
122 /* text-align: left; */
123 /* margin: 10px; */
124 width: auto;
125 height: fit-content;
126 /* padding: 10px; */
127 display: grid;
128 grid-template-columns: min-content auto min-content;
129 grid-template-rows: min-content min-content;
130 grid-gap: 0px;
131 }
132
133 /* .client:hover {
134 box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
135 } */
136
137 .client.disconnected {
138 opacity: 0.27;
139 }
140
141 .name {
142 color: #686868;
143 user-select: none;
144 /* background: red; */
145 padding-top: 5px;
146 grid-row: 1;
147 grid-column: 1/3;
148 text-decoration: none;
149 }
150
151 .editdiv {
152 background: violet;
153 grid-row: 0/4;
154 grid-column: 3;
155 }
156
157 .edit-icon {
158 color: #686868;
159 text-decoration: none;
160 }
161
162 .delete-icon {
163 color: #ff4081;
164 text-decoration: none;
165 }
166
167 .edit-icons {
168 align-items: center;
169 display: flex;
170 grid-row: 1/3;
171 grid-column: 3;
172 }
173
174 .edit-group-icon {
175 display: flex;
176 color: transparent;
177 align-items: center;
178 grid-row: 1/3;
179 grid-column: 3;
180 text-decoration: none;
181 }
182
183 .mute-button {
184 color: #686868;
185 grid-row: 2;
186 grid-column: 1;
187 height: 25px;
188 width: 25px;
189 padding-left: 10px;
190 padding-right: 10px;
191 text-decoration: none;
192 }
193
194 .sliderdiv {
195 display: flex;
196 justify-content: center;
197 align-items: center;
198 grid-row: 2;
199 grid-column: 2;
200 /* padding-left: 40px; */
201 /* display: inline-block;
202 text-align: left;
203 width: 250px; */
204 }
205
206 .slider {
207 writing-mode: bt-lr;
208 -webkit-appearance: none;
209 background: #dbdbdb;
210 outline: none;
211 -webkit-transition: .2s;
212 transition: opacity .2s;
213 height: 2px;
214 width: 90%;
215 }
216
217 .slider::-moz-range-track {
218 padding: 6px;
219 background-color: transparent;
220 border: none;
221 }
222
223 .slider::-webkit-slider-thumb {
224 -webkit-appearance: none;
225 appearance: none;
226 height: 12px;
227 width: 12px;
228 border-radius: 50%;
229 background: #ff4081;
230 cursor: pointer;
231 }
232
233 .slider::-moz-range-thumb {
234 height: 12px;
235 width: 12px;
236 border-radius: 50%;
237 background: #ff4081;
238 cursor: pointer;
239 }
240
241 .slider.muted {
242 opacity: 0.27;
243 }
244
245 .client-settings {
246 display: none; /* Hidden by default */
247 position: fixed; /* Stay in place */
248 z-index: 1; /* Sit on top */
249 left: 0;
250 top: 0;
251 width: 100%; /* Full width */
252 height: 100%; /* Full height */
253 overflow: auto; /* Enable scroll if needed */
254 background-color: rgb(0,0,0); /* Fallback color */
255 background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
256 }
257
258 .client-setting-content {
259 background-color: #fefefe;
260 color: #686868;
261 margin: 15% auto; /* 15% from the top and centered */
262 padding: 20px;
263 border: 1px solid #888;
264 width: 80%; /* Could be more or less, depending on screen size */
265 }
266
267 .client-input {
268 color: #686868;
269 width: 100%;
270 padding: 12px 20px;
271 margin: 8px 0;
272 display: block;
273 border: 1px solid #ccc;
274 border-radius: 4px;
275 box-sizing: border-box;
276 }
277
278 input[type=submit] {
279 width: 100%;
280 background-color: #4CAF50;
281 color: white;
282 padding: 14px 20px;
283 margin: 8px 0;
284 border: none;
285 border-radius: 4px;
286 cursor: pointer;
287 }
288
289 input[type=submit]:hover {
290 background-color: #45a049;
291 }
292
293 div.container {
294 border-radius: 5px;
295 background-color: #f2f2f2;
296 padding: 20px;
297 }
298
3434
3535 #if defined(HAS_AVAHI)
3636 #include "publish_avahi.hpp"
37 typedef PublishAvahi PublishZeroConf;
37 using PublishZeroConf = PublishAvahi;
3838 #elif defined(HAS_BONJOUR)
3939 #include "publish_bonjour.hpp"
40 typedef PublishBonjour PublishZeroConf;
40 using PublishZeroConf = PublishBonjour;
4141 #endif
4242
4343 #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 "server.hpp"
19 #include "common/aixlog.hpp"
20 #include "config.hpp"
21 #include "message/client_info.hpp"
22 #include "message/hello.hpp"
23 #include "message/stream_tags.hpp"
24 #include "message/time.hpp"
25 #include "stream_session_tcp.hpp"
26 #include <iostream>
27
28 using namespace std;
29 using namespace streamreader;
30
31 using json = nlohmann::json;
32
33 static constexpr auto LOG_TAG = "Server";
34
35 Server::Server(boost::asio::io_context& io_context, const ServerSettings& serverSettings)
36 : io_context_(io_context), config_timer_(io_context), settings_(serverSettings)
37 {
38 }
39
40
41 Server::~Server() = default;
42
43
44 void Server::onNewSession(const std::shared_ptr<StreamSession>& session)
45 {
46 LOG(INFO, LOG_TAG) << "onNewSession\n";
47 streamServer_->addSession(session);
48 }
49
50
51 void Server::onMetaChanged(const PcmStream* pcmStream)
52 {
53 // clang-format off
54 // Notification: {"jsonrpc":"2.0","method":"Stream.OnMetadata","params":{"id":"stream 1", "meta": {"album": "some album", "artist": "some artist", "track": "some track"...}}
55 // clang-format on
56
57 const auto meta = pcmStream->getMeta();
58 LOG(DEBUG, LOG_TAG) << "metadata = " << meta->msg.dump(3) << "\n";
59 LOG(INFO, LOG_TAG) << "onMetaChanged (" << pcmStream->getName() << ")\n";
60
61 streamServer_->onMetaChanged(pcmStream, meta);
62
63 // Send meta to all connected clients
64 json notification = jsonrpcpp::Notification("Stream.OnMetadata", jsonrpcpp::Parameter("id", pcmStream->getId(), "meta", meta->msg)).to_json();
65 controlServer_->send(notification.dump(), nullptr);
66 // cout << "Notification: " << notification.dump() << "\n";
67 }
68
69
70 void Server::onStateChanged(const PcmStream* pcmStream, ReaderState state)
71 {
72 // clang-format off
73 // 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"}}}}
74 // clang-format on
75 LOG(INFO, LOG_TAG) << "onStateChanged (" << pcmStream->getName() << "): " << static_cast<int>(state) << "\n";
76 // LOG(INFO, LOG_TAG) << pcmStream->toJson().dump(4);
77 json notification = jsonrpcpp::Notification("Stream.OnUpdate", jsonrpcpp::Parameter("id", pcmStream->getId(), "stream", pcmStream->toJson())).to_json();
78 controlServer_->send(notification.dump(), nullptr);
79 // cout << "Notification: " << notification.dump() << "\n";
80 }
81
82
83 void Server::onChunkRead(const PcmStream* pcmStream, const msg::PcmChunk& chunk)
84 {
85 std::ignore = pcmStream;
86 std::ignore = chunk;
87 }
88
89
90 void Server::onChunkEncoded(const PcmStream* pcmStream, std::shared_ptr<msg::PcmChunk> chunk, double duration)
91 {
92 streamServer_->onChunkEncoded(pcmStream, pcmStream == streamManager_->getDefaultStream().get(), chunk, duration);
93 }
94
95
96 void Server::onResync(const PcmStream* pcmStream, double ms)
97 {
98 LOG(INFO, LOG_TAG) << "onResync (" << pcmStream->getName() << "): " << ms << " ms\n";
99 }
100
101
102 void Server::onDisconnect(StreamSession* streamSession)
103 {
104 // notify controllers if not yet done
105 ClientInfoPtr clientInfo = Config::instance().getClientInfo(streamSession->clientId);
106 if (!clientInfo || !clientInfo->connected)
107 return;
108
109 clientInfo->connected = false;
110 chronos::systemtimeofday(&clientInfo->lastSeen);
111 saveConfig();
112 if (controlServer_ != nullptr)
113 {
114 // Check if there is no session of this client left
115 // Can happen in case of ungraceful disconnect/reconnect or
116 // in case of a duplicate client id
117 if (streamServer_->getStreamSession(clientInfo->id) == nullptr)
118 {
119 // clang-format off
120 // Notification:
121 // {"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"}}
122 // clang-format on
123 json notification =
124 jsonrpcpp::Notification("Client.OnDisconnect", jsonrpcpp::Parameter("id", clientInfo->id, "client", clientInfo->toJson())).to_json();
125 controlServer_->send(notification.dump());
126 // cout << "Notification: " << notification.dump() << "\n";
127 }
128 }
129 }
130
131
132 void Server::processRequest(const jsonrpcpp::request_ptr request, jsonrpcpp::entity_ptr& response, jsonrpcpp::notification_ptr& notification) const
133 {
134 try
135 {
136 // LOG(INFO, LOG_TAG) << "Server::processRequest method: " << request->method << ", " << "id: " << request->id() << "\n";
137 Json result;
138
139 if (request->method().find("Client.") == 0)
140 {
141 ClientInfoPtr clientInfo = Config::instance().getClientInfo(request->params().get<std::string>("id"));
142 if (clientInfo == nullptr)
143 throw jsonrpcpp::InternalErrorException("Client not found", request->id());
144
145 if (request->method() == "Client.GetStatus")
146 {
147 // clang-format off
148 // Request: {"id":8,"jsonrpc":"2.0","method":"Client.GetStatus","params":{"id":"00:21:6a:7d:74:fc"}}
149 // 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"}}}}
150 // clang-format on
151 result["client"] = clientInfo->toJson();
152 }
153 else if (request->method() == "Client.SetVolume")
154 {
155 // clang-format off
156 // Request: {"id":8,"jsonrpc":"2.0","method":"Client.SetVolume","params":{"id":"00:21:6a:7d:74:fc","volume":{"muted":false,"percent":74}}}
157 // Response: {"id":8,"jsonrpc":"2.0","result":{"volume":{"muted":false,"percent":74}}}
158 // Notification: {"jsonrpc":"2.0","method":"Client.OnVolumeChanged","params":{"id":"00:21:6a:7d:74:fc","volume":{"muted":false,"percent":74}}}
159 // clang-format on
160
161 std::lock_guard<std::recursive_mutex> lock(clientMutex_);
162 clientInfo->config.volume.fromJson(request->params().get("volume"));
163 result["volume"] = clientInfo->config.volume.toJson();
164 notification.reset(new jsonrpcpp::Notification("Client.OnVolumeChanged",
165 jsonrpcpp::Parameter("id", clientInfo->id, "volume", clientInfo->config.volume.toJson())));
166 }
167 else if (request->method() == "Client.SetLatency")
168 {
169 // clang-format off
170 // Request: {"id":7,"jsonrpc":"2.0","method":"Client.SetLatency","params":{"id":"00:21:6a:7d:74:fc#2","latency":10}}
171 // Response: {"id":7,"jsonrpc":"2.0","result":{"latency":10}}
172 // Notification: {"jsonrpc":"2.0","method":"Client.OnLatencyChanged","params":{"id":"00:21:6a:7d:74:fc#2","latency":10}}
173 // clang-format on
174 int latency = request->params().get("latency");
175 if (latency < -10000)
176 latency = -10000;
177 else if (latency > settings_.stream.bufferMs)
178 latency = settings_.stream.bufferMs;
179 clientInfo->config.latency = latency; //, -10000, settings_.stream.bufferMs);
180 result["latency"] = clientInfo->config.latency;
181 notification.reset(
182 new jsonrpcpp::Notification("Client.OnLatencyChanged", jsonrpcpp::Parameter("id", clientInfo->id, "latency", clientInfo->config.latency)));
183 }
184 else if (request->method() == "Client.SetName")
185 {
186 // clang-format off
187 // Request: {"id":6,"jsonrpc":"2.0","method":"Client.SetName","params":{"id":"00:21:6a:7d:74:fc#2","name":"Laptop"}}
188 // Response: {"id":6,"jsonrpc":"2.0","result":{"name":"Laptop"}}
189 // Notification: {"jsonrpc":"2.0","method":"Client.OnNameChanged","params":{"id":"00:21:6a:7d:74:fc#2","name":"Laptop"}}
190 // clang-format on
191 clientInfo->config.name = request->params().get<std::string>("name");
192 result["name"] = clientInfo->config.name;
193 notification.reset(
194 new jsonrpcpp::Notification("Client.OnNameChanged", jsonrpcpp::Parameter("id", clientInfo->id, "name", clientInfo->config.name)));
195 }
196 else
197 throw jsonrpcpp::MethodNotFoundException(request->id());
198
199
200 if (request->method().find("Client.Set") == 0)
201 {
202 /// Update client
203 session_ptr session = streamServer_->getStreamSession(clientInfo->id);
204 if (session != nullptr)
205 {
206 auto serverSettings = make_shared<msg::ServerSettings>();
207 serverSettings->setBufferMs(settings_.stream.bufferMs);
208 serverSettings->setVolume(clientInfo->config.volume.percent);
209 GroupPtr group = Config::instance().getGroupFromClient(clientInfo);
210 serverSettings->setMuted(clientInfo->config.volume.muted || group->muted);
211 serverSettings->setLatency(clientInfo->config.latency);
212 session->send(serverSettings);
213 }
214 }
215 }
216 else if (request->method().find("Group.") == 0)
217 {
218 GroupPtr group = Config::instance().getGroup(request->params().get<std::string>("id"));
219 if (group == nullptr)
220 throw jsonrpcpp::InternalErrorException("Group not found", request->id());
221
222 if (request->method() == "Group.GetStatus")
223 {
224 // clang-format off
225 // Request: {"id":5,"jsonrpc":"2.0","method":"Group.GetStatus","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1"}}
226 // 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"}}}
227 // clang-format on
228 result["group"] = group->toJson();
229 }
230 else if (request->method() == "Group.SetName")
231 {
232 // clang-format off
233 // Request: {"id":6,"jsonrpc":"2.0","method":"Group.SetName","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","name":"Laptop"}}
234 // Response: {"id":6,"jsonrpc":"2.0","result":{"name":"MediaPlayer"}}
235 // Notification: {"jsonrpc":"2.0","method":"Group.OnNameChanged","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","MediaPlayer":"Laptop"}}
236 // clang-format on
237 group->name = request->params().get<std::string>("name");
238 result["name"] = group->name;
239 notification.reset(new jsonrpcpp::Notification("Group.OnNameChanged", jsonrpcpp::Parameter("id", group->id, "name", group->name)));
240 }
241 else if (request->method() == "Group.SetMute")
242 {
243 // clang-format off
244 // Request: {"id":5,"jsonrpc":"2.0","method":"Group.SetMute","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","mute":true}}
245 // Response: {"id":5,"jsonrpc":"2.0","result":{"mute":true}}
246 // Notification: {"jsonrpc":"2.0","method":"Group.OnMute","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","mute":true}}
247 // clang-format on
248 bool muted = request->params().get<bool>("mute");
249 group->muted = muted;
250
251 /// Update clients
252 for (auto client : group->clients)
253 {
254 session_ptr session = streamServer_->getStreamSession(client->id);
255 if (session != nullptr)
256 {
257 auto serverSettings = make_shared<msg::ServerSettings>();
258 serverSettings->setBufferMs(settings_.stream.bufferMs);
259 serverSettings->setVolume(client->config.volume.percent);
260 GroupPtr group = Config::instance().getGroupFromClient(client);
261 serverSettings->setMuted(client->config.volume.muted || group->muted);
262 serverSettings->setLatency(client->config.latency);
263 session->send(serverSettings);
264 }
265 }
266
267 result["mute"] = group->muted;
268 notification.reset(new jsonrpcpp::Notification("Group.OnMute", jsonrpcpp::Parameter("id", group->id, "mute", group->muted)));
269 }
270 else if (request->method() == "Group.SetStream")
271 {
272 // clang-format off
273 // Request: {"id":4,"jsonrpc":"2.0","method":"Group.SetStream","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","stream_id":"stream 1"}}
274 // Response: {"id":4,"jsonrpc":"2.0","result":{"stream_id":"stream 1"}}
275 // Notification: {"jsonrpc":"2.0","method":"Group.OnStreamChanged","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","stream_id":"stream 1"}}
276 // clang-format on
277 string streamId = request->params().get<std::string>("stream_id");
278 PcmStreamPtr stream = streamManager_->getStream(streamId);
279 if (stream == nullptr)
280 throw jsonrpcpp::InternalErrorException("Stream not found", request->id());
281
282 group->streamId = streamId;
283
284 // Update clients
285 for (auto client : group->clients)
286 {
287 session_ptr session = streamServer_->getStreamSession(client->id);
288 if (session && (session->pcmStream() != stream))
289 {
290 session->send(stream->getMeta());
291 session->send(stream->getHeader());
292 session->setPcmStream(stream);
293 }
294 }
295
296 // Notify others
297 result["stream_id"] = group->streamId;
298 notification.reset(new jsonrpcpp::Notification("Group.OnStreamChanged", jsonrpcpp::Parameter("id", group->id, "stream_id", group->streamId)));
299 }
300 else if (request->method() == "Group.SetClients")
301 {
302 // clang-format off
303 // 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"}}
304 // 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"}}]}}}
305 // 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"}}]}}}
306 // clang-format on
307 vector<string> clients = request->params().get("clients");
308 // Remove clients from group
309 for (auto iter = group->clients.begin(); iter != group->clients.end();)
310 {
311 auto client = *iter;
312 if (find(clients.begin(), clients.end(), client->id) != clients.end())
313 {
314 ++iter;
315 continue;
316 }
317 iter = group->clients.erase(iter);
318 GroupPtr newGroup = Config::instance().addClientInfo(client);
319 newGroup->streamId = group->streamId;
320 }
321
322 // Add clients to group
323 PcmStreamPtr stream = streamManager_->getStream(group->streamId);
324 for (const auto& clientId : clients)
325 {
326 ClientInfoPtr client = Config::instance().getClientInfo(clientId);
327 if (!client)
328 continue;
329 GroupPtr oldGroup = Config::instance().getGroupFromClient(client);
330 if (oldGroup && (oldGroup->id == group->id))
331 continue;
332
333 if (oldGroup)
334 {
335 oldGroup->removeClient(client);
336 Config::instance().remove(oldGroup);
337 }
338
339 group->addClient(client);
340
341 // assign new stream
342 session_ptr session = streamServer_->getStreamSession(client->id);
343 if (session && stream && (session->pcmStream() != stream))
344 {
345 session->send(stream->getMeta());
346 session->send(stream->getHeader());
347 session->setPcmStream(stream);
348 }
349 }
350
351 if (group->empty())
352 Config::instance().remove(group);
353
354 json server = Config::instance().getServerStatus(streamManager_->toJson());
355 result["server"] = server;
356
357 // Notify others: since at least two groups are affected, send a complete server update
358 notification.reset(new jsonrpcpp::Notification("Server.OnUpdate", jsonrpcpp::Parameter("server", server)));
359 }
360 else
361 throw jsonrpcpp::MethodNotFoundException(request->id());
362 }
363 else if (request->method().find("Server.") == 0)
364 {
365 if (request->method().find("Server.GetRPCVersion") == 0)
366 {
367 // Request: {"id":8,"jsonrpc":"2.0","method":"Server.GetRPCVersion"}
368 // Response: {"id":8,"jsonrpc":"2.0","result":{"major":2,"minor":0,"patch":0}}
369 // <major>: backwards incompatible change
370 result["major"] = 2;
371 // <minor>: feature addition to the API
372 result["minor"] = 0;
373 // <patch>: bugfix release
374 result["patch"] = 0;
375 }
376 else if (request->method() == "Server.GetStatus")
377 {
378 // clang-format off
379 // Request: {"id":1,"jsonrpc":"2.0","method":"Server.GetStatus"}
380 // 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"}}]}}}
381 // clang-format on
382 result["server"] = Config::instance().getServerStatus(streamManager_->toJson());
383 }
384 else if (request->method() == "Server.DeleteClient")
385 {
386 // clang-format off
387 // Request: {"id":2,"jsonrpc":"2.0","method":"Server.DeleteClient","params":{"id":"00:21:6a:7d:74:fc"}}
388 // 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"}}]}}}
389 // 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"}}]}}}
390 // clang-format on
391 ClientInfoPtr clientInfo = Config::instance().getClientInfo(request->params().get<std::string>("id"));
392 if (clientInfo == nullptr)
393 throw jsonrpcpp::InternalErrorException("Client not found", request->id());
394
395 Config::instance().remove(clientInfo);
396
397 json server = Config::instance().getServerStatus(streamManager_->toJson());
398 result["server"] = server;
399
400 /// Notify others
401 notification.reset(new jsonrpcpp::Notification("Server.OnUpdate", jsonrpcpp::Parameter("server", server)));
402 }
403 else
404 throw jsonrpcpp::MethodNotFoundException(request->id());
405 }
406 else if (request->method().find("Stream.") == 0)
407 {
408 if (request->method().find("Stream.SetMeta") == 0)
409 {
410 // clang-format off
411 // Request: {"id":4,"jsonrpc":"2.0","method":"Stream.SetMeta","params":{"id":"Spotify", "meta": {"album": "some album", "artist": "some artist", "track": "some track"...}}}
412 // Response: {"id":4,"jsonrpc":"2.0","result":{"stream_id":"Spotify"}}
413 // Call onMetaChanged(const PcmStream* pcmStream) for updates and notifications
414 // clang-format on
415
416 LOG(INFO, LOG_TAG) << "Stream.SetMeta(" << request->params().get<std::string>("id") << ")" << request->params().get("meta") << "\n";
417
418 // Find stream
419 string streamId = request->params().get<std::string>("id");
420 PcmStreamPtr stream = streamManager_->getStream(streamId);
421 if (stream == nullptr)
422 throw jsonrpcpp::InternalErrorException("Stream not found", request->id());
423
424 // Set metadata from request
425 stream->setMeta(request->params().get("meta"));
426
427 // Setup response
428 result["id"] = streamId;
429 }
430 else if (request->method() == "Stream.AddStream")
431 {
432 // clang-format off
433 // Request: {"id":4,"jsonrpc":"2.0","method":"Stream.AddStream","params":{"streamUri":"uri"}}
434 // Response: {"id":4,"jsonrpc":"2.0","result":{"stream_id":"Spotify"}}
435 // Call onMetaChanged(const PcmStream* pcmStream) for updates and notifications
436 // clang-format on
437
438 LOG(INFO, LOG_TAG) << "Stream.AddStream(" << request->params().get("streamUri") << ")"
439 << "\n";
440
441 // Find stream
442 string streamUri = request->params().get("streamUri");
443 PcmStreamPtr stream = streamManager_->addStream(streamUri);
444 if (stream == nullptr)
445 throw jsonrpcpp::InternalErrorException("Stream not created", request->id());
446 stream->start(); // We start the stream, otherwise it would be silent
447 // Setup response
448 result["id"] = stream->getId();
449 }
450 else if (request->method() == "Stream.RemoveStream")
451 {
452 // clang-format off
453 // Request: {"id":4,"jsonrpc":"2.0","method":"Stream.RemoveStream","params":{"id":"Spotify"}}
454 // Response: {"id":4,"jsonrpc":"2.0","result":{"stream_id":"Spotify"}}
455 // Call onMetaChanged(const PcmStream* pcmStream) for updates and notifications
456 // clang-format on
457
458 LOG(INFO, LOG_TAG) << "Stream.RemoveStream(" << request->params().get("id") << ")"
459 << "\n";
460
461 // Find stream
462 string streamId = request->params().get("id");
463 streamManager_->removeStream(streamId);
464 // Setup response
465 result["id"] = streamId;
466 }
467 else
468 throw jsonrpcpp::MethodNotFoundException(request->id());
469 }
470 else
471 throw jsonrpcpp::MethodNotFoundException(request->id());
472
473 response.reset(new jsonrpcpp::Response(*request, result));
474 }
475 catch (const jsonrpcpp::RequestException& e)
476 {
477 LOG(ERROR, LOG_TAG) << "Server::onMessageReceived JsonRequestException: " << e.to_json().dump() << ", message: " << request->to_json().dump() << "\n";
478 response.reset(new jsonrpcpp::RequestException(e));
479 }
480 catch (const exception& e)
481 {
482 LOG(ERROR, LOG_TAG) << "Server::onMessageReceived exception: " << e.what() << ", message: " << request->to_json().dump() << "\n";
483 response.reset(new jsonrpcpp::InternalErrorException(e.what(), request->id()));
484 }
485 }
486
487
488 std::string Server::onMessageReceived(ControlSession* controlSession, const std::string& message)
489 {
490 // LOG(DEBUG, LOG_TAG) << "onMessageReceived: " << message << "\n";
491 jsonrpcpp::entity_ptr entity(nullptr);
492 try
493 {
494 entity = jsonrpcpp::Parser::do_parse(message);
495 if (!entity)
496 return "";
497 }
498 catch (const jsonrpcpp::ParseErrorException& e)
499 {
500 return e.to_json().dump();
501 }
502 catch (const std::exception& e)
503 {
504 return jsonrpcpp::ParseErrorException(e.what()).to_json().dump();
505 }
506
507 jsonrpcpp::entity_ptr response(nullptr);
508 jsonrpcpp::notification_ptr notification(nullptr);
509 if (entity->is_request())
510 {
511 jsonrpcpp::request_ptr request = dynamic_pointer_cast<jsonrpcpp::Request>(entity);
512 processRequest(request, response, notification);
513 saveConfig();
514 ////cout << "Request: " << request->to_json().dump() << "\n";
515 if (notification)
516 {
517 ////cout << "Notification: " << notification->to_json().dump() << "\n";
518 controlServer_->send(notification->to_json().dump(), controlSession);
519 }
520 if (response)
521 {
522 ////cout << "Response: " << response->to_json().dump() << "\n";
523 return response->to_json().dump();
524 }
525 return "";
526 }
527 else if (entity->is_batch())
528 {
529 jsonrpcpp::batch_ptr batch = dynamic_pointer_cast<jsonrpcpp::Batch>(entity);
530 ////cout << "Batch: " << batch->to_json().dump() << "\n";
531 jsonrpcpp::Batch responseBatch;
532 jsonrpcpp::Batch notificationBatch;
533 for (const auto& batch_entity : batch->entities)
534 {
535 if (batch_entity->is_request())
536 {
537 jsonrpcpp::request_ptr request = dynamic_pointer_cast<jsonrpcpp::Request>(batch_entity);
538 processRequest(request, response, notification);
539 if (response != nullptr)
540 responseBatch.add_ptr(response);
541 if (notification != nullptr)
542 notificationBatch.add_ptr(notification);
543 }
544 }
545 saveConfig();
546 if (!notificationBatch.entities.empty())
547 controlServer_->send(notificationBatch.to_json().dump(), controlSession);
548 if (!responseBatch.entities.empty())
549 return responseBatch.to_json().dump();
550 return "";
551 }
552 return "";
553 }
554
555
556
557 void Server::onMessageReceived(StreamSession* streamSession, const msg::BaseMessage& baseMessage, char* buffer)
558 {
559 LOG(DEBUG, LOG_TAG) << "onMessageReceived: " << baseMessage.type << ", size: " << baseMessage.size << ", id: " << baseMessage.id
560 << ", refers: " << baseMessage.refersTo << ", sent: " << baseMessage.sent.sec << "," << baseMessage.sent.usec
561 << ", recv: " << baseMessage.received.sec << "," << baseMessage.received.usec << "\n";
562 if (baseMessage.type == message_type::kTime)
563 {
564 auto timeMsg = make_shared<msg::Time>();
565 timeMsg->deserialize(baseMessage, buffer);
566 timeMsg->refersTo = timeMsg->id;
567 timeMsg->latency = timeMsg->received - timeMsg->sent;
568 // LOG(INFO, LOG_TAG) << "Latency sec: " << timeMsg.latency.sec << ", usec: " << timeMsg.latency.usec << ", refers to: " << timeMsg.refersTo << "\n";
569 streamSession->send(timeMsg);
570
571 // refresh streamSession state
572 ClientInfoPtr client = Config::instance().getClientInfo(streamSession->clientId);
573 if (client != nullptr)
574 {
575 chronos::systemtimeofday(&client->lastSeen);
576 client->connected = true;
577 }
578 }
579 else if (baseMessage.type == message_type::kClientInfo)
580 {
581 ClientInfoPtr clientInfo = Config::instance().getClientInfo(streamSession->clientId);
582 if (clientInfo == nullptr)
583 {
584 LOG(ERROR, LOG_TAG) << "client not found: " << streamSession->clientId << "\n";
585 return;
586 }
587 msg::ClientInfo infoMsg;
588 infoMsg.deserialize(baseMessage, buffer);
589
590 clientInfo->config.volume.percent = infoMsg.getVolume();
591 clientInfo->config.volume.muted = infoMsg.isMuted();
592 jsonrpcpp::notification_ptr notification = make_shared<jsonrpcpp::Notification>(
593 "Client.OnVolumeChanged", jsonrpcpp::Parameter("id", streamSession->clientId, "volume", clientInfo->config.volume.toJson()));
594 controlServer_->send(notification->to_json().dump());
595 }
596 else if (baseMessage.type == message_type::kHello)
597 {
598 msg::Hello helloMsg;
599 helloMsg.deserialize(baseMessage, buffer);
600 streamSession->clientId = helloMsg.getUniqueId();
601 LOG(INFO, LOG_TAG) << "Hello from " << streamSession->clientId << ", host: " << helloMsg.getHostName() << ", v" << helloMsg.getVersion()
602 << ", ClientName: " << helloMsg.getClientName() << ", OS: " << helloMsg.getOS() << ", Arch: " << helloMsg.getArch()
603 << ", Protocol version: " << helloMsg.getProtocolVersion() << "\n";
604
605 bool newGroup(false);
606 GroupPtr group = Config::instance().getGroupFromClient(streamSession->clientId);
607 if (group == nullptr)
608 {
609 group = Config::instance().addClientInfo(streamSession->clientId);
610 newGroup = true;
611 }
612
613 ClientInfoPtr client = group->getClient(streamSession->clientId);
614
615 LOG(DEBUG, LOG_TAG) << "Sending ServerSettings to " << streamSession->clientId << "\n";
616 auto serverSettings = make_shared<msg::ServerSettings>();
617 serverSettings->setVolume(client->config.volume.percent);
618 serverSettings->setMuted(client->config.volume.muted || group->muted);
619 serverSettings->setLatency(client->config.latency);
620 serverSettings->setBufferMs(settings_.stream.bufferMs);
621 serverSettings->refersTo = helloMsg.id;
622 streamSession->send(serverSettings);
623
624 client->host.mac = helloMsg.getMacAddress();
625 client->host.ip = streamSession->getIP();
626 client->host.name = helloMsg.getHostName();
627 client->host.os = helloMsg.getOS();
628 client->host.arch = helloMsg.getArch();
629 client->snapclient.version = helloMsg.getVersion();
630 client->snapclient.name = helloMsg.getClientName();
631 client->snapclient.protocolVersion = helloMsg.getProtocolVersion();
632 client->config.instance = helloMsg.getInstance();
633 client->connected = true;
634 chronos::systemtimeofday(&client->lastSeen);
635
636 // Assign and update stream
637 PcmStreamPtr stream = streamManager_->getStream(group->streamId);
638 if (!stream)
639 {
640 stream = streamManager_->getDefaultStream();
641 group->streamId = stream->getId();
642 }
643 LOG(DEBUG, LOG_TAG) << "Group: " << group->id << ", stream: " << group->streamId << "\n";
644
645 saveConfig();
646
647 LOG(DEBUG, LOG_TAG) << "Sending meta data to " << streamSession->clientId << "\n";
648 streamSession->send(stream->getMeta());
649 streamSession->setPcmStream(stream);
650 auto headerChunk = stream->getHeader();
651 LOG(DEBUG, LOG_TAG) << "Sending codec header to " << streamSession->clientId << "\n";
652 streamSession->send(headerChunk);
653
654 if (newGroup)
655 {
656 // clang-format off
657 // 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"}}]}}}
658 // clang-format on
659 json server = Config::instance().getServerStatus(streamManager_->toJson());
660 json notification = jsonrpcpp::Notification("Server.OnUpdate", jsonrpcpp::Parameter("server", server)).to_json();
661 controlServer_->send(notification.dump());
662 }
663 else
664 {
665 // clang-format off
666 // 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"}}
667 // clang-format on
668 json notification = jsonrpcpp::Notification("Client.OnConnect", jsonrpcpp::Parameter("id", client->id, "client", client->toJson())).to_json();
669 controlServer_->send(notification.dump());
670 // cout << "Notification: " << notification.dump() << "\n";
671 }
672 // cout << Config::instance().getServerStatus(streamManager_->toJson()).dump(4) << "\n";
673 // cout << group->toJson().dump(4) << "\n";
674 }
675 }
676
677
678 void Server::saveConfig(const std::chrono::milliseconds& deferred)
679 {
680 config_timer_.cancel();
681 config_timer_.expires_after(deferred);
682 config_timer_.async_wait([](const boost::system::error_code& ec) {
683 if (!ec)
684 {
685 LOG(DEBUG, LOG_TAG) << "Saving config\n";
686 Config::instance().save();
687 }
688 });
689 }
690
691
692 void Server::start()
693 {
694 try
695 {
696 controlServer_ = std::make_unique<ControlServer>(io_context_, settings_.tcp, settings_.http, this);
697 streamServer_ = std::make_unique<StreamServer>(io_context_, settings_, this);
698 streamManager_ =
699 std::make_unique<StreamManager>(this, io_context_, settings_.stream.sampleFormat, settings_.stream.codec, settings_.stream.streamChunkMs);
700 // throw SnapException("xxx");
701 // Add normal sources first
702 for (const auto& sourceUri : settings_.stream.sources)
703 {
704 StreamUri streamUri(sourceUri);
705 if (streamUri.scheme == "meta")
706 continue;
707 PcmStreamPtr stream = streamManager_->addStream(streamUri);
708 if (stream)
709 LOG(INFO, LOG_TAG) << "Stream: " << stream->getUri().toJson() << "\n";
710 }
711 // Add meta sources second
712 for (const auto& sourceUri : settings_.stream.sources)
713 {
714 StreamUri streamUri(sourceUri);
715 if (streamUri.scheme != "meta")
716 continue;
717 PcmStreamPtr stream = streamManager_->addStream(streamUri);
718 if (stream)
719 LOG(INFO, LOG_TAG) << "Stream: " << stream->getUri().toJson() << "\n";
720 }
721
722 streamManager_->start();
723 controlServer_->start();
724 streamServer_->start();
725 }
726 catch (const std::exception& e)
727 {
728 LOG(NOTICE, LOG_TAG) << "Server::start: " << e.what() << endl;
729 stop();
730 throw;
731 }
732 }
733
734
735 void Server::stop()
736 {
737 if (streamManager_)
738 {
739 streamManager_->stop();
740 streamManager_ = nullptr;
741 }
742
743 if (controlServer_)
744 {
745 controlServer_->stop();
746 controlServer_ = nullptr;
747 }
748
749 if (streamServer_)
750 {
751 streamServer_->stop();
752 streamServer_ = nullptr;
753 }
754 }
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 SERVER_HPP
19 #define SERVER_HPP
20
21 #include <boost/asio.hpp>
22 #include <memory>
23 #include <mutex>
24 #include <set>
25 #include <sstream>
26 #include <vector>
27
28 #include "common/queue.h"
29 #include "common/sample_format.hpp"
30 #include "control_server.hpp"
31 #include "jsonrpcpp.hpp"
32 #include "message/codec_header.hpp"
33 #include "message/message.hpp"
34 #include "message/server_settings.hpp"
35 #include "server_settings.hpp"
36 #include "stream_server.hpp"
37 #include "stream_session.hpp"
38 #include "streamreader/stream_manager.hpp"
39
40 using namespace streamreader;
41
42 using boost::asio::ip::tcp;
43 using acceptor_ptr = std::unique_ptr<tcp::acceptor>;
44 using session_ptr = std::shared_ptr<StreamSession>;
45
46
47 /// Forwars PCM data to the connected clients
48 /**
49 * Reads PCM data using PipeStream, implements PcmListener to get the (encoded) PCM stream.
50 * Accepts and holds client connections (StreamSession)
51 * Receives (via the StreamMessageReceiver interface) and answers messages from the clients
52 * Forwards PCM data to the clients
53 */
54 class Server : public StreamMessageReceiver, public ControlMessageReceiver, public PcmListener
55 {
56 public:
57 Server(boost::asio::io_context& io_context, const ServerSettings& serverSettings);
58 virtual ~Server();
59
60 void start();
61 void stop();
62
63 private:
64 /// Implementation of StreamMessageReceiver
65 void onMessageReceived(StreamSession* connection, const msg::BaseMessage& baseMessage, char* buffer) override;
66 void onDisconnect(StreamSession* connection) override;
67
68 /// Implementation of ControllMessageReceiver
69 std::string onMessageReceived(ControlSession* connection, const std::string& message) override;
70 void onNewSession(const std::shared_ptr<ControlSession>& session) override
71 {
72 std::ignore = session;
73 };
74 void onNewSession(const std::shared_ptr<StreamSession>& session) override;
75
76 /// Implementation of PcmListener
77 void onMetaChanged(const PcmStream* pcmStream) override;
78 void onStateChanged(const PcmStream* pcmStream, ReaderState state) override;
79 void onChunkRead(const PcmStream* pcmStream, const msg::PcmChunk& chunk) override;
80 void onChunkEncoded(const PcmStream* pcmStream, std::shared_ptr<msg::PcmChunk> chunk, double duration) override;
81 void onResync(const PcmStream* pcmStream, double ms) override;
82
83 private:
84 void processRequest(const jsonrpcpp::request_ptr request, jsonrpcpp::entity_ptr& response, jsonrpcpp::notification_ptr& notification) const;
85 /// Save the server state deferred to prevent blocking and lower disk io
86 /// @param deferred the delay after the last call to saveConfig
87 void saveConfig(const std::chrono::milliseconds& deferred = std::chrono::seconds(2));
88
89 mutable std::recursive_mutex sessionsMutex_;
90 mutable std::recursive_mutex clientMutex_;
91 boost::asio::io_context& io_context_;
92 boost::asio::steady_timer config_timer_;
93
94 ServerSettings settings_;
95 Queue<std::shared_ptr<msg::BaseMessage>> messages_;
96 std::unique_ptr<ControlServer> controlServer_;
97 std::unique_ptr<StreamServer> streamServer_;
98 std::unique_ptr<StreamManager> streamManager_;
99 };
100
101
102
103 #endif
2323
2424 struct ServerSettings
2525 {
26 struct HttpSettings
26 struct Server
27 {
28 int threads{-1};
29 std::string pid_file{"/var/run/snapserver/pid"};
30 std::string user{"snapserver"};
31 std::string group{""};
32 std::string data_dir{""};
33 };
34
35 struct Http
2736 {
2837 bool enabled{true};
2938 size_t port{1780};
3140 std::string doc_root{""};
3241 };
3342
34 struct TcpSettings
43 struct Tcp
3544 {
3645 bool enabled{true};
3746 size_t port{1705};
3847 std::vector<std::string> bind_to_address{{"0.0.0.0"}};
3948 };
4049
41 struct StreamSettings
50 struct Stream
4251 {
4352 size_t port{1704};
44 std::vector<std::string> pcmStreams;
53 std::vector<std::string> sources;
4554 std::string codec{"flac"};
4655 int32_t bufferMs{1000};
4756 std::string sampleFormat{"48000:16:2"};
5059 std::vector<std::string> bind_to_address{{"0.0.0.0"}};
5160 };
5261
53 struct LoggingSettings
62 struct Logging
5463 {
55 bool debug{false};
56 std::string debug_logfile{""};
64 std::string sink{""};
65 std::string filter{"*:info"};
5766 };
5867
59 HttpSettings http;
60 TcpSettings tcp;
61 StreamSettings stream;
62 LoggingSettings logging;
68 Server server;
69 Http http;
70 Tcp tcp;
71 Stream stream;
72 Logging logging;
6373 };
6474
6575 #endif
0 .TH SNAPSERVER 1 "January 2020"
0 .TH SNAPSERVER 1 "June 2020"
11 .SH NAME
22 snapserver - Snapcast server
33 .SH SYNOPSIS
2424 Daemonize
2525 optional process priority [-20..19]
2626 .TP
27 \fB--user arg\fR
28 the user[:group] to run snapserver as when daemonized
29 .TP
3027 \fB-c, --config arg (=/etc/snapserver.conf)\fR
3128 path to the configuration file
3229 .SH FILES
2929 #include "common/utils/string_utils.hpp"
3030 #include "encoder/encoder_factory.hpp"
3131 #include "message/message.hpp"
32 #include "server.hpp"
3233 #include "server_settings.hpp"
33 #include "stream_server.hpp"
3434 #if defined(HAS_AVAHI) || defined(HAS_BONJOUR)
3535 #include "publishZeroConf/publish_mdns.hpp"
3636 #endif
5252 try
5353 {
5454 ServerSettings settings;
55 std::string pcmStream = "pipe:///tmp/snapfifo?name=default";
55 std::string pcmSource = "pipe:///tmp/snapfifo?name=default";
5656 std::string config_file = "/etc/snapserver.conf";
5757
5858 OptionParser op("Allowed options");
6262 #ifdef HAS_DAEMON
6363 int processPriority(0);
6464 auto daemonOption = op.add<Implicit<int>>("d", "daemon", "Daemonize\noptional process priority [-20..19]", 0, &processPriority);
65 auto userValue = op.add<Value<string>>("", "user", "the user[:group] to run snapserver as when daemonized", "");
66 #endif
67
65 #endif
6866 op.add<Value<string>>("c", "config", "path to the configuration file", config_file, &config_file);
6967
70 // debug settings
71 OptionParser conf("");
72 conf.add<Value<bool>>("", "logging.debug", "enable debug logging", settings.logging.debug, &settings.logging.debug);
73 conf.add<Value<string>>("", "logging.debug_logfile", "log file name for the debug logs (debug must be enabled)", settings.logging.debug_logfile,
74 &settings.logging.debug_logfile);
68 OptionParser conf("Overridable config file options");
69
70 // server settings
71 conf.add<Value<int>>("", "server.threads", "number of server threads", settings.server.threads, &settings.server.threads);
72 conf.add<Value<string>>("", "server.pidfile", "pid file when running as daemon", settings.server.pid_file, &settings.server.pid_file);
73 conf.add<Value<string>>("", "server.user", "the user to run as when daemonized", settings.server.user, &settings.server.user);
74 conf.add<Implicit<string>>("", "server.group", "the group to run as when daemonized", settings.server.group, &settings.server.group);
75 conf.add<Implicit<string>>("", "server.datadir", "directory where persistent data is stored", settings.server.data_dir, &settings.server.data_dir);
76
77 // HTTP RPC settings
78 conf.add<Value<bool>>("", "http.enabled", "enable HTTP Json RPC (HTTP POST and websockets)", settings.http.enabled, &settings.http.enabled);
79 conf.add<Value<size_t>>("", "http.port", "which port the server should listen on", settings.http.port, &settings.http.port);
80 auto http_bind_to_address = conf.add<Value<string>>("", "http.bind_to_address", "address for the server to listen on",
81 settings.http.bind_to_address.front(), &settings.http.bind_to_address[0]);
82 conf.add<Implicit<string>>("", "http.doc_root", "serve a website from the doc_root location", settings.http.doc_root, &settings.http.doc_root);
83
84 // TCP RPC settings
85 conf.add<Value<bool>>("", "tcp.enabled", "enable TCP Json RPC)", settings.tcp.enabled, &settings.tcp.enabled);
86 conf.add<Value<size_t>>("", "tcp.port", "which port the server should listen on", settings.tcp.port, &settings.tcp.port);
87 auto tcp_bind_to_address = conf.add<Value<string>>("", "tcp.bind_to_address", "address for the server to listen on",
88 settings.tcp.bind_to_address.front(), &settings.tcp.bind_to_address[0]);
7589
7690 // stream settings
77 conf.add<Value<size_t>>("", "stream.port", "Server port", settings.stream.port, &settings.stream.port);
78 auto streamValue = conf.add<Value<string>>(
79 "", "stream.stream", "URI of the PCM input stream.\nFormat: TYPE://host/path?name=NAME\n[&codec=CODEC]\n[&sampleformat=SAMPLEFORMAT]", pcmStream,
80 &pcmStream);
81 int num_threads = -1;
82 conf.add<Value<int>>("", "server.threads", "number of server threads", num_threads, &num_threads);
83 std::string pid_file = "/var/run/snapserver/pid";
84 conf.add<Value<string>>("", "server.pidfile", "pid file when running as daemon", pid_file, &pid_file);
85 std::string data_dir;
86 conf.add<Implicit<string>>("", "server.datadir", "directory where persistent data is stored", data_dir, &data_dir);
91 auto stream_bind_to_address = conf.add<Value<string>>("", "stream.bind_to_address", "address for the server to listen on",
92 settings.stream.bind_to_address.front(), &settings.stream.bind_to_address[0]);
93 conf.add<Value<size_t>>("", "stream.port", "which port the server should listen on", settings.stream.port, &settings.stream.port);
94 // deprecated: stream.stream, use stream.source instead
95 auto streamValue = conf.add<Value<string>>("", "stream.stream", "Deprecated: use stream.source", pcmSource, &pcmSource);
96 auto sourceValue = conf.add<Value<string>>(
97 "", "stream.source", "URI of the PCM input stream.\nFormat: TYPE://host/path?name=NAME\n[&codec=CODEC]\n[&sampleformat=SAMPLEFORMAT]", pcmSource,
98 &pcmSource);
8799
88100 conf.add<Value<string>>("", "stream.sampleformat", "Default sample format", settings.stream.sampleFormat, &settings.stream.sampleFormat);
89101 conf.add<Value<string>>("", "stream.codec", "Default transport codec\n(flac|ogg|opus|pcm)[:options]\nType codec:? to get codec specific options",
90102 settings.stream.codec, &settings.stream.codec);
91103 // deprecated: stream_buffer, use chunk_ms instead
92 conf.add<Value<size_t>>("", "stream.stream_buffer", "Default stream read chunk size [ms]", settings.stream.streamChunkMs,
93 &settings.stream.streamChunkMs);
104 conf.add<Value<size_t>>("", "stream.stream_buffer", "Default stream read chunk size [ms], deprecated, use stream.chunk_ms instead",
105 settings.stream.streamChunkMs, &settings.stream.streamChunkMs);
94106 conf.add<Value<size_t>>("", "stream.chunk_ms", "Default stream read chunk size [ms]", settings.stream.streamChunkMs, &settings.stream.streamChunkMs);
95107 conf.add<Value<int>>("", "stream.buffer", "Buffer [ms]", settings.stream.bufferMs, &settings.stream.bufferMs);
96108 conf.add<Value<bool>>("", "stream.send_to_muted", "Send audio to muted clients", settings.stream.sendAudioToMutedClients,
97109 &settings.stream.sendAudioToMutedClients);
98 auto stream_bind_to_address = conf.add<Value<string>>("", "stream.bind_to_address", "address for the server to listen on",
99 settings.stream.bind_to_address.front(), &settings.stream.bind_to_address[0]);
100
101 // HTTP RPC settings
102 conf.add<Value<bool>>("", "http.enabled", "enable HTTP Json RPC (HTTP POST and websockets)", settings.http.enabled, &settings.http.enabled);
103 conf.add<Value<size_t>>("", "http.port", "which port the server should listen to", settings.http.port, &settings.http.port);
104 auto http_bind_to_address = conf.add<Value<string>>("", "http.bind_to_address", "address for the server to listen on",
105 settings.http.bind_to_address.front(), &settings.http.bind_to_address[0]);
106 conf.add<Implicit<string>>("", "http.doc_root", "serve a website from the doc_root location", settings.http.doc_root, &settings.http.doc_root);
107
108 // TCP RPC settings
109 conf.add<Value<bool>>("", "tcp.enabled", "enable TCP Json RPC)", settings.tcp.enabled, &settings.tcp.enabled);
110 conf.add<Value<size_t>>("", "tcp.port", "which port the server should listen to", settings.tcp.port, &settings.tcp.port);
111 auto tcp_bind_to_address = conf.add<Value<string>>("", "tcp.bind_to_address", "address for the server to listen on",
112 settings.tcp.bind_to_address.front(), &settings.tcp.bind_to_address[0]);
110
111 // logging settings
112 conf.add<Value<string>>("", "logging.sink", "log sink [null,system,stdout,stderr,file:<filename>]", settings.logging.sink, &settings.logging.sink);
113 auto logfilterOption = conf.add<Value<string>>(
114 "", "logging.filter",
115 "log filter <tag>:<level>[,<tag>:<level>]* with tag = * or <log tag> and level = [trace,debug,info,notice,warning,error,fatal]",
116 settings.logging.filter);
113117
114118 try
115119 {
137141 }
138142 catch (const std::invalid_argument& e)
139143 {
140 SLOG(ERROR) << "Exception: " << e.what() << std::endl;
144 cerr << "Exception: " << e.what() << std::endl;
141145 cout << "\n" << op << "\n";
142146 exit(EXIT_FAILURE);
143147 }
181185 exit(EXIT_SUCCESS);
182186 }
183187
184 AixLog::Log::init<AixLog::SinkNative>("snapserver", AixLog::Severity::trace, AixLog::Type::special);
185 if (settings.logging.debug)
186 {
187 AixLog::Log::instance().add_logsink<AixLog::SinkCout>(AixLog::Severity::trace, AixLog::Type::all, "%Y-%m-%d %H-%M-%S.#ms [#severity] (#tag_func)");
188 if (!settings.logging.debug_logfile.empty())
189 AixLog::Log::instance().add_logsink<AixLog::SinkFile>(AixLog::Severity::trace, AixLog::Type::all, settings.logging.debug_logfile,
190 "%Y-%m-%d %H-%M-%S.#ms [#severity] (#tag_func)");
191 }
188 settings.logging.filter = logfilterOption->value();
189 if (logfilterOption->is_set())
190 {
191 for (size_t n = 1; n < logfilterOption->count(); ++n)
192 settings.logging.filter += "," + logfilterOption->value(n);
193 }
194
195 if (settings.logging.sink.empty())
196 {
197 settings.logging.sink = "stdout";
198 #ifdef HAS_DAEMON
199 if (daemonOption->is_set())
200 settings.logging.sink = "system";
201 #endif
202 }
203 AixLog::Filter logfilter;
204 auto filters = utils::string::split(settings.logging.filter, ',');
205 for (const auto& filter : filters)
206 logfilter.add_filter(filter);
207
208 string logformat = "%Y-%m-%d %H-%M-%S.#ms [#severity] (#tag_func)";
209 if (settings.logging.sink.find("file:") != string::npos)
210 {
211 string logfile = settings.logging.sink.substr(settings.logging.sink.find(":") + 1);
212 AixLog::Log::init<AixLog::SinkFile>(logfilter, logfile, logformat);
213 }
214 else if (settings.logging.sink == "stdout")
215 AixLog::Log::init<AixLog::SinkCout>(logfilter, logformat);
216 else if (settings.logging.sink == "stderr")
217 AixLog::Log::init<AixLog::SinkCerr>(logfilter, logformat);
218 else if (settings.logging.sink == "system")
219 AixLog::Log::init<AixLog::SinkNative>("snapserver", logfilter);
220 else if (settings.logging.sink == "null")
221 AixLog::Log::init<AixLog::SinkNull>();
192222 else
193 {
194 AixLog::Log::instance().add_logsink<AixLog::SinkCout>(AixLog::Severity::info, AixLog::Type::all, "%Y-%m-%d %H-%M-%S [#severity] (#tag_func)");
195 }
196
197 for (const auto& opt : conf.unknown_options())
198 LOG(WARNING) << "unknown configuration option: " << opt << "\n";
199
200 if (!streamValue->is_set())
201 settings.stream.pcmStreams.push_back(streamValue->value());
223 throw SnapException("Invalid log sink: " + settings.logging.sink);
224
225 if (!streamValue->is_set() && !sourceValue->is_set())
226 settings.stream.sources.push_back(sourceValue->value());
202227
203228 for (size_t n = 0; n < streamValue->count(); ++n)
204229 {
205230 LOG(INFO) << "Adding stream: " << streamValue->value(n) << "\n";
206 settings.stream.pcmStreams.push_back(streamValue->value(n));
231 settings.stream.sources.push_back(streamValue->value(n));
232 }
233 for (size_t n = 0; n < sourceValue->count(); ++n)
234 {
235 LOG(INFO) << "Adding source: " << sourceValue->value(n) << "\n";
236 settings.stream.sources.push_back(sourceValue->value(n));
207237 }
208238
209239 #ifdef HAS_DAEMON
210240 std::unique_ptr<Daemon> daemon;
211241 if (daemonOption->is_set())
212242 {
213 string user = "";
214 string group = "";
215
216 if (userValue->is_set())
217 {
218 if (userValue->value().empty())
219 std::invalid_argument("user must not be empty");
220
221 vector<string> user_group = utils::string::split(userValue->value(), ':');
222 user = user_group[0];
223 if (user_group.size() > 1)
224 group = user_group[1];
225 }
226 if (data_dir.empty())
227 data_dir = "/var/lib/snapserver";
228 Config::instance().init(data_dir, user, group);
229 daemon.reset(new Daemon(user, group, pid_file));
230 SLOG(NOTICE) << "daemonizing" << std::endl;
231 daemon->daemonize();
232 if (processPriority < -20)
233 processPriority = -20;
234 else if (processPriority > 19)
235 processPriority = 19;
243 if (settings.server.user.empty())
244 std::invalid_argument("user must not be empty");
245
246 if (settings.server.data_dir.empty())
247 settings.server.data_dir = "/var/lib/snapserver";
248 Config::instance().init(settings.server.data_dir, settings.server.user, settings.server.group);
249
250 daemon = std::make_unique<Daemon>(settings.server.user, settings.server.group, settings.server.pid_file);
251 processPriority = std::min(std::max(-20, processPriority), 19);
236252 if (processPriority != 0)
237253 setpriority(PRIO_PROCESS, 0, processPriority);
238 SLOG(NOTICE) << "daemon started" << std::endl;
254 LOG(NOTICE) << "daemonizing" << std::endl;
255 daemon->daemonize();
256 LOG(NOTICE) << "daemon started" << std::endl;
239257 }
240258 else
241 Config::instance().init(data_dir);
259 Config::instance().init(settings.server.data_dir);
242260 #else
243261 Config::instance().init();
244262 #endif
272290 settings.stream.bufferMs = 400;
273291 }
274292
275 auto streamServer = std::make_unique<StreamServer>(io_context, settings);
276 streamServer->start();
277
278 if (num_threads < 0)
279 num_threads = std::max(2, std::min(4, static_cast<int>(std::thread::hardware_concurrency())));
280 LOG(INFO) << "number of threads: " << num_threads << ", hw threads: " << std::thread::hardware_concurrency() << "\n";
293 auto server = std::make_unique<Server>(io_context, settings);
294 server->start();
295
296 if (settings.server.threads < 0)
297 settings.server.threads = std::max(2, std::min(4, static_cast<int>(std::thread::hardware_concurrency())));
298 LOG(INFO) << "number of threads: " << settings.server.threads << ", hw threads: " << std::thread::hardware_concurrency() << "\n";
281299
282300 // Construct a signal set registered for process termination.
283301 boost::asio::signal_set signals(io_context, SIGHUP, SIGINT, SIGTERM);
284302 signals.async_wait([&io_context](const boost::system::error_code& ec, int signal) {
285303 if (!ec)
286 SLOG(INFO) << "Received signal " << signal << ": " << strsignal(signal) << "\n";
304 LOG(INFO) << "Received signal " << signal << ": " << strsignal(signal) << "\n";
287305 else
288 SLOG(INFO) << "Failed to wait for signal: " << ec << "\n";
306 LOG(INFO) << "Failed to wait for signal, error: " << ec.message() << "\n";
289307 io_context.stop();
290308 });
291309
292310 std::vector<std::thread> threads;
293 for (int n = 0; n < num_threads; ++n)
311 for (int n = 0; n < settings.server.threads; ++n)
294312 threads.emplace_back([&] { io_context.run(); });
295313
296314 io_context.run();
299317 t.join();
300318
301319 LOG(INFO) << "Stopping streamServer" << endl;
302 streamServer->stop();
320 server->stop();
303321 LOG(INFO) << "done" << endl;
304322 }
305323 catch (const std::exception& e)
306324 {
307 SLOG(ERROR) << "Exception: " << e.what() << std::endl;
325 LOG(ERROR) << "Exception: " << e.what() << std::endl;
308326 exitcode = EXIT_FAILURE;
309327 }
310328 Config::instance().save();
311 SLOG(NOTICE) << "daemon terminated." << endl;
329 LOG(NOTICE) << "daemon terminated." << endl;
312330 exit(exitcode);
313331 }
1818 #include "stream_server.hpp"
1919 #include "common/aixlog.hpp"
2020 #include "config.hpp"
21 #include "message/client_info.hpp"
2122 #include "message/hello.hpp"
2223 #include "message/stream_tags.hpp"
2324 #include "message/time.hpp"
25 #include "stream_session_tcp.hpp"
2426 #include <iostream>
2527
2628 using namespace std;
2830
2931 using json = nlohmann::json;
3032
31
32 StreamServer::StreamServer(boost::asio::io_context& io_context, const ServerSettings& serverSettings)
33 : io_context_(io_context), config_timer_(io_context), settings_(serverSettings)
33 static constexpr auto LOG_TAG = "StreamServer";
34
35 StreamServer::StreamServer(boost::asio::io_context& io_context, const ServerSettings& serverSettings, StreamMessageReceiver* messageReceiver)
36 : io_context_(io_context), config_timer_(io_context), settings_(serverSettings), messageReceiver_(messageReceiver)
3437 {
3538 }
3639
4447 auto count = distance(new_end, sessions_.end());
4548 if (count > 0)
4649 {
47 SLOG(ERROR) << "Removing " << count << " inactive session(s), active sessions: " << sessions_.size() - count << "\n";
50 LOG(ERROR, LOG_TAG) << "Removing " << count << " inactive session(s), active sessions: " << sessions_.size() - count << "\n";
4851 sessions_.erase(new_end, sessions_.end());
4952 }
5053 }
5154
5255
53 void StreamServer::onMetaChanged(const PcmStream* pcmStream)
56 void StreamServer::addSession(const std::shared_ptr<StreamSession>& session)
57 {
58 session->setMessageReceiver(this);
59 session->setBufferMs(settings_.stream.bufferMs);
60 session->start();
61
62 std::lock_guard<std::recursive_mutex> mlock(sessionsMutex_);
63 sessions_.emplace_back(session);
64 cleanup();
65 }
66
67
68 void StreamServer::onMetaChanged(const PcmStream* pcmStream, std::shared_ptr<msg::StreamTags> meta)
5469 {
5570 // clang-format off
5671 // Notification: {"jsonrpc":"2.0","method":"Stream.OnMetadata","params":{"id":"stream 1", "meta": {"album": "some album", "artist": "some artist", "track": "some track"...}}
5772 // clang-format on
5873
5974 // Send meta to all connected clients
60 const auto meta = pcmStream->getMeta();
61 LOG(DEBUG) << "metadata = " << meta->msg.dump(3) << "\n";
6275
6376 std::lock_guard<std::recursive_mutex> mlock(sessionsMutex_);
6477 for (auto s : sessions_)
6679 if (auto session = s.lock())
6780 {
6881 if (session->pcmStream().get() == pcmStream)
69 session->sendAsync(meta);
70 }
71 }
72
73 LOG(INFO) << "onMetaChanged (" << pcmStream->getName() << ")\n";
74 json notification = jsonrpcpp::Notification("Stream.OnMetadata", jsonrpcpp::Parameter("id", pcmStream->getId(), "meta", meta->msg)).to_json();
75 controlServer_->send(notification.dump(), nullptr);
76 // cout << "Notification: " << notification.dump() << "\n";
77 }
78
79 void StreamServer::onStateChanged(const PcmStream* pcmStream, const ReaderState& state)
80 {
81 // clang-format off
82 // 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"}}}}
83 // clang-format on
84 LOG(INFO) << "onStateChanged (" << pcmStream->getName() << "): " << static_cast<int>(state) << "\n";
85 // LOG(INFO) << pcmStream->toJson().dump(4);
86 json notification = jsonrpcpp::Notification("Stream.OnUpdate", jsonrpcpp::Parameter("id", pcmStream->getId(), "stream", pcmStream->toJson())).to_json();
87 controlServer_->send(notification.dump(), nullptr);
88 // cout << "Notification: " << notification.dump() << "\n";
89 }
90
91
92 void StreamServer::onChunkRead(const PcmStream* pcmStream, std::shared_ptr<msg::PcmChunk> chunk, double /*duration*/)
93 {
94 // LOG(INFO) << "onChunkRead (" << pcmStream->getName() << "): " << duration << "ms\n";
95 bool isDefaultStream(pcmStream == streamManager_->getDefaultStream().get());
96
82 session->send(meta);
83 }
84 }
85 }
86
87
88 void StreamServer::onChunkEncoded(const PcmStream* pcmStream, bool isDefaultStream, std::shared_ptr<msg::PcmChunk> chunk, double /*duration*/)
89 {
90 // LOG(TRACE, LOG_TAG) << "onChunkRead (" << pcmStream->getName() << "): " << duration << "ms\n";
9791 shared_const_buffer buffer(*chunk);
9892
93 // make a copy of the sessions to avoid that a session get's deleted
9994 std::vector<std::shared_ptr<StreamSession>> sessions;
10095 {
10196 std::lock_guard<std::recursive_mutex> mlock(sessionsMutex_);
126121 }
127122
128123 if (!session->pcmStream() && isDefaultStream) //->getName() == "default")
129 session->sendAsync(buffer);
124 session->send(buffer);
130125 else if (session->pcmStream().get() == pcmStream)
131 session->sendAsync(buffer);
132 }
133 }
134
135
136 void StreamServer::onResync(const PcmStream* pcmStream, double ms)
137 {
138 LOG(INFO) << "onResync (" << pcmStream->getName() << "): " << ms << " ms\n";
126 session->send(buffer);
127 }
128 }
129
130
131 void StreamServer::onMessageReceived(StreamSession* streamSession, const msg::BaseMessage& baseMessage, char* buffer)
132 {
133 if (messageReceiver_)
134 messageReceiver_->onMessageReceived(streamSession, baseMessage, buffer);
139135 }
140136
141137
147143 if (session == nullptr)
148144 return;
149145
150 LOG(INFO) << "onDisconnect: " << session->clientId << "\n";
151 LOG(DEBUG) << "sessions: " << sessions_.size() << "\n";
146 LOG(INFO, LOG_TAG) << "onDisconnect: " << session->clientId << "\n";
147 LOG(DEBUG, LOG_TAG) << "sessions: " << sessions_.size() << "\n";
152148 sessions_.erase(std::remove_if(sessions_.begin(), sessions_.end(),
153149 [streamSession](std::weak_ptr<StreamSession> session) {
154150 auto s = session.lock();
155151 return s.get() == streamSession;
156152 }),
157153 sessions_.end());
158 LOG(DEBUG) << "sessions: " << sessions_.size() << "\n";
159
160 // notify controllers if not yet done
161 ClientInfoPtr clientInfo = Config::instance().getClientInfo(session->clientId);
162 if (!clientInfo || !clientInfo->connected)
163 return;
164
165 clientInfo->connected = false;
166 chronos::systemtimeofday(&clientInfo->lastSeen);
167 saveConfig();
168 if (controlServer_ != nullptr)
169 {
170 // Check if there is no session of this client is left
171 // Can happen in case of ungraceful disconnect/reconnect or
172 // in case of a duplicate client id
173 if (getStreamSession(clientInfo->id) == nullptr)
174 {
175 // clang-format off
176 // Notification:
177 // {"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"}}
178 // clang-format on
179 json notification =
180 jsonrpcpp::Notification("Client.OnDisconnect", jsonrpcpp::Parameter("id", clientInfo->id, "client", clientInfo->toJson())).to_json();
181 controlServer_->send(notification.dump());
182 // cout << "Notification: " << notification.dump() << "\n";
183 }
184 }
154 LOG(DEBUG, LOG_TAG) << "sessions: " << sessions_.size() << "\n";
155 if (messageReceiver_)
156 messageReceiver_->onDisconnect(streamSession);
185157 cleanup();
186 }
187
188
189 void StreamServer::ProcessRequest(const jsonrpcpp::request_ptr request, jsonrpcpp::entity_ptr& response, jsonrpcpp::notification_ptr& notification) const
190 {
191 try
192 {
193 // LOG(INFO) << "StreamServer::ProcessRequest method: " << request->method << ", " << "id: " << request->id() << "\n";
194 Json result;
195
196 if (request->method().find("Client.") == 0)
197 {
198 ClientInfoPtr clientInfo = Config::instance().getClientInfo(request->params().get<std::string>("id"));
199 if (clientInfo == nullptr)
200 throw jsonrpcpp::InternalErrorException("Client not found", request->id());
201
202 if (request->method() == "Client.GetStatus")
203 {
204 // clang-format off
205 // Request: {"id":8,"jsonrpc":"2.0","method":"Client.GetStatus","params":{"id":"00:21:6a:7d:74:fc"}}
206 // 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"}}}}
207 // clang-format on
208 result["client"] = clientInfo->toJson();
209 }
210 else if (request->method() == "Client.SetVolume")
211 {
212 // clang-format off
213 // Request: {"id":8,"jsonrpc":"2.0","method":"Client.SetVolume","params":{"id":"00:21:6a:7d:74:fc","volume":{"muted":false,"percent":74}}}
214 // Response: {"id":8,"jsonrpc":"2.0","result":{"volume":{"muted":false,"percent":74}}}
215 // Notification: {"jsonrpc":"2.0","method":"Client.OnVolumeChanged","params":{"id":"00:21:6a:7d:74:fc","volume":{"muted":false,"percent":74}}}
216 // clang-format on
217
218 std::lock_guard<std::recursive_mutex> lock(clientMutex_);
219 clientInfo->config.volume.fromJson(request->params().get("volume"));
220 result["volume"] = clientInfo->config.volume.toJson();
221 notification.reset(new jsonrpcpp::Notification("Client.OnVolumeChanged",
222 jsonrpcpp::Parameter("id", clientInfo->id, "volume", clientInfo->config.volume.toJson())));
223 }
224 else if (request->method() == "Client.SetLatency")
225 {
226 // clang-format off
227 // Request: {"id":7,"jsonrpc":"2.0","method":"Client.SetLatency","params":{"id":"00:21:6a:7d:74:fc#2","latency":10}}
228 // Response: {"id":7,"jsonrpc":"2.0","result":{"latency":10}}
229 // Notification: {"jsonrpc":"2.0","method":"Client.OnLatencyChanged","params":{"id":"00:21:6a:7d:74:fc#2","latency":10}}
230 // clang-format on
231 int latency = request->params().get("latency");
232 if (latency < -10000)
233 latency = -10000;
234 else if (latency > settings_.stream.bufferMs)
235 latency = settings_.stream.bufferMs;
236 clientInfo->config.latency = latency; //, -10000, settings_.stream.bufferMs);
237 result["latency"] = clientInfo->config.latency;
238 notification.reset(
239 new jsonrpcpp::Notification("Client.OnLatencyChanged", jsonrpcpp::Parameter("id", clientInfo->id, "latency", clientInfo->config.latency)));
240 }
241 else if (request->method() == "Client.SetName")
242 {
243 // clang-format off
244 // Request: {"id":6,"jsonrpc":"2.0","method":"Client.SetName","params":{"id":"00:21:6a:7d:74:fc#2","name":"Laptop"}}
245 // Response: {"id":6,"jsonrpc":"2.0","result":{"name":"Laptop"}}
246 // Notification: {"jsonrpc":"2.0","method":"Client.OnNameChanged","params":{"id":"00:21:6a:7d:74:fc#2","name":"Laptop"}}
247 // clang-format on
248 clientInfo->config.name = request->params().get<std::string>("name");
249 result["name"] = clientInfo->config.name;
250 notification.reset(
251 new jsonrpcpp::Notification("Client.OnNameChanged", jsonrpcpp::Parameter("id", clientInfo->id, "name", clientInfo->config.name)));
252 }
253 else
254 throw jsonrpcpp::MethodNotFoundException(request->id());
255
256
257 if (request->method().find("Client.Set") == 0)
258 {
259 /// Update client
260 session_ptr session = getStreamSession(clientInfo->id);
261 if (session != nullptr)
262 {
263 auto serverSettings = make_shared<msg::ServerSettings>();
264 serverSettings->setBufferMs(settings_.stream.bufferMs);
265 serverSettings->setVolume(clientInfo->config.volume.percent);
266 GroupPtr group = Config::instance().getGroupFromClient(clientInfo);
267 serverSettings->setMuted(clientInfo->config.volume.muted || group->muted);
268 serverSettings->setLatency(clientInfo->config.latency);
269 session->sendAsync(serverSettings);
270 }
271 }
272 }
273 else if (request->method().find("Group.") == 0)
274 {
275 GroupPtr group = Config::instance().getGroup(request->params().get<std::string>("id"));
276 if (group == nullptr)
277 throw jsonrpcpp::InternalErrorException("Group not found", request->id());
278
279 if (request->method() == "Group.GetStatus")
280 {
281 // clang-format off
282 // Request: {"id":5,"jsonrpc":"2.0","method":"Group.GetStatus","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1"}}
283 // 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"}}}
284 // clang-format on
285 result["group"] = group->toJson();
286 }
287 else if (request->method() == "Group.SetName")
288 {
289 // clang-format off
290 // Request: {"id":6,"jsonrpc":"2.0","method":"Group.SetName","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","name":"Laptop"}}
291 // Response: {"id":6,"jsonrpc":"2.0","result":{"name":"MediaPlayer"}}
292 // Notification: {"jsonrpc":"2.0","method":"Group.OnNameChanged","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","MediaPlayer":"Laptop"}}
293 // clang-format on
294 group->name = request->params().get<std::string>("name");
295 result["name"] = group->name;
296 notification.reset(new jsonrpcpp::Notification("Group.OnNameChanged", jsonrpcpp::Parameter("id", group->id, "name", group->name)));
297 }
298 else if (request->method() == "Group.SetMute")
299 {
300 // clang-format off
301 // Request: {"id":5,"jsonrpc":"2.0","method":"Group.SetMute","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","mute":true}}
302 // Response: {"id":5,"jsonrpc":"2.0","result":{"mute":true}}
303 // Notification: {"jsonrpc":"2.0","method":"Group.OnMute","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","mute":true}}
304 // clang-format on
305 bool muted = request->params().get<bool>("mute");
306 group->muted = muted;
307
308 /// Update clients
309 for (auto client : group->clients)
310 {
311 session_ptr session = getStreamSession(client->id);
312 if (session != nullptr)
313 {
314 auto serverSettings = make_shared<msg::ServerSettings>();
315 serverSettings->setBufferMs(settings_.stream.bufferMs);
316 serverSettings->setVolume(client->config.volume.percent);
317 GroupPtr group = Config::instance().getGroupFromClient(client);
318 serverSettings->setMuted(client->config.volume.muted || group->muted);
319 serverSettings->setLatency(client->config.latency);
320 session->sendAsync(serverSettings);
321 }
322 }
323
324 result["mute"] = group->muted;
325 notification.reset(new jsonrpcpp::Notification("Group.OnMute", jsonrpcpp::Parameter("id", group->id, "mute", group->muted)));
326 }
327 else if (request->method() == "Group.SetStream")
328 {
329 // clang-format off
330 // Request: {"id":4,"jsonrpc":"2.0","method":"Group.SetStream","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","stream_id":"stream 1"}}
331 // Response: {"id":4,"jsonrpc":"2.0","result":{"stream_id":"stream 1"}}
332 // Notification: {"jsonrpc":"2.0","method":"Group.OnStreamChanged","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","stream_id":"stream 1"}}
333 // clang-format on
334 string streamId = request->params().get<std::string>("stream_id");
335 PcmStreamPtr stream = streamManager_->getStream(streamId);
336 if (stream == nullptr)
337 throw jsonrpcpp::InternalErrorException("Stream not found", request->id());
338
339 group->streamId = streamId;
340
341 // Update clients
342 for (auto client : group->clients)
343 {
344 session_ptr session = getStreamSession(client->id);
345 if (session && (session->pcmStream() != stream))
346 {
347 session->sendAsync(stream->getMeta());
348 session->sendAsync(stream->getHeader());
349 session->setPcmStream(stream);
350 }
351 }
352
353 // Notify others
354 result["stream_id"] = group->streamId;
355 notification.reset(new jsonrpcpp::Notification("Group.OnStreamChanged", jsonrpcpp::Parameter("id", group->id, "stream_id", group->streamId)));
356 }
357 else if (request->method() == "Group.SetClients")
358 {
359 // clang-format off
360 // 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"}}
361 // 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"}}]}}}
362 // 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"}}]}}}
363 // clang-format on
364 vector<string> clients = request->params().get("clients");
365 // Remove clients from group
366 for (auto iter = group->clients.begin(); iter != group->clients.end();)
367 {
368 auto client = *iter;
369 if (find(clients.begin(), clients.end(), client->id) != clients.end())
370 {
371 ++iter;
372 continue;
373 }
374 iter = group->clients.erase(iter);
375 GroupPtr newGroup = Config::instance().addClientInfo(client);
376 newGroup->streamId = group->streamId;
377 }
378
379 // Add clients to group
380 PcmStreamPtr stream = streamManager_->getStream(group->streamId);
381 for (const auto& clientId : clients)
382 {
383 ClientInfoPtr client = Config::instance().getClientInfo(clientId);
384 if (!client)
385 continue;
386 GroupPtr oldGroup = Config::instance().getGroupFromClient(client);
387 if (oldGroup && (oldGroup->id == group->id))
388 continue;
389
390 if (oldGroup)
391 {
392 oldGroup->removeClient(client);
393 Config::instance().remove(oldGroup);
394 }
395
396 group->addClient(client);
397
398 // assign new stream
399 session_ptr session = getStreamSession(client->id);
400 if (session && stream && (session->pcmStream() != stream))
401 {
402 session->sendAsync(stream->getMeta());
403 session->sendAsync(stream->getHeader());
404 session->setPcmStream(stream);
405 }
406 }
407
408 if (group->empty())
409 Config::instance().remove(group);
410
411 json server = Config::instance().getServerStatus(streamManager_->toJson());
412 result["server"] = server;
413
414 // Notify others: since at least two groups are affected, send a complete server update
415 notification.reset(new jsonrpcpp::Notification("Server.OnUpdate", jsonrpcpp::Parameter("server", server)));
416 }
417 else
418 throw jsonrpcpp::MethodNotFoundException(request->id());
419 }
420 else if (request->method().find("Server.") == 0)
421 {
422 if (request->method().find("Server.GetRPCVersion") == 0)
423 {
424 // Request: {"id":8,"jsonrpc":"2.0","method":"Server.GetRPCVersion"}
425 // Response: {"id":8,"jsonrpc":"2.0","result":{"major":2,"minor":0,"patch":0}}
426 // <major>: backwards incompatible change
427 result["major"] = 2;
428 // <minor>: feature addition to the API
429 result["minor"] = 0;
430 // <patch>: bugfix release
431 result["patch"] = 0;
432 }
433 else if (request->method() == "Server.GetStatus")
434 {
435 // clang-format off
436 // Request: {"id":1,"jsonrpc":"2.0","method":"Server.GetStatus"}
437 // 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"}}]}}}
438 // clang-format on
439 result["server"] = Config::instance().getServerStatus(streamManager_->toJson());
440 }
441 else if (request->method() == "Server.DeleteClient")
442 {
443 // clang-format off
444 // Request: {"id":2,"jsonrpc":"2.0","method":"Server.DeleteClient","params":{"id":"00:21:6a:7d:74:fc"}}
445 // 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"}}]}}}
446 // 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"}}]}}}
447 // clang-format on
448 ClientInfoPtr clientInfo = Config::instance().getClientInfo(request->params().get<std::string>("id"));
449 if (clientInfo == nullptr)
450 throw jsonrpcpp::InternalErrorException("Client not found", request->id());
451
452 Config::instance().remove(clientInfo);
453
454 json server = Config::instance().getServerStatus(streamManager_->toJson());
455 result["server"] = server;
456
457 /// Notify others
458 notification.reset(new jsonrpcpp::Notification("Server.OnUpdate", jsonrpcpp::Parameter("server", server)));
459 }
460 else
461 throw jsonrpcpp::MethodNotFoundException(request->id());
462 }
463 else if (request->method().find("Stream.") == 0)
464 {
465 if (request->method().find("Stream.SetMeta") == 0)
466 {
467 /// Request: {"id":4,"jsonrpc":"2.0","method":"Stream.SetMeta","params":{"id":"Spotify",
468 /// "meta": {"album": "some album", "artist": "some artist", "track": "some track"...}}}
469 ///
470 /// Response: {"id":4,"jsonrpc":"2.0","result":{"stream_id":"Spotify"}}
471 /// Call onMetaChanged(const PcmStream* pcmStream) for updates and notifications
472
473 LOG(INFO) << "Stream.SetMeta(" << request->params().get<std::string>("id") << ")" << request->params().get("meta") << "\n";
474
475 // Find stream
476 string streamId = request->params().get<std::string>("id");
477 PcmStreamPtr stream = streamManager_->getStream(streamId);
478 if (stream == nullptr)
479 throw jsonrpcpp::InternalErrorException("Stream not found", request->id());
480
481 // Set metadata from request
482 stream->setMeta(request->params().get("meta"));
483
484 // Setup response
485 result["id"] = streamId;
486 }
487 else if (request->method() == "Stream.AddStream")
488 {
489 // clang-format off
490 // Request: {"id":4,"jsonrpc":"2.0","method":"Stream.AddStream","params":{"streamUri":"uri"}}
491 // Response: {"id":4,"jsonrpc":"2.0","result":{"stream_id":"Spotify"}}
492 // Call onMetaChanged(const PcmStream* pcmStream) for updates and notifications
493 // clang-format on
494
495 LOG(INFO) << "Stream.AddStream(" << request->params().get("streamUri") << ")"
496 << "\n";
497
498 // Find stream
499 string streamUri = request->params().get("streamUri");
500 PcmStreamPtr stream = streamManager_->addStream(streamUri);
501 if (stream == nullptr)
502 throw jsonrpcpp::InternalErrorException("Stream not created", request->id());
503 stream->start(); // We start the stream, otherwise it would be silent
504 // Setup response
505 result["id"] = stream->getId();
506 }
507 else if (request->method() == "Stream.RemoveStream")
508 {
509 // clang-format off
510 // Request: {"id":4,"jsonrpc":"2.0","method":"Stream.RemoveStream","params":{"id":"Spotify"}}
511 // Response: {"id":4,"jsonrpc":"2.0","result":{"stream_id":"Spotify"}}
512 // Call onMetaChanged(const PcmStream* pcmStream) for updates and notifications
513 // clang-format on
514
515 LOG(INFO) << "Stream.RemoveStream(" << request->params().get("id") << ")"
516 << "\n";
517
518 // Find stream
519 string streamId = request->params().get("id");
520 streamManager_->removeStream(streamId);
521 // Setup response
522 result["id"] = streamId;
523 }
524 else
525 throw jsonrpcpp::MethodNotFoundException(request->id());
526 }
527 else
528 throw jsonrpcpp::MethodNotFoundException(request->id());
529
530 response.reset(new jsonrpcpp::Response(*request, result));
531 }
532 catch (const jsonrpcpp::RequestException& e)
533 {
534 LOG(ERROR) << "StreamServer::onMessageReceived JsonRequestException: " << e.to_json().dump() << ", message: " << request->to_json().dump() << "\n";
535 response.reset(new jsonrpcpp::RequestException(e));
536 }
537 catch (const exception& e)
538 {
539 LOG(ERROR) << "StreamServer::onMessageReceived exception: " << e.what() << ", message: " << request->to_json().dump() << "\n";
540 response.reset(new jsonrpcpp::InternalErrorException(e.what(), request->id()));
541 }
542 }
543
544
545 std::string StreamServer::onMessageReceived(ControlSession* controlSession, const std::string& message)
546 {
547 // LOG(DEBUG) << "onMessageReceived: " << message << "\n";
548 jsonrpcpp::entity_ptr entity(nullptr);
549 try
550 {
551 entity = jsonrpcpp::Parser::do_parse(message);
552 if (!entity)
553 return "";
554 }
555 catch (const jsonrpcpp::ParseErrorException& e)
556 {
557 return e.to_json().dump();
558 }
559 catch (const std::exception& e)
560 {
561 return jsonrpcpp::ParseErrorException(e.what()).to_json().dump();
562 }
563
564 jsonrpcpp::entity_ptr response(nullptr);
565 jsonrpcpp::notification_ptr notification(nullptr);
566 if (entity->is_request())
567 {
568 jsonrpcpp::request_ptr request = dynamic_pointer_cast<jsonrpcpp::Request>(entity);
569 ProcessRequest(request, response, notification);
570 saveConfig();
571 ////cout << "Request: " << request->to_json().dump() << "\n";
572 if (notification)
573 {
574 ////cout << "Notification: " << notification->to_json().dump() << "\n";
575 controlServer_->send(notification->to_json().dump(), controlSession);
576 }
577 if (response)
578 {
579 ////cout << "Response: " << response->to_json().dump() << "\n";
580 return response->to_json().dump();
581 }
582 return "";
583 }
584 else if (entity->is_batch())
585 {
586 jsonrpcpp::batch_ptr batch = dynamic_pointer_cast<jsonrpcpp::Batch>(entity);
587 ////cout << "Batch: " << batch->to_json().dump() << "\n";
588 jsonrpcpp::Batch responseBatch;
589 jsonrpcpp::Batch notificationBatch;
590 for (const auto& batch_entity : batch->entities)
591 {
592 if (batch_entity->is_request())
593 {
594 jsonrpcpp::request_ptr request = dynamic_pointer_cast<jsonrpcpp::Request>(batch_entity);
595 ProcessRequest(request, response, notification);
596 if (response != nullptr)
597 responseBatch.add_ptr(response);
598 if (notification != nullptr)
599 notificationBatch.add_ptr(notification);
600 }
601 }
602 saveConfig();
603 if (!notificationBatch.entities.empty())
604 controlServer_->send(notificationBatch.to_json().dump(), controlSession);
605 if (!responseBatch.entities.empty())
606 return responseBatch.to_json().dump();
607 return "";
608 }
609 return "";
610 }
611
612
613
614 void StreamServer::onMessageReceived(StreamSession* streamSession, const msg::BaseMessage& baseMessage, char* buffer)
615 {
616 // LOG(DEBUG) << "onMessageReceived: " << baseMessage.type << ", size: " << baseMessage.size << ", id: " << baseMessage.id << ", refers: " <<
617 // baseMessage.refersTo << ", sent: " << baseMessage.sent.sec << "," << baseMessage.sent.usec << ", recv: " << baseMessage.received.sec << "," <<
618 // baseMessage.received.usec << "\n";
619 if (baseMessage.type == message_type::kTime)
620 {
621 auto timeMsg = make_shared<msg::Time>();
622 timeMsg->deserialize(baseMessage, buffer);
623 timeMsg->refersTo = timeMsg->id;
624 timeMsg->latency = timeMsg->received - timeMsg->sent;
625 // LOG(INFO) << "Latency sec: " << timeMsg.latency.sec << ", usec: " << timeMsg.latency.usec << ", refers to: " << timeMsg.refersTo << "\n";
626 streamSession->sendAsync(timeMsg);
627
628 // refresh streamSession state
629 ClientInfoPtr client = Config::instance().getClientInfo(streamSession->clientId);
630 if (client != nullptr)
631 {
632 chronos::systemtimeofday(&client->lastSeen);
633 client->connected = true;
634 }
635 }
636 else if (baseMessage.type == message_type::kHello)
637 {
638 msg::Hello helloMsg;
639 helloMsg.deserialize(baseMessage, buffer);
640 streamSession->clientId = helloMsg.getUniqueId();
641 LOG(INFO) << "Hello from " << streamSession->clientId << ", host: " << helloMsg.getHostName() << ", v" << helloMsg.getVersion()
642 << ", ClientName: " << helloMsg.getClientName() << ", OS: " << helloMsg.getOS() << ", Arch: " << helloMsg.getArch()
643 << ", Protocol version: " << helloMsg.getProtocolVersion() << "\n";
644
645 LOG(DEBUG) << "request kServerSettings: " << streamSession->clientId << "\n";
646 // std::lock_guard<std::mutex> mlock(mutex_);
647 bool newGroup(false);
648 GroupPtr group = Config::instance().getGroupFromClient(streamSession->clientId);
649 if (group == nullptr)
650 {
651 group = Config::instance().addClientInfo(streamSession->clientId);
652 newGroup = true;
653 }
654
655 ClientInfoPtr client = group->getClient(streamSession->clientId);
656
657 LOG(DEBUG) << "request kServerSettings\n";
658 auto serverSettings = make_shared<msg::ServerSettings>();
659 serverSettings->setVolume(client->config.volume.percent);
660 serverSettings->setMuted(client->config.volume.muted || group->muted);
661 serverSettings->setLatency(client->config.latency);
662 serverSettings->setBufferMs(settings_.stream.bufferMs);
663 serverSettings->refersTo = helloMsg.id;
664 streamSession->sendAsync(serverSettings);
665
666 client->host.mac = helloMsg.getMacAddress();
667 client->host.ip = streamSession->getIP();
668 client->host.name = helloMsg.getHostName();
669 client->host.os = helloMsg.getOS();
670 client->host.arch = helloMsg.getArch();
671 client->snapclient.version = helloMsg.getVersion();
672 client->snapclient.name = helloMsg.getClientName();
673 client->snapclient.protocolVersion = helloMsg.getProtocolVersion();
674 client->config.instance = helloMsg.getInstance();
675 client->connected = true;
676 chronos::systemtimeofday(&client->lastSeen);
677
678 // Assign and update stream
679 PcmStreamPtr stream = streamManager_->getStream(group->streamId);
680 if (!stream)
681 {
682 stream = streamManager_->getDefaultStream();
683 group->streamId = stream->getId();
684 }
685 LOG(DEBUG) << "Group: " << group->id << ", stream: " << group->streamId << "\n";
686
687 saveConfig();
688
689 streamSession->sendAsync(stream->getMeta());
690 streamSession->setPcmStream(stream);
691 auto headerChunk = stream->getHeader();
692 streamSession->sendAsync(headerChunk);
693
694 if (newGroup)
695 {
696 // clang-format off
697 // 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"}}]}}}
698 // clang-format on
699 json server = Config::instance().getServerStatus(streamManager_->toJson());
700 json notification = jsonrpcpp::Notification("Server.OnUpdate", jsonrpcpp::Parameter("server", server)).to_json();
701 controlServer_->send(notification.dump());
702 }
703 else
704 {
705 // clang-format off
706 // 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"}}
707 // clang-format on
708 json notification = jsonrpcpp::Notification("Client.OnConnect", jsonrpcpp::Parameter("id", client->id, "client", client->toJson())).to_json();
709 controlServer_->send(notification.dump());
710 // cout << "Notification: " << notification.dump() << "\n";
711 }
712 // cout << Config::instance().getServerStatus(streamManager_->toJson()).dump(4) << "\n";
713 // cout << group->toJson().dump(4) << "\n";
714 }
715 }
716
717
718 void StreamServer::saveConfig(const std::chrono::milliseconds& deferred)
719 {
720 config_timer_.cancel();
721 config_timer_.expires_after(deferred);
722 config_timer_.async_wait([](const boost::system::error_code& ec) {
723 if (!ec)
724 {
725 LOG(DEBUG) << "Saving config\n";
726 Config::instance().save();
727 }
728 });
729158 }
730159
731160
745174
746175 session_ptr StreamServer::getStreamSession(const std::string& clientId) const
747176 {
748 // LOG(INFO) << "getStreamSession: " << mac << "\n";
177 // LOG(INFO, LOG_TAG) << "getStreamSession: " << mac << "\n";
749178 std::lock_guard<std::recursive_mutex> mlock(sessionsMutex_);
750179 for (auto session : sessions_)
751180 {
763192 if (!ec)
764193 handleAccept(std::move(socket));
765194 else
766 LOG(ERROR) << "Error while accepting socket connection: " << ec.message() << "\n";
195 LOG(ERROR, LOG_TAG) << "Error while accepting socket connection: " << ec.message() << "\n";
767196 };
768197
769198 for (auto& acceptor : acceptor_)
784213 /// experimental: turn on tcp::no_delay
785214 socket.set_option(tcp::no_delay(true));
786215
787 SLOG(NOTICE) << "StreamServer::NewConnection: " << socket.remote_endpoint().address().to_string() << endl;
788 shared_ptr<StreamSession> session = make_shared<StreamSession>(io_context_, this, std::move(socket));
789
790 session->setBufferMs(settings_.stream.bufferMs);
791 session->start();
792
793 std::lock_guard<std::recursive_mutex> mlock(sessionsMutex_);
794 sessions_.emplace_back(session);
795 cleanup();
216 LOG(NOTICE, LOG_TAG) << "StreamServer::NewConnection: " << socket.remote_endpoint().address().to_string() << endl;
217 shared_ptr<StreamSession> session = make_shared<StreamSessionTcp>(io_context_, this, std::move(socket));
218 addSession(session);
796219 }
797220 catch (const std::exception& e)
798221 {
799 SLOG(ERROR) << "Exception in StreamServer::handleAccept: " << e.what() << endl;
222 LOG(ERROR, LOG_TAG) << "Exception in StreamServer::handleAccept: " << e.what() << endl;
800223 }
801224 startAccept();
802225 }
804227
805228 void StreamServer::start()
806229 {
807 try
808 {
809 controlServer_ = std::make_unique<ControlServer>(io_context_, settings_.tcp, settings_.http, this);
810 controlServer_->start();
811
812 streamManager_ =
813 std::make_unique<StreamManager>(this, io_context_, settings_.stream.sampleFormat, settings_.stream.codec, settings_.stream.streamChunkMs);
814 // throw SnapException("xxx");
815 for (const auto& streamUri : settings_.stream.pcmStreams)
816 {
817 PcmStreamPtr stream = streamManager_->addStream(streamUri);
818 if (stream)
819 LOG(INFO) << "Stream: " << stream->getUri().toJson() << "\n";
820 }
821 streamManager_->start();
822
823 for (const auto& address : settings_.stream.bind_to_address)
824 {
825 try
826 {
827 LOG(INFO) << "Creating stream acceptor for address: " << address << ", port: " << settings_.stream.port << "\n";
828 acceptor_.emplace_back(
829 make_unique<tcp::acceptor>(io_context_, tcp::endpoint(boost::asio::ip::address::from_string(address), settings_.stream.port)));
830 }
831 catch (const boost::system::system_error& e)
832 {
833 LOG(ERROR) << "error creating TCP acceptor: " << e.what() << ", code: " << e.code() << "\n";
834 }
835 }
836
837 startAccept();
838 }
839 catch (const std::exception& e)
840 {
841 SLOG(NOTICE) << "StreamServer::start: " << e.what() << endl;
842 stop();
843 throw;
844 }
230 for (const auto& address : settings_.stream.bind_to_address)
231 {
232 try
233 {
234 LOG(INFO, LOG_TAG) << "Creating stream acceptor for address: " << address << ", port: " << settings_.stream.port << "\n";
235 acceptor_.emplace_back(
236 make_unique<tcp::acceptor>(io_context_, tcp::endpoint(boost::asio::ip::address::from_string(address), settings_.stream.port)));
237 }
238 catch (const boost::system::system_error& e)
239 {
240 LOG(ERROR, LOG_TAG) << "error creating TCP acceptor: " << e.what() << ", code: " << e.code() << "\n";
241 }
242 }
243
244 startAccept();
845245 }
846246
847247
851251 acceptor->cancel();
852252 acceptor_.clear();
853253
854 if (streamManager_)
855 {
856 streamManager_->stop();
857 streamManager_ = nullptr;
858 }
859
860 if (controlServer_)
861 {
862 controlServer_->stop();
863 controlServer_ = nullptr;
864 }
865
866254 std::lock_guard<std::recursive_mutex> mlock(sessionsMutex_);
867255 cleanup();
868256 for (auto s : sessions_)
4747 /**
4848 * Reads PCM data using PipeStream, implements PcmListener to get the (encoded) PCM stream.
4949 * Accepts and holds client connections (StreamSession)
50 * Receives (via the MessageReceiver interface) and answers messages from the clients
50 * Receives (via the StreamMessageReceiver interface) and answers messages from the clients
5151 * Forwards PCM data to the clients
5252 */
53 class StreamServer : public MessageReceiver, public ControlMessageReceiver, public PcmListener
53 class StreamServer : public StreamMessageReceiver
5454 {
5555 public:
56 StreamServer(boost::asio::io_context& io_context, const ServerSettings& serverSettings);
56 StreamServer(boost::asio::io_context& io_context, const ServerSettings& serverSettings, StreamMessageReceiver* messageReceiver = nullptr);
5757 virtual ~StreamServer();
5858
5959 void start();
6262 /// Send a message to all connceted clients
6363 // void send(const msg::BaseMessage* message);
6464
65 /// Clients call this when they receive a message. Implementation of MessageReceiver::onMessageReceived
66 void onMessageReceived(StreamSession* connection, const msg::BaseMessage& baseMessage, char* buffer) override;
67 void onDisconnect(StreamSession* connection) override;
65 void addSession(const std::shared_ptr<StreamSession>& session);
66 void onMetaChanged(const PcmStream* pcmStream, std::shared_ptr<msg::StreamTags> meta);
67 void onChunkEncoded(const PcmStream* pcmStream, bool isDefaultStream, std::shared_ptr<msg::PcmChunk> chunk, double duration);
6868
69 /// Implementation of ControllMessageReceiver::onMessageReceived, called by ControlServer::onMessageReceived
70 std::string onMessageReceived(ControlSession* connection, const std::string& message) override;
71
72 /// Implementation of PcmListener
73 void onMetaChanged(const PcmStream* pcmStream) override;
74 void onStateChanged(const PcmStream* pcmStream, const ReaderState& state) override;
75 void onChunkRead(const PcmStream* pcmStream, std::shared_ptr<msg::PcmChunk> chunk, double duration) override;
76 void onResync(const PcmStream* pcmStream, double ms) override;
69 session_ptr getStreamSession(const std::string& mac) const;
70 session_ptr getStreamSession(StreamSession* session) const;
7771
7872 private:
7973 void startAccept();
8074 void handleAccept(tcp::socket socket);
81 session_ptr getStreamSession(const std::string& mac) const;
82 session_ptr getStreamSession(StreamSession* session) const;
83 void ProcessRequest(const jsonrpcpp::request_ptr request, jsonrpcpp::entity_ptr& response, jsonrpcpp::notification_ptr& notification) const;
8475 void cleanup();
85 /// Save the server state deferred to prevent blocking and lower disk io
86 /// @param deferred the delay after the last call to saveConfig
87 void saveConfig(const std::chrono::milliseconds& deferred = std::chrono::seconds(2));
76
77 /// Implementation of StreamMessageReceiver
78 void onMessageReceived(StreamSession* connection, const msg::BaseMessage& baseMessage, char* buffer) override;
79 void onDisconnect(StreamSession* connection) override;
8880
8981 mutable std::recursive_mutex sessionsMutex_;
9082 mutable std::recursive_mutex clientMutex_;
9587
9688 ServerSettings settings_;
9789 Queue<std::shared_ptr<msg::BaseMessage>> messages_;
98 std::unique_ptr<ControlServer> controlServer_;
99 std::unique_ptr<StreamManager> streamManager_;
90 StreamMessageReceiver* messageReceiver_;
10091 };
10192
10293
2828 static constexpr auto LOG_TAG = "StreamSession";
2929
3030
31 StreamSession::StreamSession(boost::asio::io_context& ioc, MessageReceiver* receiver, tcp::socket&& socket)
32 : socket_(std::move(socket)), messageReceiver_(receiver), pcmStream_(nullptr), strand_(ioc)
31 StreamSession::StreamSession(boost::asio::io_context& ioc, StreamMessageReceiver* receiver) : messageReceiver_(receiver), pcmStream_(nullptr), strand_(ioc)
3332 {
3433 base_msg_size_ = baseMessage_.getSize();
3534 buffer_.resize(base_msg_size_);
36 }
37
38
39 StreamSession::~StreamSession()
40 {
41 stop();
42 }
43
44
45 void StreamSession::read_next()
46 {
47 boost::asio::async_read(socket_, boost::asio::buffer(buffer_, base_msg_size_),
48 boost::asio::bind_executor(strand_, [ this, self = shared_from_this() ](boost::system::error_code ec, std::size_t length) mutable {
49 if (ec)
50 {
51 LOG(ERROR, LOG_TAG) << "Error reading message header of length " << length << ": " << ec.message() << "\n";
52 messageReceiver_->onDisconnect(this);
53 return;
54 }
55
56 baseMessage_.deserialize(buffer_.data());
57 LOG(DEBUG, LOG_TAG) << "getNextMessage: " << baseMessage_.type << ", size: " << baseMessage_.size << ", id: " << baseMessage_.id
58 << ", refers: " << baseMessage_.refersTo << "\n";
59 if (baseMessage_.type > message_type::kLast)
60 {
61 LOG(ERROR, LOG_TAG) << "unknown message type received: " << baseMessage_.type << ", size: " << baseMessage_.size << "\n";
62 messageReceiver_->onDisconnect(this);
63 return;
64 }
65 else if (baseMessage_.size > msg::max_size)
66 {
67 LOG(ERROR, LOG_TAG) << "received message of type " << baseMessage_.type << " to large: " << baseMessage_.size << "\n";
68 messageReceiver_->onDisconnect(this);
69 return;
70 }
71
72 if (baseMessage_.size > buffer_.size())
73 buffer_.resize(baseMessage_.size);
74
75 boost::asio::async_read(
76 socket_, boost::asio::buffer(buffer_, baseMessage_.size),
77 boost::asio::bind_executor(strand_, [this, self](boost::system::error_code ec, std::size_t length) mutable {
78 if (ec)
79 {
80 LOG(ERROR, LOG_TAG) << "Error reading message body of length " << length << ": " << ec.message() << "\n";
81 messageReceiver_->onDisconnect(this);
82 return;
83 }
84
85 tv t;
86 baseMessage_.received = t;
87 if (messageReceiver_ != nullptr)
88 messageReceiver_->onMessageReceived(this, baseMessage_, buffer_.data());
89 read_next();
90 }));
91 }));
9235 }
9336
9437
10447 }
10548
10649
107 void StreamSession::start()
108 {
109 read_next();
110 // strand_.post([this]() { read_next(); });
111 }
112
113
114 void StreamSession::stop()
115 {
116 LOG(DEBUG, LOG_TAG) << "StreamSession::stop\n";
117 boost::system::error_code ec;
118 socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
119 if (ec)
120 LOG(ERROR, LOG_TAG) << "Error in socket shutdown: " << ec.message() << "\n";
121 socket_.close(ec);
122 if (ec)
123 LOG(ERROR, LOG_TAG) << "Error in socket close: " << ec.message() << "\n";
124 LOG(DEBUG, LOG_TAG) << "StreamSession stopped\n";
125 }
126
127
12850 void StreamSession::send_next()
12951 {
13052 auto& buffer = messages_.front();
13153 buffer.on_air = true;
132 boost::asio::async_write(socket_, buffer,
133 boost::asio::bind_executor(strand_, [ this, self = shared_from_this(), buffer ](boost::system::error_code ec, std::size_t length) {
134 messages_.pop_front();
135 if (ec)
136 {
137 LOG(ERROR, LOG_TAG) << "StreamSession write error (msg length: " << length << "): " << ec.message() << "\n";
138 messageReceiver_->onDisconnect(this);
139 return;
140 }
141 if (!messages_.empty())
142 send_next();
143 }));
54 strand_.post([ this, self = shared_from_this(), buffer ]() {
55 sendAsync(buffer, [this](boost::system::error_code ec, std::size_t length) {
56 messages_.pop_front();
57 if (ec)
58 {
59 LOG(ERROR, LOG_TAG) << "StreamSession write error (msg length: " << length << "): " << ec.message() << "\n";
60 messageReceiver_->onDisconnect(this);
61 return;
62 }
63 if (!messages_.empty())
64 send_next();
65 });
66 });
14467 }
14568
14669
147 void StreamSession::sendAsync(shared_const_buffer const_buf, bool send_now)
70 void StreamSession::send(shared_const_buffer const_buf)
14871 {
149 strand_.post([ this, self = shared_from_this(), const_buf, send_now ]() {
72 strand_.post([ this, self = shared_from_this(), const_buf ]() {
15073 // delete PCM chunks that are older than the overall buffer duration
15174 messages_.erase(std::remove_if(messages_.begin(), messages_.end(),
15275 [this](const shared_const_buffer& buffer) {
15881 }),
15982 messages_.end());
16083
161 if (send_now)
162 messages_.push_front(const_buf);
163 else
164 messages_.push_back(const_buf);
84 messages_.push_back(const_buf);
16585
16686 if (messages_.size() > 1)
16787 {
168 LOG(DEBUG, LOG_TAG) << "outstanding async_write\n";
88 LOG(TRACE, LOG_TAG) << "outstanding async_write\n";
16989 return;
17090 }
17191 send_next();
17393 }
17494
17595
176 void StreamSession::sendAsync(msg::message_ptr message, bool send_now)
96 void StreamSession::send(msg::message_ptr message)
17797 {
17898 if (!message)
17999 return;
180100
181101 // TODO: better set the timestamp in send_next for more accurate time sync
182 sendAsync(shared_const_buffer(*message), send_now);
102 send(shared_const_buffer(*message));
183103 }
184104
185105
3939
4040
4141 /// Interface: callback for a received message.
42 class MessageReceiver
42 class StreamMessageReceiver
4343 {
4444 public:
4545 virtual void onMessageReceived(StreamSession* connection, const msg::BaseMessage& baseMessage, char* buffer) = 0;
5454 {
5555 std::vector<char> data;
5656 bool is_pcm_chunk;
57 uint16_t type;
5758 chronos::time_point_clk rec_time;
5859 };
5960
6465 message.sent = t;
6566 const msg::PcmChunk* pcm_chunk = dynamic_cast<const msg::PcmChunk*>(&message);
6667 message_ = std::make_shared<Message>();
68 message_->type = message.type;
6769 message_->is_pcm_chunk = (pcm_chunk != nullptr);
6870 if (message_->is_pcm_chunk)
6971 message_->rec_time = pcm_chunk->start();
7678 }
7779
7880 // Implement the ConstBufferSequence requirements.
79 typedef boost::asio::const_buffer value_type;
80 typedef const boost::asio::const_buffer* const_iterator;
81 using value_type = boost::asio::const_buffer;
82 using const_iterator = const boost::asio::const_buffer*;
8183 const boost::asio::const_buffer* begin() const
8284 {
8385 return &buffer_;
101103 };
102104
103105
106 using WriteHandler = std::function<void(boost::system::error_code ec, std::size_t length)>;
107
104108 /// Endpoint for a connected client.
105109 /**
106110 * Endpoint for a connected client.
107111 * Messages are sent to the client with the "send" method.
108 * Received messages from the client are passed to the MessageReceiver callback
112 * Received messages from the client are passed to the StreamMessageReceiver callback
109113 */
110114 class StreamSession : public std::enable_shared_from_this<StreamSession>
111115 {
112116 public:
113 /// ctor. Received message from the client are passed to MessageReceiver
114 StreamSession(boost::asio::io_context& ioc, MessageReceiver* receiver, tcp::socket&& socket);
115 ~StreamSession();
116 void start();
117 void stop();
117 /// ctor. Received message from the client are passed to StreamMessageReceiver
118 StreamSession(boost::asio::io_context& ioc, StreamMessageReceiver* receiver);
119 virtual ~StreamSession() = default;
120
121 virtual std::string getIP() = 0;
122
123 virtual void start() = 0;
124 virtual void stop() = 0;
125
126 void setMessageReceiver(StreamMessageReceiver* receiver)
127 {
128 messageReceiver_ = receiver;
129 }
130
131 protected:
132 virtual void sendAsync(const shared_const_buffer& buffer, const WriteHandler& handler) = 0;
133
134 public:
135 /// Sends a message to the client (asynchronous)
136 void send(msg::message_ptr message);
118137
119138 /// Sends a message to the client (asynchronous)
120 void sendAsync(msg::message_ptr message, bool send_now = false);
121
122 /// Sends a message to the client (asynchronous)
123 void sendAsync(shared_const_buffer const_buf, bool send_now = false);
139 void send(shared_const_buffer const_buf);
124140
125141 /// Max playout latency. No need to send PCM data that is older than bufferMs
126142 void setBufferMs(size_t bufferMs);
127143
128144 std::string clientId;
129145
130 std::string getIP()
131 {
132 return socket_.remote_endpoint().address().to_string();
133 }
134
135146 void setPcmStream(streamreader::PcmStreamPtr pcmStream);
136147 const streamreader::PcmStreamPtr pcmStream() const;
137148
138149 protected:
139 void read_next();
140150 void send_next();
141151
142152 msg::BaseMessage baseMessage_;
143153 std::vector<char> buffer_;
144154 size_t base_msg_size_;
145 tcp::socket socket_;
146 MessageReceiver* messageReceiver_;
155 StreamMessageReceiver* messageReceiver_;
147156 size_t bufferMs_;
148157 streamreader::PcmStreamPtr pcmStream_;
149158 boost::asio::io_context::strand strand_;
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 "stream_session_tcp.hpp"
19
20 #include "common/aixlog.hpp"
21 #include "message/pcm_chunk.hpp"
22 #include <iostream>
23
24 using namespace std;
25 using namespace streamreader;
26
27
28 static constexpr auto LOG_TAG = "StreamSessionTCP";
29
30
31 StreamSessionTcp::StreamSessionTcp(boost::asio::io_context& ioc, StreamMessageReceiver* receiver, tcp::socket&& socket)
32 : StreamSession(ioc, receiver), socket_(std::move(socket))
33 {
34 }
35
36
37 StreamSessionTcp::~StreamSessionTcp()
38 {
39 LOG(DEBUG, LOG_TAG) << "~StreamSessionTcp\n";
40 stop();
41 }
42
43
44 void StreamSessionTcp::start()
45 {
46 read_next();
47 }
48
49
50 void StreamSessionTcp::stop()
51 {
52 LOG(DEBUG, LOG_TAG) << "stop\n";
53 if (socket_.is_open())
54 {
55 boost::system::error_code ec;
56 socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
57 if (ec)
58 LOG(ERROR, LOG_TAG) << "Error in socket shutdown: " << ec.message() << "\n";
59 socket_.close(ec);
60 if (ec)
61 LOG(ERROR, LOG_TAG) << "Error in socket close: " << ec.message() << "\n";
62 LOG(DEBUG, LOG_TAG) << "stopped\n";
63 }
64 }
65
66
67 std::string StreamSessionTcp::getIP()
68 {
69 try
70 {
71 return socket_.remote_endpoint().address().to_string();
72 }
73 catch (...)
74 {
75 return "0.0.0.0";
76 }
77 }
78
79
80 void StreamSessionTcp::read_next()
81 {
82 boost::asio::async_read(socket_, boost::asio::buffer(buffer_, base_msg_size_),
83 boost::asio::bind_executor(strand_, [ this, self = shared_from_this() ](boost::system::error_code ec, std::size_t length) mutable {
84 if (ec)
85 {
86 LOG(ERROR, LOG_TAG) << "Error reading message header of length " << length << ": " << ec.message() << "\n";
87 messageReceiver_->onDisconnect(this);
88 return;
89 }
90
91 baseMessage_.deserialize(buffer_.data());
92 LOG(DEBUG, LOG_TAG) << "getNextMessage: " << baseMessage_.type << ", size: " << baseMessage_.size << ", id: " << baseMessage_.id
93 << ", refers: " << baseMessage_.refersTo << "\n";
94 if (baseMessage_.type > message_type::kLast)
95 {
96 LOG(ERROR, LOG_TAG) << "unknown message type received: " << baseMessage_.type << ", size: " << baseMessage_.size << "\n";
97 messageReceiver_->onDisconnect(this);
98 return;
99 }
100 else if (baseMessage_.size > msg::max_size)
101 {
102 LOG(ERROR, LOG_TAG) << "received message of type " << baseMessage_.type << " to large: " << baseMessage_.size << "\n";
103 messageReceiver_->onDisconnect(this);
104 return;
105 }
106
107 if (baseMessage_.size > buffer_.size())
108 buffer_.resize(baseMessage_.size);
109
110 boost::asio::async_read(
111 socket_, boost::asio::buffer(buffer_, baseMessage_.size),
112 boost::asio::bind_executor(strand_, [this, self](boost::system::error_code ec, std::size_t length) mutable {
113 if (ec)
114 {
115 LOG(ERROR, LOG_TAG) << "Error reading message body of length " << length << ": " << ec.message() << "\n";
116 messageReceiver_->onDisconnect(this);
117 return;
118 }
119
120 tv t;
121 baseMessage_.received = t;
122 if (messageReceiver_ != nullptr)
123 messageReceiver_->onMessageReceived(this, baseMessage_, buffer_.data());
124 read_next();
125 }));
126 }));
127 }
128
129
130 void StreamSessionTcp::sendAsync(const shared_const_buffer& buffer, const WriteHandler& handler)
131 {
132 boost::asio::async_write(socket_, buffer,
133 boost::asio::bind_executor(strand_, [ self = shared_from_this(), buffer, handler ](boost::system::error_code ec,
134 std::size_t length) { handler(ec, length); }));
135 }
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_SESSION_TCP_HPP
19 #define STREAM_SESSION_TCP_HPP
20
21 #include "stream_session.hpp"
22
23 using boost::asio::ip::tcp;
24
25
26 /// Endpoint for a connected client.
27 /**
28 * Endpoint for a connected client.
29 * Messages are sent to the client with the "send" method.
30 * Received messages from the client are passed to the StreamMessageReceiver callback
31 */
32 class StreamSessionTcp : public StreamSession
33 {
34 public:
35 /// ctor. Received message from the client are passed to StreamMessageReceiver
36 StreamSessionTcp(boost::asio::io_context& ioc, StreamMessageReceiver* receiver, tcp::socket&& socket);
37 ~StreamSessionTcp() override;
38 void start() override;
39 void stop() override;
40 std::string getIP() override;
41
42 protected:
43 void read_next();
44 void sendAsync(const shared_const_buffer& buffer, const WriteHandler& handler) override;
45
46 private:
47 tcp::socket socket_;
48 };
49
50
51
52 #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 "stream_session_ws.hpp"
19 #include "common/aixlog.hpp"
20 #include "message/pcm_chunk.hpp"
21 #include <iostream>
22
23 using namespace std;
24
25 static constexpr auto LOG_TAG = "StreamSessionWS";
26
27
28 StreamSessionWebsocket::StreamSessionWebsocket(boost::asio::io_context& ioc, StreamMessageReceiver* receiver, websocket::stream<beast::tcp_stream>&& socket)
29 : StreamSession(ioc, receiver), ws_(std::move(socket))
30 {
31 LOG(DEBUG, LOG_TAG) << "StreamSessionWS\n";
32 }
33
34
35 StreamSessionWebsocket::~StreamSessionWebsocket()
36 {
37 LOG(DEBUG, LOG_TAG) << "~StreamSessionWS\n";
38 stop();
39 }
40
41
42 void StreamSessionWebsocket::start()
43 {
44 // Read a message
45 LOG(DEBUG, LOG_TAG) << "start\n";
46 ws_.binary(true);
47 do_read_ws();
48 }
49
50
51 void StreamSessionWebsocket::stop()
52 {
53 if (ws_.is_open())
54 {
55 boost::beast::error_code ec;
56 ws_.close(beast::websocket::close_code::normal, ec);
57 if (ec)
58 LOG(ERROR, LOG_TAG) << "Error in socket close: " << ec.message() << "\n";
59 }
60 }
61
62
63 std::string StreamSessionWebsocket::getIP()
64 {
65 try
66 {
67 return ws_.next_layer().socket().remote_endpoint().address().to_string();
68 }
69 catch (...)
70 {
71 return "0.0.0.0";
72 }
73 }
74
75
76 void StreamSessionWebsocket::sendAsync(const shared_const_buffer& buffer, const WriteHandler& handler)
77 {
78 LOG(TRACE, LOG_TAG) << "sendAsync: " << buffer.message().type << "\n";
79 ws_.async_write(buffer, boost::asio::bind_executor(strand_, [ self = shared_from_this(), buffer, handler ](boost::system::error_code ec,
80 std::size_t length) { handler(ec, length); }));
81 }
82
83
84 void StreamSessionWebsocket::do_read_ws()
85 {
86 // Read a message into our buffer
87 ws_.async_read(buffer_, boost::asio::bind_executor(strand_, [ this, self = shared_from_this() ](beast::error_code ec, std::size_t bytes_transferred) {
88 on_read_ws(ec, bytes_transferred);
89 }));
90 }
91
92
93 void StreamSessionWebsocket::on_read_ws(beast::error_code ec, std::size_t bytes_transferred)
94 {
95 LOG(DEBUG, LOG_TAG) << "on_read_ws, ec: " << ec << ", bytes_transferred: " << bytes_transferred << "\n";
96 boost::ignore_unused(bytes_transferred);
97
98 // This indicates that the session was closed
99 if (ec == websocket::error::closed)
100 {
101 messageReceiver_->onDisconnect(this);
102 return;
103 }
104
105 if (ec)
106 {
107 LOG(ERROR, LOG_TAG) << "ControlSessionWebsocket::on_read_ws error: " << ec.message() << "\n";
108 messageReceiver_->onDisconnect(this);
109 return;
110 }
111
112 auto data = boost::asio::buffer_cast<char*>(buffer_.data());
113 baseMessage_.deserialize(data);
114 LOG(DEBUG, LOG_TAG) << "getNextMessage: " << baseMessage_.type << ", size: " << baseMessage_.size << ", id: " << baseMessage_.id
115 << ", refers: " << baseMessage_.refersTo << "\n";
116
117 tv t;
118 baseMessage_.received = t;
119 if (messageReceiver_ != nullptr)
120 messageReceiver_->onMessageReceived(this, baseMessage_, data + base_msg_size_);
121
122 buffer_.consume(bytes_transferred);
123 do_read_ws();
124 }
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_SESSION_WS_HPP
19 #define STREAM_SESSION_WS_HPP
20
21 #include "stream_session.hpp"
22 #include <boost/beast/core.hpp>
23 #include <boost/beast/websocket.hpp>
24 #include <deque>
25
26 namespace beast = boost::beast; // from <boost/beast.hpp>
27 namespace http = beast::http; // from <boost/beast/http.hpp>
28 namespace websocket = beast::websocket; // from <boost/beast/websocket.hpp>
29 namespace net = boost::asio; // from <boost/asio.hpp>
30
31
32 /// Endpoint for a connected control client.
33 /**
34 * Endpoint for a connected control client.
35 * Messages are sent to the client with the "send" method.
36 * Received messages from the client are passed to the ControlMessageReceiver callback
37 */
38 class StreamSessionWebsocket : public StreamSession
39 {
40 public:
41 /// ctor. Received message from the client are passed to StreamMessageReceiver
42 StreamSessionWebsocket(boost::asio::io_context& ioc, StreamMessageReceiver* receiver, websocket::stream<beast::tcp_stream>&& socket);
43 ~StreamSessionWebsocket() override;
44 void start() override;
45 void stop() override;
46 std::string getIP() override;
47
48 protected:
49 // Websocket methods
50 void sendAsync(const shared_const_buffer& buffer, const WriteHandler& handler) override;
51 void on_read_ws(beast::error_code ec, std::size_t bytes_transferred);
52 void do_read_ws();
53
54 websocket::stream<beast::tcp_stream> ws_;
55
56 protected:
57 beast::flat_buffer buffer_;
58 };
59
60
61
62 #endif
3333 {
3434 string hex2str(string input)
3535 {
36 typedef unsigned char byte;
36 using byte = unsigned char;
3737 unsigned long x = strtoul(input.c_str(), nullptr, 16);
3838 byte a[] = {byte(x >> 24), byte(x >> 16), byte(x >> 8), byte(x), 0};
3939 return string((char*)a);
231231 boost::asio::async_read_until(*pipe_fd_, streambuf_pipe_, delimiter, [this, delimiter](const std::error_code& ec, std::size_t bytes_transferred) {
232232 if (ec)
233233 {
234 if (ec.value() == boost::asio::error::eof)
234 if ((ec.value() == boost::asio::error::eof) || (ec.value() == boost::asio::error::bad_descriptor))
235235 {
236236 // For some reason, EOF is returned until the first metadata is written to the pipe.
237 // Is this a boost bug?
237 // If shairport-sync has not finished setting up the pipe, bad file descriptor is returned.
238238 LOG(INFO, LOG_TAG) << "Waiting for metadata, retrying in 2500ms"
239239 << "\n";
240240 wait(pipe_open_timer_, 2500ms, [this] { pipeReadLine(); });
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
28 #include "alsa_stream.hpp"
29
30
31 using namespace std;
32 using namespace std::chrono_literals;
33
34 namespace streamreader
35 {
36
37 static constexpr auto LOG_TAG = "AlsaStream";
38 static constexpr auto kResyncTolerance = 50ms;
39
40 // https://superuser.com/questions/597227/linux-arecord-capture-sound-card-output-rather-than-microphone-input
41 // https://wiki.ubuntuusers.de/.asoundrc/
42 // https://alsa.opensrc.org/Dsnoop#The_dsnoop_howto
43 // https://linuxconfig.org/how-to-test-microphone-with-audio-linux-sound-architecture-alsa
44 // https://www.alsa-project.org/alsa-doc/alsa-lib/_2test_2latency_8c-example.html#a30
45
46
47 namespace
48 {
49 template <typename Rep, typename Period>
50 void wait(boost::asio::steady_timer& timer, const std::chrono::duration<Rep, Period>& duration, std::function<void()> handler)
51 {
52 timer.expires_after(duration);
53 timer.async_wait([handler = std::move(handler)](const boost::system::error_code& ec) {
54 if (ec)
55 {
56 LOG(ERROR, LOG_TAG) << "Error during async wait: " << ec.message() << "\n";
57 }
58 else
59 {
60 handler();
61 }
62 });
63 }
64 } // namespace
65
66
67 AlsaStream::AlsaStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri)
68 : PcmStream(pcmListener, ioc, uri), handle_(nullptr), read_timer_(ioc), silence_(0ms)
69 {
70 device_ = uri_.getQuery("device", "hw:0");
71 LOG(DEBUG, LOG_TAG) << "Device: " << device_ << "\n";
72 }
73
74
75 void AlsaStream::start()
76 {
77 LOG(DEBUG, LOG_TAG) << "Start, sampleformat: " << sampleFormat_.toString() << "\n";
78
79 // idle_bytes_ = 0;
80 // max_idle_bytes_ = sampleFormat_.rate() * sampleFormat_.frameSize() * dryout_ms_ / 1000;
81
82 chunk_ = std::make_unique<msg::PcmChunk>(sampleFormat_, chunk_ms_);
83 silent_chunk_ = std::vector<char>(chunk_->payloadSize, 0);
84 LOG(DEBUG, LOG_TAG) << "Chunk duration: " << chunk_->durationMs() << " ms, frames: " << chunk_->getFrameCount() << ", size: " << chunk_->payloadSize
85 << "\n";
86 first_ = true;
87 tvEncodedChunk_ = std::chrono::steady_clock::now();
88 initAlsa();
89 PcmStream::start();
90 // wait(read_timer_, std::chrono::milliseconds(chunk_ms_), [this] { do_read(); });
91 do_read();
92 }
93
94
95 void AlsaStream::stop()
96 {
97 PcmStream::stop();
98 uninitAlsa();
99 }
100
101
102 void AlsaStream::initAlsa()
103 {
104 int err;
105 unsigned int rate = sampleFormat_.rate();
106 snd_pcm_format_t snd_pcm_format;
107 if (sampleFormat_.bits() == 8)
108 snd_pcm_format = SND_PCM_FORMAT_S8;
109 else if (sampleFormat_.bits() == 16)
110 snd_pcm_format = SND_PCM_FORMAT_S16_LE;
111 else if ((sampleFormat_.bits() == 24) && (sampleFormat_.sampleSize() == 4))
112 snd_pcm_format = SND_PCM_FORMAT_S24_LE;
113 else if (sampleFormat_.bits() == 32)
114 snd_pcm_format = SND_PCM_FORMAT_S32_LE;
115 else
116 throw SnapException("Unsupported sample format: " + cpt::to_string(sampleFormat_.bits()));
117
118 if ((err = snd_pcm_open(&handle_, device_.c_str(), SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK)) < 0) // SND_PCM_NONBLOCK
119 throw SnapException("Can't open device '" + device_ + "', error: " + snd_strerror(err));
120
121 snd_pcm_hw_params_t* hw_params;
122 if ((err = snd_pcm_hw_params_malloc(&hw_params)) < 0)
123 throw SnapException("Can't allocate hardware parameter structure: " + string(snd_strerror(err)));
124
125 if ((err = snd_pcm_hw_params_any(handle_, hw_params)) < 0)
126 throw SnapException("Can't fill params: " + string(snd_strerror(err)));
127
128 if ((err = snd_pcm_hw_params_set_access(handle_, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0)
129 throw SnapException("Can't set interleaved mode: " + string(snd_strerror(err)));
130
131 if ((err = snd_pcm_hw_params_set_format(handle_, hw_params, snd_pcm_format)) < 0)
132 throw SnapException("Can't set sample format: " + string(snd_strerror(err)));
133
134 if ((err = snd_pcm_hw_params_set_rate_near(handle_, hw_params, &rate, 0)) < 0)
135 throw SnapException("Can't set rate: " + string(snd_strerror(err)));
136
137 if ((err = snd_pcm_hw_params_set_channels(handle_, hw_params, sampleFormat_.channels())) < 0)
138 throw SnapException("Can't set channel count: " + string(snd_strerror(err)));
139
140 if ((err = snd_pcm_hw_params(handle_, hw_params)) < 0)
141 throw SnapException("Can't set hardware parameters: " + string(snd_strerror(err)));
142
143 snd_pcm_hw_params_free(hw_params);
144
145 if ((err = snd_pcm_prepare(handle_)) < 0)
146 throw SnapException("Can't prepare audio interface for use: " + string(snd_strerror(err)));
147 }
148
149
150 void AlsaStream::uninitAlsa()
151 {
152 if (handle_)
153 {
154 snd_pcm_close(handle_);
155 handle_ = nullptr;
156 }
157 }
158
159
160 void AlsaStream::do_read()
161 {
162 try
163 {
164 if (first_)
165 {
166 LOG(TRACE, LOG_TAG) << "First read, initializing nextTick to now\n";
167 nextTick_ = std::chrono::steady_clock::now();
168 }
169
170 int toRead = chunk_->payloadSize;
171 auto duration = chunk_->duration<std::chrono::nanoseconds>();
172 int len = 0;
173 do
174 {
175 int count = snd_pcm_readi(handle_, chunk_->payload + len, (toRead - len) / chunk_->format.frameSize());
176 if (count == -EAGAIN)
177 {
178 LOG(INFO, LOG_TAG) << "No data availabale, playing silence.\n";
179 // no data available, fill with silence
180 memset(chunk_->payload + len, 0, toRead - len);
181 // idle_bytes_ += toRead - len;
182 break;
183 }
184 else if (count == 0)
185 {
186 throw SnapException("end of file");
187 }
188 else if (count < 0)
189 {
190 // ESTRPIPE
191 LOG(ERROR, LOG_TAG) << "Error reading PCM data: " << snd_strerror(count) << " (code: " << count << ")\n";
192 first_ = true;
193 uninitAlsa();
194 initAlsa();
195 continue;
196 }
197 else
198 {
199 // LOG(TRACE, LOG_TAG) << "count: " << count << ", len: " << len << ", toRead: " << toRead << "\n";
200 len += count * chunk_->format.frameSize();
201 }
202 } while (len < toRead);
203
204 if (std::memcmp(chunk_->payload, silent_chunk_.data(), silent_chunk_.size()) == 0)
205 {
206 silence_ += chunk_->duration<std::chrono::microseconds>();
207 if (silence_ > 100ms)
208 {
209 setState(ReaderState::kIdle);
210 }
211 }
212 else
213 {
214 silence_ = 0ms;
215 setState(ReaderState::kPlaying);
216 }
217
218 // LOG(DEBUG, LOG_TAG) << "Received " << len << "/" << toRead << " bytes\n";
219 if (first_)
220 {
221 first_ = false;
222 // initialize the stream's base timestamp to now minus the chunk's duration
223 tvEncodedChunk_ = std::chrono::steady_clock::now() - duration;
224 }
225
226 chunkRead(*chunk_);
227
228 nextTick_ += duration;
229 auto currentTick = std::chrono::steady_clock::now();
230 auto next_read = nextTick_ - currentTick;
231 if (next_read >= 0ms)
232 {
233 // LOG(DEBUG, LOG_TAG) << "Next read: " << std::chrono::duration_cast<std::chrono::milliseconds>(next_read).count() << "\n";
234 // synchronize reads to an interval of chunk_ms_
235 wait(read_timer_, nextTick_ - currentTick, [this] { do_read(); });
236 return;
237 }
238 else if (next_read >= -kResyncTolerance)
239 {
240 LOG(INFO, LOG_TAG) << "next read < 0 (" << getName() << "): " << std::chrono::duration_cast<std::chrono::microseconds>(next_read).count() / 1000.
241 << " ms\n ";
242 do_read();
243 }
244 else
245 {
246 // reading chunk_ms_ took longer than chunk_ms_
247 resync(-next_read);
248 first_ = true;
249 wait(read_timer_, nextTick_ - currentTick, [this] { do_read(); });
250 }
251
252 lastException_ = "";
253 }
254 catch (const std::exception& e)
255 {
256 if (lastException_ != e.what())
257 {
258 LOG(ERROR, LOG_TAG) << "Exception: " << e.what() << std::endl;
259 lastException_ = e.what();
260 }
261 first_ = true;
262 uninitAlsa();
263 initAlsa();
264 wait(read_timer_, 100ms, [this] { do_read(); });
265 }
266 }
267
268 } // 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 ALSA_STREAM_HPP
19 #define ALSA_STREAM_HPP
20
21 #include "pcm_stream.hpp"
22 #include <alsa/asoundlib.h>
23 #include <boost/asio.hpp>
24
25 namespace streamreader
26 {
27
28
29 /// Reads and decodes PCM data from an alsa audio device device
30 /**
31 * Reads PCM from an alsa audio device device 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 AlsaStream : public PcmStream
36 {
37 public:
38 /// ctor. Encoded PCM data is passed to the PipeListener
39 AlsaStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri);
40
41 void start() override;
42 void stop() override;
43
44 protected:
45 void do_read();
46 void initAlsa();
47 void uninitAlsa();
48
49 snd_pcm_t* handle_;
50 std::unique_ptr<msg::PcmChunk> chunk_;
51 bool first_;
52 std::chrono::time_point<std::chrono::steady_clock> nextTick_;
53 boost::asio::steady_timer read_timer_;
54 std::string device_;
55 std::vector<char> silent_chunk_;
56 std::chrono::microseconds silence_;
57 std::string lastException_;
58 };
59
60 } // namespace streamreader
61
62 #endif
8585 : PcmStream(pcmListener, ioc, uri), read_timer_(ioc), state_timer_(ioc)
8686 {
8787 chunk_ = std::make_unique<msg::PcmChunk>(sampleFormat_, chunk_ms_);
88 LOG(DEBUG) << "Chunk duration: " << chunk_->durationMs() << " ms, frames: " << chunk_->getFrameCount() << ", size: " << chunk_->payloadSize << "\n";
88 LOG(DEBUG, "AsioStream") << "Chunk duration: " << chunk_->durationMs() << " ms, frames: " << chunk_->getFrameCount() << ", size: " << chunk_->payloadSize
89 << "\n";
8990
9091 bytes_read_ = 0;
9192 buffer_ms_ = 50;
118119 template <typename ReadStream>
119120 void AsioStream<ReadStream>::start()
120121 {
121 encoder_->init(this, sampleFormat_);
122 active_ = true;
122 PcmStream::start();
123123 check_state();
124124 connect();
125125 }
196196 nextTick_ = std::chrono::steady_clock::now();
197197 }
198198
199 encoder_->encode(chunk_.get());
199 chunkRead(*chunk_);
200200 nextTick_ += chunk_->duration<std::chrono::nanoseconds>();
201201 auto currentTick = std::chrono::steady_clock::now();
202202
219219 // Read took longer, wait for the buffer to fill up
220220 else
221221 {
222 pcmListener_->onResync(this, std::chrono::duration_cast<std::chrono::milliseconds>(currentTick - nextTick_).count());
222 resync(std::chrono::duration_cast<std::chrono::nanoseconds>(currentTick - nextTick_));
223223 nextTick_ = currentTick + std::chrono::milliseconds(buffer_ms_);
224224 first_ = true;
225225 do_read();
3838 string username = uri_.getQuery("username", "");
3939 string password = uri_.getQuery("password", "");
4040 string cache = uri_.getQuery("cache", "");
41 bool disable_audio_cache = (uri_.getQuery("disable_audio_cache", "false") == "true");
4142 string volume = uri_.getQuery("volume", "100");
4243 string bitrate = uri_.getQuery("bitrate", "320");
4344 string devicename = uri_.getQuery("devicename", "Snapcast");
4950 if (username.empty() != password.empty())
5051 throw SnapException("missing parameter \"username\" or \"password\" (must provide both, or neither)");
5152
52 params_ = "--name \"" + devicename + "\"";
53 if (!params_.empty())
54 params_ += " ";
55 params_ += "--name \"" + devicename + "\"";
5356 if (!username.empty() && !password.empty())
5457 params_ += " --username \"" + username + "\" --password \"" + password + "\"";
5558 params_ += " --bitrate " + bitrate + " --backend pipe";
5659 if (!cache.empty())
5760 params_ += " --cache \"" + cache + "\"";
61 if (disable_audio_cache)
62 params_ += " --disable-audio-cache";
5863 if (!volume.empty())
5964 params_ += " --initial-volume " + volume;
6065 if (!onevent.empty())
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 "meta_stream.hpp"
19 #include "common/aixlog.hpp"
20 #include "common/snap_exception.hpp"
21 #include "common/utils/string_utils.hpp"
22 #include "encoder/encoder_factory.hpp"
23
24
25 using namespace std;
26
27 namespace streamreader
28 {
29
30 static constexpr auto LOG_TAG = "MetaStream";
31 // static constexpr auto kResyncTolerance = 50ms;
32
33
34 MetaStream::MetaStream(PcmListener* pcmListener, std::vector<std::shared_ptr<PcmStream>> streams, boost::asio::io_context& ioc, const StreamUri& uri)
35 : PcmStream(pcmListener, ioc, uri), first_read_(true)
36 {
37 auto path_components = utils::string::split(uri.path, '/');
38 for (const auto& component : path_components)
39 {
40 if (component.empty())
41 continue;
42
43 bool found = false;
44 for (const auto stream : streams)
45 {
46 if (stream->getName() == component)
47 {
48 streams_.push_back(stream);
49 stream->addListener(this);
50 found = true;
51 break;
52 }
53 }
54 if (!found)
55 throw SnapException("Unknown stream: \"" + component + "\"");
56 }
57
58 if (!streams_.empty())
59 {
60 active_stream_ = streams_.front();
61 resampler_ = make_unique<Resampler>(active_stream_->getSampleFormat(), sampleFormat_);
62 }
63 }
64
65
66 MetaStream::~MetaStream()
67 {
68 stop();
69 }
70
71
72 void MetaStream::start()
73 {
74 LOG(DEBUG, LOG_TAG) << "Start, sampleformat: " << sampleFormat_.toString() << "\n";
75 PcmStream::start();
76 }
77
78
79 void MetaStream::stop()
80 {
81 active_ = false;
82 }
83
84
85 void MetaStream::onMetaChanged(const PcmStream* pcmStream)
86 {
87 LOG(DEBUG, LOG_TAG) << "onMetaChanged: " << pcmStream->getName() << "\n";
88 std::lock_guard<std::mutex> lock(mutex_);
89 if (pcmStream != active_stream_.get())
90 return;
91 }
92
93
94 void MetaStream::onStateChanged(const PcmStream* pcmStream, ReaderState state)
95 {
96 LOG(DEBUG, LOG_TAG) << "onStateChanged: " << pcmStream->getName() << ", state: " << static_cast<int>(state) << "\n";
97 std::lock_guard<std::mutex> lock(mutex_);
98 for (const auto stream : streams_)
99 {
100 if (stream->getState() == ReaderState::kPlaying)
101 {
102 if (state_ != ReaderState::kPlaying) // || (active_stream_ != stream))
103 first_read_ = true;
104
105 if (active_stream_ != stream)
106 {
107 LOG(INFO, LOG_TAG) << "Stream: " << name_ << ", switching active stream: " << (active_stream_ ? active_stream_->getName() : "<null>") << " => "
108 << stream->getName() << "\n";
109 active_stream_ = stream;
110 resampler_ = make_unique<Resampler>(active_stream_->getSampleFormat(), sampleFormat_);
111 }
112
113 setState(ReaderState::kPlaying);
114 return;
115 }
116 }
117 active_stream_ = nullptr;
118 setState(ReaderState::kIdle);
119 }
120
121
122 void MetaStream::onChunkRead(const PcmStream* pcmStream, const msg::PcmChunk& chunk)
123 {
124 // LOG(TRACE, LOG_TAG) << "onChunkRead: " << pcmStream->getName() << ", duration: " << chunk.durationMs() << "\n";
125 std::lock_guard<std::mutex> lock(mutex_);
126 if (pcmStream != active_stream_.get())
127 return;
128 // active_stream_->sampleFormat_
129 // sampleFormat_
130
131 if (first_read_)
132 {
133 first_read_ = false;
134 LOG(INFO, LOG_TAG) << "first read, updating timestamp\n";
135 tvEncodedChunk_ = std::chrono::steady_clock::now() - chunk.duration<std::chrono::nanoseconds>();
136 next_tick_ = std::chrono::steady_clock::now();
137 }
138
139
140 next_tick_ += chunk.duration<std::chrono::nanoseconds>();
141 auto currentTick = std::chrono::steady_clock::now();
142 auto next_read = next_tick_ - currentTick;
143
144 // Read took longer, wait for the buffer to fill up
145 if (next_read < 0ms)
146 {
147 // if (next_read >= -kResyncTolerance)
148 // {
149 // LOG(INFO, LOG_TAG) << "next read < 0 (" << getName() << "): " << std::chrono::duration_cast<std::chrono::microseconds>(next_read).count() / 1000.
150 // << " ms\n";
151 // }
152 // else
153 // {
154 resync(-next_read);
155 first_read_ = true;
156 // }
157 }
158
159 if (resampler_ && resampler_->resamplingNeeded())
160 {
161 auto resampled_chunk = resampler_->resample(chunk);
162 if (resampled_chunk)
163 chunkRead(*resampled_chunk);
164 }
165 else
166 chunkRead(chunk);
167 }
168
169
170 void MetaStream::onChunkEncoded(const PcmStream* pcmStream, std::shared_ptr<msg::PcmChunk> chunk, double duration)
171 {
172 std::ignore = pcmStream;
173 std::ignore = chunk;
174 std::ignore = duration;
175 // LOG(TRACE, LOG_TAG) << "onChunkEncoded: " << pcmStream->getName() << ", duration: " << duration << "\n";
176 // chunkEncoded(*encoder_, chunk, duration);
177 }
178
179
180 void MetaStream::onResync(const PcmStream* pcmStream, double ms)
181 {
182 LOG(DEBUG, LOG_TAG) << "onResync: " << pcmStream->getName() << ", duration: " << ms << " ms\n";
183 std::lock_guard<std::mutex> lock(mutex_);
184 if (pcmStream != active_stream_.get())
185 return;
186 resync(std::chrono::nanoseconds(static_cast<int64_t>(ms * 1000000)));
187 }
188
189
190 } // 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 META_STREAM_HPP
19 #define META_STREAM_HPP
20
21 #include "posix_stream.hpp"
22 #include "resampler.hpp"
23 #include <memory>
24
25 namespace streamreader
26 {
27
28
29 /// Reads and decodes PCM data
30 /**
31 * Reads PCM 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 MetaStream : public PcmStream, public PcmListener
36 {
37 public:
38 /// ctor. Encoded PCM data is passed to the PcmListener
39 MetaStream(PcmListener* pcmListener, std::vector<std::shared_ptr<PcmStream>> streams, boost::asio::io_context& ioc, const StreamUri& uri);
40 virtual ~MetaStream();
41
42 void start() override;
43 void stop() override;
44
45 protected:
46 /// Implementation of PcmListener
47 void onMetaChanged(const PcmStream* pcmStream) override;
48 void onStateChanged(const PcmStream* pcmStream, ReaderState state) override;
49 void onChunkRead(const PcmStream* pcmStream, const msg::PcmChunk& chunk) override;
50 void onChunkEncoded(const PcmStream* pcmStream, std::shared_ptr<msg::PcmChunk> chunk, double duration) override;
51 void onResync(const PcmStream* pcmStream, double ms) override;
52
53 protected:
54 std::vector<std::shared_ptr<PcmStream>> streams_;
55 std::shared_ptr<PcmStream> active_stream_;
56 std::mutex mutex_;
57 std::unique_ptr<Resampler> resampler_;
58 bool first_read_;
59 std::chrono::time_point<std::chrono::steady_clock> next_tick_;
60 };
61
62 } // namespace streamreader
63
64 #endif
3535
3636
3737 PcmStream::PcmStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri)
38 : active_(false), pcmListener_(pcmListener), uri_(uri), chunk_ms_(20), state_(ReaderState::kIdle), ioc_(ioc)
38 : active_(false), pcmListeners_{pcmListener}, uri_(uri), chunk_ms_(20), state_(ReaderState::kIdle), ioc_(ioc)
3939 {
4040 encoder::EncoderFactory encoderFactory;
4141 if (uri_.query.find(kUriCodec) == uri_.query.end())
4949 if (uri_.query.find(kUriSampleFormat) == uri_.query.end())
5050 throw SnapException("Stream URI must have a sampleformat");
5151 sampleFormat_ = SampleFormat(uri_.query[kUriSampleFormat]);
52 LOG(INFO, LOG_TAG) << "PcmStream sampleFormat: " << sampleFormat_.getFormat() << "\n";
52 LOG(INFO, LOG_TAG) << "PcmStream: " << name_ << ", sampleFormat: " << sampleFormat_.toString() << "\n";
5353
5454 if (uri_.query.find(kUriChunkMs) != uri_.query.end())
5555 chunk_ms_ = cpt::stoul(uri_.query[kUriChunkMs]);
9494 }
9595
9696
97 std::string PcmStream::getCodec() const
98 {
99 return encoder_->name();
100 }
101
102
97103 void PcmStream::start()
98104 {
99 LOG(DEBUG, LOG_TAG) << "Start, sampleformat: " << sampleFormat_.getFormat() << "\n";
100 encoder_->init(this, sampleFormat_);
105 LOG(DEBUG, LOG_TAG) << "Start: " << name_ << ", sampleformat: " << sampleFormat_.toString() << "\n";
106 encoder_->init([this](const encoder::Encoder& encoder, std::shared_ptr<msg::PcmChunk> chunk, double duration) { chunkEncoded(encoder, chunk, duration); },
107 sampleFormat_);
101108 active_ = true;
102109 }
103110
114121 }
115122
116123
117 void PcmStream::setState(const ReaderState& newState)
124 void PcmStream::setState(ReaderState newState)
118125 {
119126 if (newState != state_)
120127 {
121 LOG(DEBUG, LOG_TAG) << "State changed: " << static_cast<int>(state_) << " => " << static_cast<int>(newState) << "\n";
128 LOG(INFO, LOG_TAG) << "State changed: " << name_ << ", state: " << static_cast<int>(state_) << " => " << static_cast<int>(newState) << "\n";
122129 state_ = newState;
123 if (pcmListener_)
124 pcmListener_->onStateChanged(this, newState);
125 }
126 }
127
128
129 void PcmStream::onChunkEncoded(const encoder::Encoder* /*encoder*/, std::shared_ptr<msg::PcmChunk> chunk, double duration)
130 {
131 // LOG(TRACE, LOG_TAG) << "onChunkEncoded: " << duration << " ms, compression ratio: " << 100 - ceil(100 * (chunk->durationMs() / duration)) << "%\n";
130 for (auto* listener : pcmListeners_)
131 {
132 if (listener)
133 listener->onStateChanged(this, newState);
134 }
135 }
136 }
137
138
139 void PcmStream::chunkEncoded(const encoder::Encoder& encoder, std::shared_ptr<msg::PcmChunk> chunk, double duration)
140 {
141 std::ignore = encoder;
142 // LOG(TRACE, LOG_TAG) << "onChunkEncoded: " << getName() << ", duration: " << duration << " ms, compression ratio: " << 100 - ceil(100 *
143 // (chunk->durationMs() / duration)) << "%\n";
132144 if (duration <= 0)
133145 return;
134146
147 // absolute start timestamp is the tvEncodedChunk_
135148 auto microsecs = std::chrono::duration_cast<std::chrono::microseconds>(tvEncodedChunk_.time_since_epoch()).count();
136149 chunk->timestamp.sec = microsecs / 1000000;
137150 chunk->timestamp.usec = microsecs % 1000000;
138151
152 // update tvEncodedChunk_ to the next chunk start by adding the current chunk duration
139153 tvEncodedChunk_ += std::chrono::nanoseconds(static_cast<std::chrono::nanoseconds::rep>(duration * 1000000));
140 if (pcmListener_)
141 pcmListener_->onChunkRead(this, chunk, duration);
154 for (auto* listener : pcmListeners_)
155 {
156 if (listener)
157 listener->onChunkEncoded(this, chunk, duration);
158 }
159 }
160
161
162 void PcmStream::chunkRead(const msg::PcmChunk& chunk)
163 {
164 for (auto* listener : pcmListeners_)
165 {
166 if (listener)
167 listener->onChunkRead(this, chunk);
168 }
169 encoder_->encode(chunk);
170 }
171
172
173 void PcmStream::resync(const std::chrono::nanoseconds& duration)
174 {
175 for (auto* listener : pcmListeners_)
176 {
177 if (listener)
178 listener->onResync(this, duration.count() / 1000000.);
179 }
142180 }
143181
144182
162200 return j;
163201 }
164202
203
204 void PcmStream::addListener(PcmListener* pcmListener)
205 {
206 pcmListeners_.push_back(pcmListener);
207 }
208
209
165210 std::shared_ptr<msg::StreamTags> PcmStream::getMeta() const
166211 {
167212 return meta_;
168213 }
214
169215
170216 void PcmStream::setMeta(const json& jtag)
171217 {
172218 meta_.reset(new msg::StreamTags(jtag));
173219 meta_->msg["STREAM"] = name_;
174 LOG(INFO, LOG_TAG) << "metadata=" << meta_->msg.dump(4) << "\n";
220 LOG(INFO, LOG_TAG) << "Stream: " << name_ << ", metadata=" << meta_->msg.dump(4) << "\n";
175221
176222 // Trigger a stream update
177 if (pcmListener_)
178 pcmListener_->onMetaChanged(this);
223 for (auto* listener : pcmListeners_)
224 {
225 if (listener)
226 listener->onMetaChanged(this);
227 }
179228 }
180229
181230 } // namespace streamreader
2929 #include <condition_variable>
3030 #include <map>
3131 #include <string>
32 #include <vector>
3233
3334
3435 namespace streamreader
5960 {
6061 public:
6162 virtual void onMetaChanged(const PcmStream* pcmStream) = 0;
62 virtual void onStateChanged(const PcmStream* pcmStream, const ReaderState& state) = 0;
63 virtual void onChunkRead(const PcmStream* pcmStream, std::shared_ptr<msg::PcmChunk> chunk, double duration) = 0;
63 virtual void onStateChanged(const PcmStream* pcmStream, ReaderState state) = 0;
64 virtual void onChunkRead(const PcmStream* pcmStream, const msg::PcmChunk& chunk) = 0;
65 virtual void onChunkEncoded(const PcmStream* pcmStream, std::shared_ptr<msg::PcmChunk> chunk, double duration) = 0;
6466 virtual void onResync(const PcmStream* pcmStream, double ms) = 0;
6567 };
6668
7173 * Implements EncoderListener to get the encoded data.
7274 * Data is passed to the PcmListener
7375 */
74 class PcmStream : public encoder::EncoderListener
76 class PcmStream
7577 {
7678 public:
7779 /// ctor. Encoded PCM data is passed to the PcmListener
8183 virtual void start();
8284 virtual void stop();
8385
84 /// Implementation of EncoderListener::onChunkEncoded
85 void onChunkEncoded(const encoder::Encoder* encoder, std::shared_ptr<msg::PcmChunk> chunk, double duration) override;
8686 virtual std::shared_ptr<msg::CodecHeader> getHeader();
8787
8888 virtual const StreamUri& getUri() const;
8989 virtual const std::string& getName() const;
9090 virtual const std::string& getId() const;
9191 virtual const SampleFormat& getSampleFormat() const;
92 virtual std::string getCodec() const;
9293
9394 std::shared_ptr<msg::StreamTags> getMeta() const;
9495 void setMeta(const json& j);
9697 virtual ReaderState getState() const;
9798 virtual json toJson() const;
9899
100 void addListener(PcmListener* pcmListener);
99101
100102 protected:
101103 std::atomic<bool> active_;
102104
103 void setState(const ReaderState& newState);
105 void setState(ReaderState newState);
106 void chunkRead(const msg::PcmChunk& chunk);
107 void resync(const std::chrono::nanoseconds& duration);
108 void chunkEncoded(const encoder::Encoder& encoder, std::shared_ptr<msg::PcmChunk> chunk, double duration);
104109
105110 std::chrono::time_point<std::chrono::steady_clock> tvEncodedChunk_;
106 PcmListener* pcmListener_;
111 std::vector<PcmListener*> pcmListeners_;
107112 StreamUri uri_;
108113 SampleFormat sampleFormat_;
109114 size_t chunk_ms_;
5454
5555 void PipeStream::do_connect()
5656 {
57 LOG(DEBUG, LOG_TAG) << "connect\n";
5857 int fd = open(uri_.path.c_str(), O_RDONLY | O_NONBLOCK);
58 int pipe_size = -1;
59 #if !defined(MACOS)
60 pipe_size = fcntl(fd, F_GETPIPE_SZ);
61 #endif
62 LOG(TRACE, LOG_TAG) << "Stream: " << name_ << ", connect to pipe: " << uri_.path << ", fd: " << fd << ", pipe size: " << pipe_size << "\n";
5963 stream_ = std::make_unique<boost::asio::posix::stream_descriptor>(ioc_, fd);
6064 on_connect();
6165 }
62 }
66
67 } // namespace streamreader
8181
8282 if (first_)
8383 {
84 LOG(DEBUG, LOG_TAG) << "First read, initializing nextTick to now\n";
84 LOG(TRACE, LOG_TAG) << "First read, initializing nextTick to now\n";
8585 nextTick_ = std::chrono::steady_clock::now();
8686 }
8787
104104 }
105105 else
106106 {
107 // LOG(DEBUG) << "count: " << count << "\n";
107 // LOG(DEBUG, LOG_TAG) << "count: " << count << "\n";
108108 len += count;
109109 bytes_read_ += len;
110110 idle_bytes_ = 0;
122122 if ((idle_bytes_ == 0) || (idle_bytes_ <= max_idle_bytes_))
123123 {
124124 // the encoder will update the tvEncodedChunk when a chunk is encoded
125 encoder_->encode(chunk_.get());
125 chunkRead(*chunk_);
126126 }
127127 else
128128 {
142142 }
143143 else if (next_read >= -kResyncTolerance)
144144 {
145 LOG(INFO) << "next read < 0 (" << getName() << "): " << std::chrono::duration_cast<std::chrono::microseconds>(next_read).count() / 1000. << " ms\n";
145 LOG(INFO, LOG_TAG) << "next read < 0 (" << getName() << "): " << std::chrono::duration_cast<std::chrono::microseconds>(next_read).count() / 1000.
146 << " ms\n";
146147 do_read();
147148 }
148149 else
149150 {
150151 // reading chunk_ms_ took longer than chunk_ms_
151 pcmListener_->onResync(this, std::chrono::duration_cast<std::chrono::milliseconds>(-next_read).count());
152 resync(-next_read);
152153 first_ = true;
153154 wait(read_timer_, duration + kResyncTolerance, [this] { do_read(); });
154155 }
4343 }
4444
4545
46 bool ProcessStream::fileExists(const std::string& filename)
46 bool ProcessStream::fileExists(const std::string& filename) const
4747 {
4848 struct stat buffer;
4949 return (stat(filename.c_str(), &buffer) == 0);
5050 }
5151
5252
53 std::string ProcessStream::findExe(const std::string& filename)
53 std::string ProcessStream::findExe(const std::string& filename) const
5454 {
5555 /// check if filename exists
5656 if (fileExists(filename))
1818 #ifndef PROCESS_STREAM_HPP
1919 #define PROCESS_STREAM_HPP
2020
21 #pragma GCC diagnostic push
22 #pragma GCC diagnostic ignored "-Wunused-result"
23 #pragma GCC diagnostic ignored "-Wunused-parameter"
24 #pragma GCC diagnostic ignored "-Wmissing-braces"
2125 #include <boost/process.hpp>
26 #pragma GCC diagnostic pop
2227 #include <memory>
2328 #include <string>
29 #include <vector>
2430
2531 #include "posix_stream.hpp"
2632 #include "watchdog.hpp"
6571 virtual void onStderrMsg(const std::string& line);
6672 virtual void initExeAndPath(const std::string& filename);
6773
68 bool fileExists(const std::string& filename);
69 std::string findExe(const std::string& filename);
74 bool fileExists(const std::string& filename) const;
75 std::string findExe(const std::string& filename) const;
7076
7177 size_t wd_timeout_sec_;
7278 std::unique_ptr<Watchdog> watchdog_;
1717
1818 #include "stream_manager.hpp"
1919 #include "airplay_stream.hpp"
20 #ifdef HAS_ALSA
21 #include "alsa_stream.hpp"
22 #endif
2023 #include "common/aixlog.hpp"
2124 #include "common/snap_exception.hpp"
2225 #include "common/str_compat.hpp"
2326 #include "common/utils.hpp"
2427 #include "file_stream.hpp"
2528 #include "librespot_stream.hpp"
29 #include "meta_stream.hpp"
2630 #include "pipe_stream.hpp"
2731 #include "process_stream.hpp"
2832 #include "tcp_stream.hpp"
4347 PcmStreamPtr StreamManager::addStream(const std::string& uri)
4448 {
4549 StreamUri streamUri(uri);
46
50 return addStream(streamUri);
51 }
52
53
54 PcmStreamPtr StreamManager::addStream(StreamUri& streamUri)
55 {
4756 if (streamUri.query.find(kUriSampleFormat) == streamUri.query.end())
4857 streamUri.query[kUriSampleFormat] = sampleFormat_;
4958
7281 {
7382 stream = make_shared<ProcessStream>(pcmListener_, ioc_, streamUri);
7483 }
84 #ifdef HAS_ALSA
85 else if (streamUri.scheme == "alsa")
86 {
87 stream = make_shared<AlsaStream>(pcmListener_, ioc_, streamUri);
88 }
89 #endif
7590 else if ((streamUri.scheme == "spotify") || (streamUri.scheme == "librespot"))
7691 {
7792 // Overwrite sample format here instead of inside the constructor, to make sure
91106 else if (streamUri.scheme == "tcp")
92107 {
93108 stream = make_shared<TcpStream>(pcmListener_, ioc_, streamUri);
109 }
110 else if (streamUri.scheme == "meta")
111 {
112 stream = make_shared<MetaStream>(pcmListener_, streams_, ioc_, streamUri);
94113 }
95114 else
96115 {
133152 if (streams_.empty())
134153 return nullptr;
135154
136 return streams_.front();
155 for (const auto stream : streams_)
156 {
157 if (stream->getCodec() != "null")
158 return stream;
159 }
160 return nullptr;
137161 }
138162
139163
150174
151175 void StreamManager::start()
152176 {
153 for (const auto& stream : streams_)
154 stream->start();
177 // Start meta streams first
178 for (const auto& stream : streams_)
179 if (stream->getUri().scheme == "meta")
180 stream->start();
181 // Start normal streams second
182 for (const auto& stream : streams_)
183 if (stream->getUri().scheme != "meta")
184 stream->start();
155185 }
156186
157187
158188 void StreamManager::stop()
159189 {
160 for (const auto& stream : streams_)
161 if (stream)
190 // Stop normal streams first
191 for (const auto& stream : streams_)
192 if (stream && (stream->getUri().scheme != "meta"))
193 stream->stop();
194 // Stop meta streams second
195 for (const auto& stream : streams_)
196 if (stream && (stream->getUri().scheme == "meta"))
162197 stream->stop();
163198 }
164199
167202 {
168203 json result = json::array();
169204 for (auto stream : streams_)
170 result.push_back(stream->toJson());
205 if (stream->getCodec() != "null")
206 result.push_back(stream->toJson());
171207 return result;
172208 }
173209
2727 namespace streamreader
2828 {
2929
30 typedef std::shared_ptr<PcmStream> PcmStreamPtr;
30 using PcmStreamPtr = std::shared_ptr<PcmStream>;
3131
3232 class StreamManager
3333 {
3636 size_t defaultChunkBufferMs = 20);
3737
3838 PcmStreamPtr addStream(const std::string& uri);
39 PcmStreamPtr addStream(StreamUri& streamUri);
3940 void removeStream(const std::string& name);
4041 void start();
4142 void stop();
3434 namespace streamreader
3535 {
3636
37 static constexpr auto LOG_TAG = "TcpStream";
38
3739 TcpStream::TcpStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const StreamUri& uri)
3840 : AsioStream<tcp::socket>(pcmListener, ioc, uri), reconnect_timer_(ioc)
3941 {
5658
5759 port_ = cpt::stoi(uri_.getQuery("port", cpt::to_string(port_)), port_);
5860
59 LOG(INFO) << "TcpStream host: " << host_ << ", port: " << port_ << ", is server: " << is_server_ << "\n";
61 LOG(INFO, LOG_TAG) << "TcpStream host: " << host_ << ", port: " << port_ << ", is server: " << is_server_ << "\n";
6062 if (is_server_)
6163 acceptor_ = make_unique<tcp::acceptor>(ioc_, tcp::endpoint(boost::asio::ip::address::from_string(host_), port_));
6264 }
7274 acceptor_->async_accept([this](boost::system::error_code ec, tcp::socket socket) {
7375 if (!ec)
7476 {
75 LOG(DEBUG) << "New client connection\n";
77 LOG(DEBUG, LOG_TAG) << "New client connection\n";
7678 stream_ = make_unique<tcp::socket>(move(socket));
7779 on_connect();
7880 }
7981 else
8082 {
81 LOG(ERROR) << "Accept failed: " << ec.message() << "\n";
83 LOG(ERROR, LOG_TAG) << "Accept failed: " << ec.message() << "\n";
8284 }
8385 });
8486 }
8991 stream_->async_connect(endpoint, [this](const boost::system::error_code& ec) {
9092 if (!ec)
9193 {
92 LOG(DEBUG) << "Connected\n";
94 LOG(DEBUG, LOG_TAG) << "Connected\n";
9395 on_connect();
9496 }
9597 else
9698 {
97 LOG(DEBUG) << "Connect failed: " << ec.message() << "\n";
99 LOG(DEBUG, LOG_TAG) << "Connect failed: " << ec.message() << "\n";
98100 wait(reconnect_timer_, 1s, [this] { connect(); });
99101 }
100102 });