Merge branch 'develop' into nonroot

This commit is contained in:
badaix 2016-10-22 20:24:40 +02:00
commit 8ae203eece
35 changed files with 1169 additions and 1010 deletions

View file

@ -8,8 +8,8 @@ android {
applicationId "de.badaix.snapcast" applicationId "de.badaix.snapcast"
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 23 targetSdkVersion 23
versionCode 7 versionCode 900
versionName "0.8.0" versionName "0.9.0"
multiDexEnabled true multiDexEnabled true
} }
buildTypes { buildTypes {

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

File diff suppressed because it is too large Load diff

View file

@ -56,7 +56,6 @@ public class ClientItem extends LinearLayout implements SeekBar.OnSeekBarChangeL
ibMute.setOnClickListener(this); ibMute.setOnClickListener(this);
ibOverflow = (ImageButton) findViewById(R.id.ibOverflow); ibOverflow = (ImageButton) findViewById(R.id.ibOverflow);
ibOverflow.setOnClickListener(this); ibOverflow.setOnClickListener(this);
volumeSeekBar.setMax(100);
setClient(client); setClient(client);
volumeSeekBar.setOnSeekBarChangeListener(this); volumeSeekBar.setOnSeekBarChangeListener(this);
this.server = server; this.server = server;

View file

@ -67,7 +67,8 @@
android:id="@+id/volumeSeekBar" android:id="@+id/volumeSeekBar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical"/> android:layout_gravity="center_vertical"
android:max="100"/>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</RelativeLayout> </RelativeLayout>

View file

@ -86,7 +86,8 @@
android:id="@+id/volumeSeekBar" android:id="@+id/volumeSeekBar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical"/> android:layout_gravity="center_vertical"
android:max="100"/>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</RelativeLayout> </RelativeLayout>

View file

@ -5,7 +5,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:2.2.0' classpath 'com.android.tools.build:gradle:2.2.2'
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files

View file

@ -1,4 +1,4 @@
VERSION = 0.8.0 VERSION = 0.9.0
TARGET = snapclient TARGET = snapclient
SHELL = /bin/bash SHELL = /bin/bash
@ -19,34 +19,34 @@ endif
ifeq ($(TARGET), ANDROID) ifeq ($(TARGET), ANDROID)
CXX = $(NDK_DIR)/bin/arm-linux-androideabi-g++ CXX = $(NDK_DIR)/bin/arm-linux-androideabi-g++
STRIP = $(NDK_DIR)/bin/arm-linux-androideabi-strip STRIP = $(NDK_DIR)/bin/arm-linux-androideabi-strip
CXXFLAGS += -pthread -DANDROID -DNO_CPP11_STRING -fPIC -DHAS_TREMOR -DHAS_OPENSL -I$(NDK_DIR)/include CXXFLAGS += -pthread -DANDROID -DNO_CPP11_STRING -fPIC -DHAS_TREMOR -DHAS_OPENSL -I$(NDK_DIR)/include
LDFLAGS = -L$(NDK_DIR)/lib -pie -lvorbisidec -logg -lFLAC -lOpenSLES LDFLAGS = -L$(NDK_DIR)/lib -pie -lvorbisidec -logg -lFLAC -lOpenSLES
OBJ += player/openslPlayer.o OBJ += player/openslPlayer.o
else ifeq ($(TARGET), OPENWRT) else ifeq ($(TARGET), OPENWRT)
STRIP = echo STRIP = echo
CXXFLAGS += -pthread -DNO_CPP11_STRING -DHAS_TREMOR -DHAS_ALSA -DHAS_AVAHI -DHAS_DAEMON CXXFLAGS += -pthread -DNO_CPP11_STRING -DHAS_TREMOR -DHAS_ALSA -DHAS_AVAHI -DHAS_DAEMON
LDFLAGS = -lasound -lvorbisidec -logg -lFLAC -lavahi-client -lavahi-common -latomic LDFLAGS = -lasound -lvorbisidec -logg -lFLAC -lavahi-client -lavahi-common -latomic
OBJ += player/alsaPlayer.o browseAvahi.o OBJ += player/alsaPlayer.o browseZeroConf/browseAvahi.o
else ifeq ($(TARGET), MACOS) else ifeq ($(TARGET), MACOS)
CXX = /usr/bin/g++ CXX = /usr/bin/g++
STRIP = strip STRIP = strip
CXXFLAGS += -DHAS_OGG -DHAS_COREAUDIO -DFREEBSD -DMACOS -DHAS_BONJOUR -I/usr/local/include -Wno-unused-local-typedef -Wno-deprecated CXXFLAGS += -DHAS_OGG -DHAS_COREAUDIO -DFREEBSD -DMACOS -DHAS_BONJOUR -I/usr/local/include -Wno-unused-local-typedef -Wno-deprecated
LDFLAGS = -logg -lvorbis -lFLAC -L/usr/local/lib -framework AudioToolbox -framework CoreFoundation LDFLAGS = -logg -lvorbis -lFLAC -L/usr/local/lib -framework AudioToolbox -framework CoreFoundation
OBJ += player/coreAudioPlayer.o browseBonjour.o OBJ += player/coreAudioPlayer.o browseZeroConf/browseBonjour.o
else else
CXX = /usr/bin/g++ CXX = /usr/bin/g++
STRIP = strip STRIP = strip
CXXFLAGS += -pthread -DHAS_OGG -DHAS_ALSA -DHAS_AVAHI -DHAS_DAEMON CXXFLAGS += -pthread -DHAS_OGG -DHAS_ALSA -DHAS_AVAHI -DHAS_DAEMON
LDFLAGS = -lrt -lasound -logg -lvorbis -lFLAC -lavahi-client -lavahi-common -static-libgcc -static-libstdc++ LDFLAGS = -lrt -lasound -logg -lvorbis -lFLAC -lavahi-client -lavahi-common -static-libgcc -static-libstdc++
OBJ += player/alsaPlayer.o browseAvahi.o OBJ += player/alsaPlayer.o browseZeroConf/browseAvahi.o
endif endif
@ -77,6 +77,15 @@ ifdef DESTDIR
install: install:
$(MAKE) installfiles $(MAKE) installfiles
else ifeq ($(TARGET), MACOS)
install:
echo macOS
install -g wheel -o root $(BIN) $(TARGET_DIR)/local/bin/$(BIN)
install -g wheel -o root $(BIN).1 $(TARGET_DIR)/local/share/man/man1/$(BIN).1
install -g wheel -o root debian/$(BIN).plist /Library/LaunchAgents/de.badaix.snapcast.$(BIN).plist
launchctl load /Library/LaunchAgents/de.badaix.snapcast.$(BIN).plist
else else
install: install:
@ -120,6 +129,17 @@ adduser:
adduser --quiet --ingroup audio --system --no-create-home --home /var/lib/snapcast snapcast; \ adduser --quiet --ingroup audio --system --no-create-home --home /var/lib/snapcast snapcast; \
fi; \ fi; \
ifeq ($(TARGET), MACOS)
uninstall:
@launchctl unload /Library/LaunchAgents/de.badaix.snapcast.$(BIN).plist; \
killall -9 $(BIN); \
rm -f $(TARGET_DIR)/local/bin/$(BIN); \
rm -f $(TARGET_DIR)/local/share/man/man1/$(BIN).1; \
rm -f /Library/LaunchAgents/de.badaix.snapcast.$(BIN).plist; \
else
uninstall: uninstall:
rm -f $(TARGET_DIR)/share/man/man1/$(BIN).1 rm -f $(TARGET_DIR)/share/man/man1/$(BIN).1
@if [[ `systemctl` =~ -\.mount ]]; then \ @if [[ `systemctl` =~ -\.mount ]]; then \
@ -134,6 +154,8 @@ uninstall:
rm -rf /var/run/$(BIN) rm -rf /var/run/$(BIN)
$(MAKE) deluser $(MAKE) deluser
endif
uninstallsysv: uninstallsysv:
@/etc/init.d/$(BIN) stop; \ @/etc/init.d/$(BIN) stop; \
killall -9 $(BIN); \ killall -9 $(BIN); \

View file

@ -1,3 +1,18 @@
snapclient (0.9.0) unstable; urgency=low
* Features
-Added (experimental) support for macOS (make TARGET=MACOS)
* Bugfixes
-Android client: Fixed crash on Nougat (Issue #97)
-Fixed FreeBSD compile error for Snapserver (Issue #107)
-Snapserver randomly dropped JSON control messages
-Long command line options (like --sampleformat)
didn't work on some systems (Issue #103)
* General
-Updated Android NDK to revision 13
-- Johannes Pohl <johannes.pohl@badaix.de> Sat, 22 Oct 2016 00:13:37 +0200
snapclient (0.8.0) unstable; urgency=low snapclient (0.8.0) unstable; urgency=low
* Features * Features

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>de.badaix.snapcast.snapclient</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/snapclient</string>
<string>-d</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
</dict>
</plist>

View file

@ -47,18 +47,19 @@ CoreAudioPlayer::~CoreAudioPlayer()
void CoreAudioPlayer::playerCallback(AudioQueueRef queue, AudioQueueBufferRef bufferRef) void CoreAudioPlayer::playerCallback(AudioQueueRef queue, AudioQueueBufferRef bufferRef)
{ {
/// Estimate the playout delay by checking the number of frames left in the buffer /// Estimate the playout delay by checking the number of frames left in the buffer
/// and add ms_ (= complete buffer size). Based on trying. /// and add ms_ (= complete buffer size). Based on trying.
AudioTimeStamp timestamp; AudioTimeStamp timestamp;
AudioQueueGetCurrentTime(queue, timeLine, &timestamp, NULL); AudioQueueGetCurrentTime(queue, timeLine_, &timestamp, NULL);
size_t bufferedFrames = (frames_ - ((uint64_t)timestamp.mSampleTime % frames_)) % frames_; size_t bufferedFrames = (frames_ - ((uint64_t)timestamp.mSampleTime % frames_)) % frames_;
size_t bufferedMs = bufferedFrames * 1000 / pubStream_->getFormat().rate + (ms_ * (NUM_BUFFERS - 1)); size_t bufferedMs = bufferedFrames * 1000 / pubStream_->getFormat().rate + (ms_ * (NUM_BUFFERS - 1));
/// 15ms DAC delay. Based on trying. /// 15ms DAC delay. Based on trying.
bufferedMs += 15; bufferedMs += 15;
// logO << "buffered: " << bufferedFrames << ", ms: " << bufferedMs << ", mSampleTime: " << timestamp.mSampleTime << "\n"; // logO << "buffered: " << bufferedFrames << ", ms: " << bufferedMs << ", mSampleTime: " << timestamp.mSampleTime << "\n";
/// TODO: sometimes this bufferedMS or AudioTimeStamp wraps around 1s (i.e. we're 1s out of sync (behind)) and recovers later on
chronos::usec delay(bufferedMs * 1000); chronos::usec delay(bufferedMs * 1000);
char *buffer = (char*)bufferRef->mAudioData; char *buffer = (char*)bufferRef->mAudioData;
if (!pubStream_->getPlayerChunk(buffer, delay, frames_)) if (!pubStream_->getPlayerChunk(buffer, delay, frames_))
{ {
logO << "Failed to get chunk. Playing silence.\n"; logO << "Failed to get chunk. Playing silence.\n";
@ -70,60 +71,60 @@ void CoreAudioPlayer::playerCallback(AudioQueueRef queue, AudioQueueBufferRef bu
} }
// OSStatus status = // OSStatus status =
AudioQueueEnqueueBuffer(queue, bufferRef, 0, NULL); AudioQueueEnqueueBuffer(queue, bufferRef, 0, NULL);
if (!active_) if (!active_)
{ {
AudioQueueStop(queue, false); AudioQueueStop(queue, false);
AudioQueueDispose(queue, false); AudioQueueDispose(queue, false);
CFRunLoopStop(CFRunLoopGetCurrent()); CFRunLoopStop(CFRunLoopGetCurrent());
} }
} }
void CoreAudioPlayer::worker() void CoreAudioPlayer::worker()
{ {
const SampleFormat& sampleFormat = pubStream_->getFormat(); const SampleFormat& sampleFormat = pubStream_->getFormat();
AudioStreamBasicDescription format; AudioStreamBasicDescription format;
format.mSampleRate = sampleFormat.rate; format.mSampleRate = sampleFormat.rate;
format.mFormatID = kAudioFormatLinearPCM; format.mFormatID = kAudioFormatLinearPCM;
format.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger;// | kAudioFormatFlagIsPacked; format.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger;// | kAudioFormatFlagIsPacked;
format.mBitsPerChannel = sampleFormat.bits; format.mBitsPerChannel = sampleFormat.bits;
format.mChannelsPerFrame = sampleFormat.channels; format.mChannelsPerFrame = sampleFormat.channels;
format.mBytesPerFrame = sampleFormat.frameSize; format.mBytesPerFrame = sampleFormat.frameSize;
format.mFramesPerPacket = 1; format.mFramesPerPacket = 1;
format.mBytesPerPacket = format.mBytesPerFrame * format.mFramesPerPacket; format.mBytesPerPacket = format.mBytesPerFrame * format.mFramesPerPacket;
format.mReserved = 0; format.mReserved = 0;
AudioQueueRef queue; AudioQueueRef queue;
AudioQueueNewOutput(&format, callback, this, CFRunLoopGetCurrent(), kCFRunLoopCommonModes, 0, &queue); AudioQueueNewOutput(&format, callback, this, CFRunLoopGetCurrent(), kCFRunLoopCommonModes, 0, &queue);
AudioQueueCreateTimeline(queue, &timeLine); AudioQueueCreateTimeline(queue, &timeLine_);
// Apple recommends this as buffer size: // Apple recommends this as buffer size:
// https://developer.apple.com/library/content/documentation/MusicAudio/Conceptual/CoreAudioOverview/CoreAudioEssentials/CoreAudioEssentials.html // https://developer.apple.com/library/content/documentation/MusicAudio/Conceptual/CoreAudioOverview/CoreAudioEssentials/CoreAudioEssentials.html
// static const int maxBufferSize = 0x10000; // limit maximum size to 64K // static const int maxBufferSize = 0x10000; // limit maximum size to 64K
// static const int minBufferSize = 0x4000; // limit minimum size to 16K // static const int minBufferSize = 0x4000; // limit minimum size to 16K
// //
// For 100ms @ 48000:16:2 we have 19.2K // For 100ms @ 48000:16:2 we have 19.2K
// frames: 4800, ms: 100, buffer size: 19200 // frames: 4800, ms: 100, buffer size: 19200
frames_ = (sampleFormat.rate * ms_) / 1000; frames_ = (sampleFormat.rate * ms_) / 1000;
ms_ = frames_ * 1000 / sampleFormat.rate; ms_ = frames_ * 1000 / sampleFormat.rate;
buff_size_ = frames_ * sampleFormat.frameSize; buff_size_ = frames_ * sampleFormat.frameSize;
logO << "frames: " << frames_ << ", ms: " << ms_ << ", buffer size: " << buff_size_ << "\n"; logO << "frames: " << frames_ << ", ms: " << ms_ << ", buffer size: " << buff_size_ << "\n";
AudioQueueBufferRef buffers[NUM_BUFFERS]; AudioQueueBufferRef buffers[NUM_BUFFERS];
for (int i = 0; i < NUM_BUFFERS; i++) for (int i = 0; i < NUM_BUFFERS; i++)
{ {
AudioQueueAllocateBuffer(queue, buff_size_, &buffers[i]); AudioQueueAllocateBuffer(queue, buff_size_, &buffers[i]);
buffers[i]->mAudioDataByteSize = buff_size_; buffers[i]->mAudioDataByteSize = buff_size_;
callback(this, queue, buffers[i]); callback(this, queue, buffers[i]);
} }
logE << "CoreAudioPlayer::worker\n"; logE << "CoreAudioPlayer::worker\n";
AudioQueueCreateTimeline(queue, &timeLine); AudioQueueCreateTimeline(queue, &timeLine_);
AudioQueueStart(queue, NULL); AudioQueueStart(queue, NULL);
CFRunLoopRun(); CFRunLoopRun();
} }

View file

@ -45,7 +45,7 @@ public:
protected: protected:
virtual void worker(); virtual void worker();
AudioQueueTimelineRef timeLine; AudioQueueTimelineRef timeLine_;
size_t ms_; size_t ms_;
size_t frames_; size_t frames_;
size_t buff_size_; size_t buff_size_;

View file

@ -21,7 +21,7 @@
#include "popl.hpp" #include "popl.hpp"
#include "controller.h" #include "controller.h"
#include "browsemDNS.h" #include "browseZeroConf/browsemDNS.h"
#ifdef HAS_ALSA #ifdef HAS_ALSA
#include "player/alsaPlayer.h" #include "player/alsaPlayer.h"
@ -68,7 +68,7 @@ PcmDevice getPcmDevice(const std::string& soundcard)
int main (int argc, char **argv) int main (int argc, char **argv)
{ {
#ifdef MACOS #ifdef MACOS
#pragma message "Warning: the Mac OS support is experimental and might not be maintained" #pragma message "Warning: the macOS support is experimental and might not be maintained"
#endif #endif
try try
{ {

View file

@ -21,6 +21,10 @@
#include <chrono> #include <chrono>
#include <sys/time.h> #include <sys/time.h>
#ifdef MACOS
#include <mach/clock.h>
#include <mach/mach.h>
#endif
namespace chronos namespace chronos
{ {
@ -49,9 +53,12 @@ namespace chronos
inline static long getTickCount() inline static long getTickCount()
{ {
#ifdef MACOS #ifdef MACOS
struct timeval now; clock_serv_t cclock;
gettimeofday(&now, NULL); mach_timespec_t mts;
return now.tv_sec*1000 + now.tv_usec / 1000; host_get_clock_service(mach_host_self(), CALENDAR_CLOCK, &cclock);
clock_get_time(cclock, &mts);
mach_port_deallocate(mach_task_self(), cclock);
return mts.tv_sec*1000 + mts.tv_nsec / 1000000;
#else #else
struct timespec now; struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now); clock_gettime(CLOCK_MONOTONIC, &now);

View file

@ -104,30 +104,50 @@ Start and stop the server with `sudo service snapserver start` and `sudo service
*Warning: macOS support is experimental* *Warning: macOS support is experimental*
Install Xcode from the App Store 1. Install Xcode from the App Store
2. Install [Homebrew](http://brew.sh)
3. Install the required libs
```
$ brew install flac libvorbis
```
###Build Snapclient ###Build Snapclient
Install the required libs:
$ brew install flac libvorbis
`cd` into the Snapclient src-root directory: `cd` into the Snapclient src-root directory:
$ cd <snapcast dir>/client $ cd <snapcast dir>/client
$ make TARGET=MACOS $ make TARGET=MACOS
Install Snapclient
$ sudo make install
This will copy the client binary to `/usr/local/bin` and create a Launch Agent to start the client as a daemon.
###Build Snapserver
`cd` into the Snapserver src-root directory:
$ cd <snapcast dir>/server
$ make TARGET=MACOS
Install Snapserver
$ sudo make install
This will copy the server binary to `/usr/local/bin` and create a Launch Agent to start the server as a daemon.
##Android (Cross compile) ##Android (Cross compile)
Cross compilation for Android is done with the [Android NDK](http://developer.android.com/tools/sdk/ndk/index.html) on a Linux host machine. Cross compilation for Android is done with the [Android NDK](http://developer.android.com/tools/sdk/ndk/index.html) on a Linux host machine.
###Android NDK setup ###Android NDK setup
http://developer.android.com/ndk/guides/standalone_toolchain.html http://developer.android.com/ndk/guides/standalone_toolchain.html
1. Download NDK: `http://dl.google.com/android/repository/android-ndk-r12b-linux-x86_64.zip` 1. Download NDK: `https://dl.google.com/android/repository/android-ndk-r13-linux-x86_64.zip`
2. Extract to: `/SOME/LOCAL/PATH/android-ndk-r12b` 2. Extract to: `/SOME/LOCAL/PATH/android-ndk-r13`
3. Setup toolchain somewhere in your home dir (`<android-ndk dir>`): 3. Setup toolchain somewhere in your home dir (`<android-ndk dir>`):
```` ````
$ cd /SOME/LOCAL/PATH/android-ndk-r10e/build/tools $ cd /SOME/LOCAL/PATH/android-ndk-r13/build/tools
$ ./make-standalone-toolchain.sh --arch=arm --platform=android-14 --install-dir=<android-ndk dir> --ndk-dir=/SOME/LOCAL/PATH/android-ndk-r12b $ ./make_standalone_toolchain.py --arch arm --api 14 --install-dir <android-ndk dir>
```` ````
###Build Snapclient ###Build Snapclient
@ -176,7 +196,7 @@ Within the OpenWrt directory create symbolic links to the Snapcast source direct
Build Snapcast: Build Snapcast:
$ cd <buildroot dir> $ cd <buildroot dir>
$ make package/sxx/snapcast/clean V=s $ make package/sxx/snapcast/clean
$ make package/sxx/snapcast/compile -j1 V=s $ make package/sxx/snapcast/compile
The packaged `ipk` files are in `<buildroot dir>/bin/ar71xx/packages/base/snap[client|server]_0.6.0_ar71xx.ipk` The packaged `ipk` files are in `<buildroot dir>/bin/ar71xx/packages/base/snap[client|server]_x.x.x_ar71xx.ipk`

View file

@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
include $(INCLUDE_DIR)/target.mk include $(INCLUDE_DIR)/target.mk
PKG_NAME := snapcast PKG_NAME := snapcast
PKG_VERSION := 0.8.0 PKG_VERSION := 0.9.0
PKG_RELEASE := $(PKG_SOURCE_VERSION) PKG_RELEASE := $(PKG_SOURCE_VERSION)
PKG_USE_MIPS16 := 0 PKG_USE_MIPS16 := 0

View file

@ -1,4 +1,4 @@
VERSION = 0.8.0 VERSION = 0.9.0
TARGET = snapserver TARGET = snapserver
SHELL = /bin/bash SHELL = /bin/bash
@ -9,7 +9,8 @@ else
TARGET_DIR ?= /usr TARGET_DIR ?= /usr
endif endif
CXXFLAGS += -std=c++0x -Wall -Wno-unused-function -O3 -pthread -DASIO_STANDALONE -DVERSION=\"$(VERSION)\" -I. -I.. -I../externals/asio/asio/include -I../externals/popl/include CXXFLAGS += -std=c++0x -Wall -Wno-unused-function -O3 -DASIO_STANDALONE -DVERSION=\"$(VERSION)\" -I. -I.. -I../externals/asio/asio/include -I../externals/popl/include
OBJ = snapServer.o config.o controlServer.o controlSession.o streamServer.o streamSession.o json/jsonrpc.o streamreader/streamUri.o streamreader/streamManager.o streamreader/pcmStream.o streamreader/pipeStream.o streamreader/fileStream.o encoder/encoderFactory.o encoder/flacEncoder.o encoder/pcmEncoder.o encoder/oggEncoder.o ../common/log.o ../common/sampleFormat.o ../message/pcmChunk.o
ifeq ($(ENDIAN), BIG) ifeq ($(ENDIAN), BIG)
CXXFLAGS += -DIS_BIG_ENDIAN CXXFLAGS += -DIS_BIG_ENDIAN
@ -17,28 +18,38 @@ endif
ifeq ($(TARGET), OPENWRT) ifeq ($(TARGET), OPENWRT)
STRIP = echo STRIP = echo
CXXFLAGS += -DNO_CPP11_STRING CXXFLAGS += -DNO_CPP11_STRING -DHAS_AVAHI -pthread
LDFLAGS = -lvorbis -lvorbisenc -logg -lFLAC -lavahi-client -lavahi-common -latomic LDFLAGS = -lvorbis -lvorbisenc -logg -lFLAC -lavahi-client -lavahi-common -latomic
OBJ += publishZeroConf/publishAvahi.o
else ifeq ($(TARGET), FREEBSD) else ifeq ($(TARGET), FREEBSD)
SHELL = /usr/local/bin/bash CXX = /usr/local/bin/g++
CXX = /usr/local/bin/g++ STRIP = echo
CXXFLAGS += -DNO_CPP11_STRING -DFREEBSD CXXFLAGS += -DNO_CPP11_STRING -DFREEBSD -DHAS_AVAHI -pthread
STRIP = strip LDFLAGS = -lrt -lvorbis -lvorbisenc -logg -lFLAC -lavahi-client -lavahi-common -static-libgcc -static-libstdc++
LDFLAGS = -lrt -lvorbis -lvorbisenc -logg -lFLAC -lavahi-client -lavahi-common -static-libgcc -static-libstdc++ OBJ += publishZeroConf/publishAvahi.o
else ifeq ($(TARGET), MACOS)
CXX = /usr/bin/g++
STRIP = strip
CXXFLAGS += -DFREEBSD -DMACOS -DHAS_BONJOUR -Wno-deprecated -I/usr/local/include
LDFLAGS = -lvorbis -lvorbisenc -logg -lFLAC -L/usr/local/lib
OBJ += publishZeroConf/publishBonjour.o
else else
CXX = /usr/bin/g++ CXX = /usr/bin/g++
STRIP = strip STRIP = strip
LDFLAGS = -lrt -lvorbis -lvorbisenc -logg -lFLAC -lavahi-client -lavahi-common -static-libgcc -static-libstdc++ CXXFLAGS += -DHAS_AVAHI -pthread
LDFLAGS = -lrt -lvorbis -lvorbisenc -logg -lFLAC -lavahi-client -lavahi-common -static-libgcc -static-libstdc++
OBJ += publishZeroConf/publishAvahi.o
endif endif
OBJ = snapServer.o config.o controlServer.o controlSession.o streamServer.o streamSession.o json/jsonrpc.o streamreader/streamUri.o streamreader/streamManager.o streamreader/pcmStream.o streamreader/pipeStream.o streamreader/fileStream.o encoder/encoderFactory.o encoder/flacEncoder.o encoder/pcmEncoder.o encoder/oggEncoder.o publishAvahi.o ../common/log.o ../common/sampleFormat.o ../message/pcmChunk.o
BIN = snapserver BIN = snapserver
all: $(TARGET) all: $(TARGET)
@ -73,6 +84,15 @@ install:
install -g wheel -o root -m 555 $(BIN).1 $(TARGET_DIR)/local/man/man1/$(BIN).1 install -g wheel -o root -m 555 $(BIN).1 $(TARGET_DIR)/local/man/man1/$(BIN).1
install -g wheel -o root -m 555 debian/$(BIN).bsd $(TARGET_DIR)/local/etc/rc.d/$(BIN) install -g wheel -o root -m 555 debian/$(BIN).bsd $(TARGET_DIR)/local/etc/rc.d/$(BIN)
else ifeq ($(TARGET), MACOS)
install:
echo macOS
install -g wheel -o root $(BIN) $(TARGET_DIR)/local/bin/$(BIN)
install -g wheel -o root $(BIN).1 $(TARGET_DIR)/local/share/man/man1/$(BIN).1
install -g wheel -o root debian/$(BIN).plist /Library/LaunchAgents/de.badaix.snapcast.$(BIN).plist
launchctl load /Library/LaunchAgents/de.badaix.snapcast.$(BIN).plist
else else
install: install:
@ -119,6 +139,15 @@ uninstall:
rm -f $(TARGET_DIR)/local/man/man1/$(BIN).1; \ rm -f $(TARGET_DIR)/local/man/man1/$(BIN).1; \
rm -f $(TARGET_DIR)/local/etc/rc.d/$(BIN); \ rm -f $(TARGET_DIR)/local/etc/rc.d/$(BIN); \
else ifeq ($(TARGET), MACOS)
uninstall:
@launchctl unload /Library/LaunchAgents/de.badaix.snapcast.$(BIN).plist; \
killall -9 $(BIN); \
rm -f $(TARGET_DIR)/local/bin/$(BIN); \
rm -f $(TARGET_DIR)/local/share/man/man1/$(BIN).1; \
rm -f /Library/LaunchAgents/de.badaix.snapcast.$(BIN).plist; \
else else
uninstall: uninstall:

View file

@ -30,7 +30,7 @@
using json = nlohmann::json; using json = nlohmann::json;
template<typename T> template<typename T>
T jGet(json j, const std::string& what, const T& def) T jGet(const json& j, const std::string& what, const T& def)
{ {
try try
{ {

View file

@ -1,3 +1,18 @@
snapserver (0.9.0) unstable; urgency=low
* Features
-Added (experimental) support for macOS (make TARGET=MACOS)
* Bugfixes
-Android client: Fixed crash on Nougat (Issue #97)
-Fixed FreeBSD compile error for Snapserver (Issue #107)
-Snapserver randomly dropped JSON control messages
-Long command line options (like --sampleformat)
didn't work on some systems (Issue #103)
* General
-Updated Android NDK to revision 13
-- Johannes Pohl <johannes.pohl@badaix.de> Sat, 22 Oct 2016 00:13:37 +0200
snapserver (0.8.0) unstable; urgency=low snapserver (0.8.0) unstable; urgency=low
* Features * Features

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>de.badaix.snapcast.snapserver</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/snapserver</string>
<string>-d</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
</dict>
</plist>

View file

@ -28,7 +28,7 @@
using namespace std; using namespace std;
OggEncoder::OggEncoder(const std::string& codecOptions) : Encoder(codecOptions), lastGranulepos(0) OggEncoder::OggEncoder(const std::string& codecOptions) : Encoder(codecOptions), lastGranulepos_(0)
{ {
} }
@ -56,7 +56,7 @@ void OggEncoder::encode(const msg::PcmChunk* chunk)
double res = 0; double res = 0;
logD << "payload: " << chunk->payloadSize << "\tframes: " << chunk->getFrameCount() << "\tduration: " << chunk->duration<chronos::msec>().count() << "\n"; logD << "payload: " << chunk->payloadSize << "\tframes: " << chunk->getFrameCount() << "\tduration: " << chunk->duration<chronos::msec>().count() << "\n";
int frames = chunk->getFrameCount(); int frames = chunk->getFrameCount();
float **buffer=vorbis_analysis_buffer(&vd, frames); float **buffer=vorbis_analysis_buffer(&vd_, frames);
/* uninterleave samples */ /* uninterleave samples */
for (size_t channel = 0; channel < sampleFormat_.channels; ++channel) for (size_t channel = 0; channel < sampleFormat_.channels; ++channel)
@ -82,7 +82,7 @@ void OggEncoder::encode(const msg::PcmChunk* chunk)
} }
/* tell the library how much we actually submitted */ /* tell the library how much we actually submitted */
vorbis_analysis_wrote(&vd, frames); vorbis_analysis_wrote(&vd_, frames);
msg::PcmChunk* oggChunk = new msg::PcmChunk(chunk->format, 0); msg::PcmChunk* oggChunk = new msg::PcmChunk(chunk->format, 0);
@ -90,36 +90,36 @@ void OggEncoder::encode(const msg::PcmChunk* chunk)
more involved (potentially parallel) processing. Get a single more involved (potentially parallel) processing. Get a single
block for encoding now */ block for encoding now */
size_t pos = 0; size_t pos = 0;
while (vorbis_analysis_blockout(&vd, &vb)==1) while (vorbis_analysis_blockout(&vd_, &vb_)==1)
{ {
/* analysis, assume we want to use bitrate management */ /* analysis, assume we want to use bitrate management */
vorbis_analysis(&vb, NULL); vorbis_analysis(&vb_, NULL);
vorbis_bitrate_addblock(&vb); vorbis_bitrate_addblock(&vb_);
while (vorbis_bitrate_flushpacket(&vd, &op)) while (vorbis_bitrate_flushpacket(&vd_, &op_))
{ {
/* weld the packet into the bitstream */ /* weld the packet into the bitstream */
ogg_stream_packetin(&os, &op); ogg_stream_packetin(&os_, &op_);
/* write out pages (if any) */ /* write out pages (if any) */
while (true) while (true)
{ {
int result = ogg_stream_flush(&os, &og); int result = ogg_stream_flush(&os_, &og_);
if (result == 0) if (result == 0)
break; break;
res = os.granulepos - lastGranulepos; res = os_.granulepos - lastGranulepos_;
size_t nextLen = pos + og.header_len + og.body_len; size_t nextLen = pos + og_.header_len + og_.body_len;
// make chunk larger // make chunk larger
if (oggChunk->payloadSize < nextLen) if (oggChunk->payloadSize < nextLen)
oggChunk->payload = (char*)realloc(oggChunk->payload, nextLen); oggChunk->payload = (char*)realloc(oggChunk->payload, nextLen);
memcpy(oggChunk->payload + pos, og.header, og.header_len); memcpy(oggChunk->payload + pos, og_.header, og_.header_len);
pos += og.header_len; pos += og_.header_len;
memcpy(oggChunk->payload + pos, og.body, og.body_len); memcpy(oggChunk->payload + pos, og_.body, og_.body_len);
pos += og.body_len; pos += og_.body_len;
if (ogg_page_eos(&og)) if (ogg_page_eos(&og_))
break; break;
} }
} }
@ -129,7 +129,7 @@ void OggEncoder::encode(const msg::PcmChunk* chunk)
{ {
res /= (sampleFormat_.rate / 1000.); res /= (sampleFormat_.rate / 1000.);
// logO << "res: " << res << "\n"; // logO << "res: " << res << "\n";
lastGranulepos = os.granulepos; lastGranulepos_ = os_.granulepos;
// make oggChunk smaller // make oggChunk smaller
oggChunk->payload = (char*)realloc(oggChunk->payload, pos); oggChunk->payload = (char*)realloc(oggChunk->payload, pos);
oggChunk->payloadSize = pos; oggChunk->payloadSize = pos;
@ -164,7 +164,7 @@ void OggEncoder::initEncoder()
} }
/********** Encode setup ************/ /********** Encode setup ************/
vorbis_info_init(&vi); vorbis_info_init(&vi_);
/* choose an encoding mode. A few possibilities commented out, one /* choose an encoding mode. A few possibilities commented out, one
actually used: */ actually used: */
@ -195,7 +195,7 @@ void OggEncoder::initEncoder()
*********************************************************************/ *********************************************************************/
int ret = vorbis_encode_init_vbr(&vi, sampleFormat_.channels, sampleFormat_.rate, quality); int ret = vorbis_encode_init_vbr(&vi_, sampleFormat_.channels, sampleFormat_.rate, quality);
/* do not continue if setup failed; this can happen if we ask for a /* do not continue if setup failed; this can happen if we ask for a
mode that libVorbis does not support (eg, too low a bitrate, etc, mode that libVorbis does not support (eg, too low a bitrate, etc,
@ -205,20 +205,20 @@ void OggEncoder::initEncoder()
throw SnapException("failed to init encoder"); throw SnapException("failed to init encoder");
/* add a comment */ /* add a comment */
vorbis_comment_init(&vc); vorbis_comment_init(&vc_);
vorbis_comment_add_tag(&vc, "TITLE", "SnapStream"); vorbis_comment_add_tag(&vc_, "TITLE", "SnapStream");
vorbis_comment_add_tag(&vc, "VERSION", VERSION); vorbis_comment_add_tag(&vc_, "VERSION", VERSION);
vorbis_comment_add_tag(&vc, "SAMPLE_FORMAT", sampleFormat_.getFormat().c_str()); vorbis_comment_add_tag(&vc_, "SAMPLE_FORMAT", sampleFormat_.getFormat().c_str());
/* set up the analysis state and auxiliary encoding storage */ /* set up the analysis state and auxiliary encoding storage */
vorbis_analysis_init(&vd, &vi); vorbis_analysis_init(&vd_, &vi_);
vorbis_block_init(&vd, &vb); vorbis_block_init(&vd_, &vb_);
/* set up our packet->stream encoder */ /* set up our packet->stream encoder */
/* pick a random serial number; that way we can more likely build /* pick a random serial number; that way we can more likely build
chained streams just by concatenation */ chained streams just by concatenation */
srand(time(NULL)); srand(time(NULL));
ogg_stream_init(&os, rand()); ogg_stream_init(&os_, rand());
/* Vorbis streams begin with three headers; the initial header (with /* Vorbis streams begin with three headers; the initial header (with
most of the codec setup parameters) which is mandated by the Ogg most of the codec setup parameters) which is mandated by the Ogg
@ -231,10 +231,10 @@ void OggEncoder::initEncoder()
ogg_packet header_comm; ogg_packet header_comm;
ogg_packet header_code; ogg_packet header_code;
vorbis_analysis_headerout(&vd, &vc, &header, &header_comm, &header_code); vorbis_analysis_headerout(&vd_, &vc_, &header, &header_comm, &header_code);
ogg_stream_packetin(&os, &header); ogg_stream_packetin(&os_, &header);
ogg_stream_packetin(&os, &header_comm); ogg_stream_packetin(&os_, &header_comm);
ogg_stream_packetin(&os, &header_code); ogg_stream_packetin(&os_, &header_code);
/* This ensures the actual /* This ensures the actual
* audio data will start on a new page, as per spec * audio data will start on a new page, as per spec
@ -243,16 +243,16 @@ void OggEncoder::initEncoder()
headerChunk_.reset(new msg::CodecHeader("ogg")); headerChunk_.reset(new msg::CodecHeader("ogg"));
while (true) while (true)
{ {
int result = ogg_stream_flush(&os, &og); int result = ogg_stream_flush(&os_, &og_);
if (result == 0) if (result == 0)
break; break;
headerChunk_->payloadSize += og.header_len + og.body_len; headerChunk_->payloadSize += og_.header_len + og_.body_len;
headerChunk_->payload = (char*)realloc(headerChunk_->payload, headerChunk_->payloadSize); headerChunk_->payload = (char*)realloc(headerChunk_->payload, headerChunk_->payloadSize);
logD << "HeadLen: " << og.header_len << ", bodyLen: " << og.body_len << ", result: " << result << "\n"; logD << "HeadLen: " << og_.header_len << ", bodyLen: " << og_.body_len << ", result: " << result << "\n";
memcpy(headerChunk_->payload + pos, og.header, og.header_len); memcpy(headerChunk_->payload + pos, og_.header, og_.header_len);
pos += og.header_len; pos += og_.header_len;
memcpy(headerChunk_->payload + pos, og.body, og.body_len); memcpy(headerChunk_->payload + pos, og_.body, og_.body_len);
pos += og.body_len; pos += og_.body_len;
} }
} }

View file

@ -35,19 +35,17 @@ protected:
virtual void initEncoder(); virtual void initEncoder();
private: private:
ogg_stream_state os; /* take physical pages, weld into a logical ogg_stream_state os_; /// take physical pages, weld into a logical stream of packets
stream of packets */ ogg_page og_; /// one Ogg bitstream page. Vorbis packets are inside
ogg_page og; /* one Ogg bitstream page. Vorbis packets are inside */ ogg_packet op_; /// one raw packet of data for decode
ogg_packet op; /* one raw packet of data for decode */
vorbis_info vi; /* struct that stores all the static vorbis bitstream vorbis_info vi_; /// struct that stores all the static vorbis bitstream settings
settings */ vorbis_comment vc_; /// struct that stores all the user comments
vorbis_comment vc; /* struct that stores all the user comments */
vorbis_dsp_state vd; /* central working state for the packet->PCM decoder */ vorbis_dsp_state vd_; /// central working state for the packet->PCM decoder
vorbis_block vb; /* local working space for packet->PCM decode */ vorbis_block vb_; /// local working space for packet->PCM decode
ogg_int64_t lastGranulepos; ogg_int64_t lastGranulepos_;
}; };

View file

@ -27,8 +27,8 @@ static AvahiEntryGroup *group;
static AvahiSimplePoll *simple_poll; static AvahiSimplePoll *simple_poll;
static char* name; static char* name;
PublishAvahi::PublishAvahi(const std::string& serviceName) : PublishAvahi::PublishAvahi(const std::string& serviceName) : PublishmDNS(serviceName),
client(NULL), serviceName_(serviceName), active_(false) client_(NULL), active_(false)
{ {
group = NULL; group = NULL;
simple_poll = NULL; simple_poll = NULL;
@ -36,24 +36,23 @@ PublishAvahi::PublishAvahi(const std::string& serviceName) :
} }
void PublishAvahi::publish(const std::vector<AvahiService>& services) void PublishAvahi::publish(const std::vector<mDNSService>& services)
{ {
this->services = services; services_ = services;
AvahiClient *client = NULL; /// Allocate main loop object
int error;
/* Allocate main loop object */
if (!(simple_poll = avahi_simple_poll_new())) if (!(simple_poll = avahi_simple_poll_new()))
{ {
///TODO: error handling
logE << "Failed to create simple poll object.\n"; logE << "Failed to create simple poll object.\n";
} }
/* Allocate a new client */ /// Allocate a new client
client = avahi_client_new(avahi_simple_poll_get(simple_poll), AVAHI_CLIENT_IGNORE_USER_CONFIG, client_callback, this, &error); int error;
client_ = avahi_client_new(avahi_simple_poll_get(simple_poll), AVAHI_CLIENT_IGNORE_USER_CONFIG, client_callback, this, &error);
/* Check wether creating the client object succeeded */ /// Check wether creating the client object succeeded
if (!client) if (!client_)
{ {
logE << "Failed to create client: " << avahi_strerror(error) << "\n"; logE << "Failed to create client: " << avahi_strerror(error) << "\n";
} }
@ -74,13 +73,13 @@ PublishAvahi::~PublishAvahi()
active_ = false; active_ = false;
pollThread_.join(); pollThread_.join();
if (client) if (client_)
avahi_client_free(client); avahi_client_free(client_);
if (simple_poll) if (simple_poll)
avahi_simple_poll_free(simple_poll); avahi_simple_poll_free(simple_poll);
avahi_free(name); avahi_free(name);
} }
@ -89,27 +88,26 @@ void PublishAvahi::entry_group_callback(AvahiEntryGroup *g, AvahiEntryGroupState
assert(g == group || group == NULL); assert(g == group || group == NULL);
group = g; group = g;
/* Called whenever the entry group state changes */ /// Called whenever the entry group state changes
switch (state) switch (state)
{ {
case AVAHI_ENTRY_GROUP_ESTABLISHED : case AVAHI_ENTRY_GROUP_ESTABLISHED :
/* The entry group has been established successfully */ /// The entry group has been established successfully
logO << "Service '" << name << "' successfully established.\n"; logO << "Service '" << name << "' successfully established.\n";
break; break;
case AVAHI_ENTRY_GROUP_COLLISION : { case AVAHI_ENTRY_GROUP_COLLISION :
{
char *n; char *n;
/* A service name collision with a remote service /// A service name collision with a remote service happened. Let's pick a new name
* happened. Let's pick a new name */
n = avahi_alternative_service_name(name); n = avahi_alternative_service_name(name);
avahi_free(name); avahi_free(name);
name = n; name = n;
logO << "Service name collision, renaming service to '" << name << "'\n"; logO << "Service name collision, renaming service to '" << name << "'\n";
/* And recreate the services */ /// And recreate the services
static_cast<PublishAvahi*>(userdata)->create_services(avahi_entry_group_get_client(g)); static_cast<PublishAvahi*>(userdata)->create_services(avahi_entry_group_get_client(g));
break; break;
} }
@ -118,7 +116,7 @@ void PublishAvahi::entry_group_callback(AvahiEntryGroup *g, AvahiEntryGroupState
logE << "Entry group failure: " << avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(g))) << "\n"; logE << "Entry group failure: " << avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(g))) << "\n";
/* Some kind of failure happened while we were registering our services */ /// Some kind of failure happened while we were registering our services
avahi_simple_poll_quit(simple_poll); avahi_simple_poll_quit(simple_poll);
break; break;
@ -130,13 +128,10 @@ void PublishAvahi::entry_group_callback(AvahiEntryGroup *g, AvahiEntryGroupState
void PublishAvahi::create_services(AvahiClient *c) void PublishAvahi::create_services(AvahiClient *c)
{ {
char *n, r[128];
int ret;
assert(c); assert(c);
char *n;
/* If this is the first time we're called, let's create a new
* entry group if necessary */ /// If this is the first time we're called, let's create a new entry group if necessary
if (!group) if (!group)
{ {
if (!(group = avahi_entry_group_new(c, entry_group_callback, this))) if (!(group = avahi_entry_group_new(c, entry_group_callback, this)))
@ -145,54 +140,34 @@ void PublishAvahi::create_services(AvahiClient *c)
goto fail; goto fail;
} }
} }
/* If the group is empty (either because it was just created, or
* because it was reset previously, add our entries. */
/// If the group is empty (either because it was just created, or because it was reset previously, add our entries.
int ret;
if (avahi_entry_group_is_empty(group)) if (avahi_entry_group_is_empty(group))
{ {
logO << "Adding service '" << name << "'\n"; logO << "Adding service '" << name << "'\n";
/* Create some random TXT data */ /// We will now add two services and one subtype to the entry group
snprintf(r, sizeof(r), "random=%i", rand()); for (const auto& service: services_)
/* We will now add two services and one subtype to the entry
* group. The two services have the same name, but differ in
* the service type (IPP vs. BSD LPR). Only services with the
* same name should be put in the same entry group. */
/* Add the service for IPP */
/* if ((ret = avahi_entry_group_add_service(group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, AVAHI_PUBLISH_UNIQUE, name, "_ipp._tcp", NULL, NULL, 651, "test=blah", r, NULL)) < 0)
{ {
if ((ret = avahi_entry_group_add_service(group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, AvahiPublishFlags(0), name, service.name_.c_str(), NULL, NULL, service.port_, NULL)) < 0)
if (ret == AVAHI_ERR_COLLISION)
goto collision;
fprintf(stderr, "Failed to add _ipp._tcp service: %s\n", avahi_strerror(ret));
goto fail;
}
*/
/* Add the same service for BSD LPR */
for (size_t n=0; n<services.size(); ++n)
{
if ((ret = avahi_entry_group_add_service(group, AVAHI_IF_UNSPEC, services[n].proto_, AvahiPublishFlags(0), name, services[n].name_.c_str(), NULL, NULL, services[n].port_, NULL)) < 0)
{ {
if (ret == AVAHI_ERR_COLLISION) if (ret == AVAHI_ERR_COLLISION)
goto collision; goto collision;
logE << "Failed to add " << services[n].name_ << " service: " << avahi_strerror(ret) << "\n"; logE << "Failed to add " << service.name_ << " service: " << avahi_strerror(ret) << "\n";
goto fail; goto fail;
} }
} }
/* Add an additional (hypothetic) subtype */ /// Add an additional (hypothetic) subtype
/* if ((ret = avahi_entry_group_add_service_subtype(group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, AvahiPublishFlags(0), name, "_printer._tcp", NULL, "_magic._sub._printer._tcp") < 0)) /* if ((ret = avahi_entry_group_add_service_subtype(group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, AvahiPublishFlags(0), name, "_printer._tcp", NULL, "_magic._sub._printer._tcp") < 0))
{ {
fprintf(stderr, "Failed to add subtype _magic._sub._printer._tcp: %s\n", avahi_strerror(ret)); fprintf(stderr, "Failed to add subtype _magic._sub._printer._tcp: %s\n", avahi_strerror(ret));
goto fail; goto fail;
} }
*/ */
/* Tell the server to register the service */ /// Tell the server to register the service
if ((ret = avahi_entry_group_commit(group)) < 0) if ((ret = avahi_entry_group_commit(group)) < 0)
{ {
logE << "Failed to commit entry group: " << avahi_strerror(ret) << "\n"; logE << "Failed to commit entry group: " << avahi_strerror(ret) << "\n";
@ -204,8 +179,7 @@ void PublishAvahi::create_services(AvahiClient *c)
collision: collision:
/* A service name collision with a local service happened. Let's /// A service name collision with a local service happened. Let's pick a new name
* pick a new name */
n = avahi_alternative_service_name(name); n = avahi_alternative_service_name(name);
avahi_free(name); avahi_free(name);
name = n; name = n;
@ -226,14 +200,12 @@ void PublishAvahi::client_callback(AvahiClient *c, AvahiClientState state, AVAHI
{ {
assert(c); assert(c);
/* Called whenever the client or server state changes */ /// Called whenever the client or server state changes
switch (state) switch (state)
{ {
case AVAHI_CLIENT_S_RUNNING: case AVAHI_CLIENT_S_RUNNING:
/* The server has startup successfully and registered its host /// The server has startup successfully and registered its host name on the network, so it's time to create our services
* name on the network, so it's time to create our services */
static_cast<PublishAvahi*>(userdata)->create_services(c); static_cast<PublishAvahi*>(userdata)->create_services(c);
break; break;
@ -241,25 +213,20 @@ void PublishAvahi::client_callback(AvahiClient *c, AvahiClientState state, AVAHI
logE << "Client failure: " << avahi_strerror(avahi_client_errno(c)) << "\n"; logE << "Client failure: " << avahi_strerror(avahi_client_errno(c)) << "\n";
avahi_simple_poll_quit(simple_poll); avahi_simple_poll_quit(simple_poll);
break; break;
case AVAHI_CLIENT_S_COLLISION: case AVAHI_CLIENT_S_COLLISION:
/* Let's drop our registered services. When the server is back /// Let's drop our registered services. When the server is back
* in AVAHI_SERVER_RUNNING state we will register them /// in AVAHI_SERVER_RUNNING state we will register them again with the new host name.
* again with the new host name. */
case AVAHI_CLIENT_S_REGISTERING: case AVAHI_CLIENT_S_REGISTERING:
/* The server records are now being established. This /// The server records are now being established. This might be caused by a host name change. We need to wait
* might be caused by a host name change. We need to wait /// for our own records to register until the host name is properly esatblished.
* for our own records to register until the host name is
* properly esatblished. */
if (group) if (group)
avahi_entry_group_reset(group); avahi_entry_group_reset(group);
break; break;
case AVAHI_CLIENT_CONNECTING: case AVAHI_CLIENT_CONNECTING:

View file

@ -33,36 +33,26 @@
#include <thread> #include <thread>
#include <atomic> #include <atomic>
class PublishAvahi;
struct AvahiService #include "publishmDNS.h"
{
AvahiService(const std::string& name, size_t port, int proto = AVAHI_PROTO_UNSPEC) : name_(name), port_(port), proto_(proto)
{
}
std::string name_; class PublishAvahi : public PublishmDNS
size_t port_;
int proto_;
};
class PublishAvahi
{ {
public: public:
PublishAvahi(const std::string& serviceName); PublishAvahi(const std::string& serviceName);
~PublishAvahi(); virtual ~PublishAvahi();
void publish(const std::vector<AvahiService>& services); virtual void publish(const std::vector<mDNSService>& services);
private: private:
static void entry_group_callback(AvahiEntryGroup *g, AvahiEntryGroupState state, AVAHI_GCC_UNUSED void *userdata); static void entry_group_callback(AvahiEntryGroup *g, AvahiEntryGroupState state, AVAHI_GCC_UNUSED void *userdata);
static void client_callback(AvahiClient *c, AvahiClientState state, AVAHI_GCC_UNUSED void * userdata); static void client_callback(AvahiClient *c, AvahiClientState state, AVAHI_GCC_UNUSED void * userdata);
void create_services(AvahiClient *c); void create_services(AvahiClient *c);
AvahiClient* client;
std::string serviceName_;
std::thread pollThread_;
void worker(); void worker();
AvahiClient* client_;
std::thread pollThread_;
std::atomic<bool> active_; std::atomic<bool> active_;
std::vector<AvahiService> services; std::vector<mDNSService> services_;
}; };

View file

@ -0,0 +1,159 @@
/***
This file is part of snapcast
Copyright (C) 2014-2016 Johannes Pohl
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#include <cstdlib>
#include <thread>
#include "publishBonjour.h"
#include "common/log.h"
typedef union { unsigned char b[2]; unsigned short NotAnInteger; } Opaque16;
PublishBonjour::PublishBonjour(const std::string& serviceName) : PublishmDNS(serviceName), active_(false)
{
/// dns-sd -R Snapcast _snapcast._tcp local 1704
/// dns-sd -R Snapcast _snapcast-jsonrpc._tcp local 1705
}
PublishBonjour::~PublishBonjour()
{
active_ = false;
pollThread_.join();
for (auto client: clients)
{
if (client)
DNSServiceRefDeallocate(client);
}
}
void PublishBonjour::worker()
{
// int dns_sd_fd = client ? DNSServiceRefSockFD(client) : -1;
// 1. Set up the fd_set as usual here.
// This example client has no file descriptors of its own,
// but a real application would call FD_SET to add them to the set here
fd_set readfds;
FD_ZERO(&readfds);
std::vector<int> dns_sd_fds;
int nfds = -1;
for (size_t n=0; n<clients.size(); ++n)
{
int dns_sd_fd = DNSServiceRefSockFD(clients[n]);
dns_sd_fds.push_back(dns_sd_fd);
if (nfds < dns_sd_fd)
nfds = dns_sd_fd;
// 2. Add the fd for our client(s) to the fd_set
FD_SET(dns_sd_fd, &readfds);
}
++nfds;
// 3. Set up the timeout.
struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 100*1000;
active_ = true;
while (active_)
{
FD_ZERO(&readfds);
for (size_t n=0; n<dns_sd_fds.size(); ++n)
FD_SET(dns_sd_fds[n], &readfds);
int result = select(nfds, &readfds, (fd_set*)NULL, (fd_set*)NULL, &tv);
if (result > 0)
{
for (size_t n=0; n<dns_sd_fds.size(); ++n)
{
if (clients[n] && FD_ISSET(dns_sd_fds[n], &readfds))
{
DNSServiceErrorType err = DNSServiceProcessResult(clients[n]);
if (err)
{
logE << "DNSServiceProcessResult returned " << err << "\n";
active_ = false;
}
}
}
}
// else if (result == 0)
// myTimerCallBack();
else if (result < 0)
{
logE << "select() returned " << result << " errno " << errno << " " << strerror(errno) << "\n";
if (errno != EINTR)
active_ = false;
}
}
}
static void DNSSD_API reg_reply(DNSServiceRef sdref, const DNSServiceFlags flags, DNSServiceErrorType errorCode, const char *name, const char *regtype, const char *domain, void *context)
{
(void)sdref; // Unused
(void)flags; // Unused
PublishBonjour* publishBonjour = (PublishBonjour*)context;
(void)publishBonjour; // unused
logO << "Got a reply for service " << name << "." << regtype << domain << "\n";
if (errorCode == kDNSServiceErr_NoError)
{
if (flags & kDNSServiceFlagsAdd)
logO << "Name now registered and active\n";
else
logO << "Name registration removed\n";
}
else if (errorCode == kDNSServiceErr_NameConflict)
{
/// TODO: Error handling
logO << "Name in use, please choose another\n";
exit(-1);
}
else
logO << "Error " << errorCode << "\n";
if (!(flags & kDNSServiceFlagsMoreComing))
fflush(stdout);
}
void PublishBonjour::publish(const std::vector<mDNSService>& services)
{
for (auto service: services)
{
DNSServiceFlags flags = 0;
Opaque16 registerPort = { { static_cast<unsigned char>(service.port_ >> 8), static_cast<unsigned char>(service.port_ & 0xFF) } };
DNSServiceRef client = NULL;
// DNSServiceRegister(&client, flags, kDNSServiceInterfaceIndexAny, serviceName_.c_str(), service.name_.c_str(), NULL, NULL, registerPort.NotAnInteger, service.txt_.size(), service.txt_.empty()?NULL:service.txt_.c_str(), reg_reply, this);
DNSServiceRegister(&client, flags, kDNSServiceInterfaceIndexAny, serviceName_.c_str(), service.name_.c_str(), NULL, NULL, registerPort.NotAnInteger, 0, NULL, reg_reply, this);
clients.push_back(client);
}
pollThread_ = std::thread(&PublishBonjour::worker, this);
}

View file

@ -0,0 +1,47 @@
/***
This file is part of snapcast
Copyright (C) 2014-2016 Johannes Pohl
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef PUBLISH_BONJOUR_H
#define PUBLISH_BONJOUR_H
#include <string>
#include <dns_sd.h>
class PublishBonjour;
#include "publishmDNS.h"
class PublishBonjour : public PublishmDNS
{
public:
PublishBonjour(const std::string& serviceName);
virtual ~PublishBonjour();
virtual void publish(const std::vector<mDNSService>& services);
private:
std::thread pollThread_;
void worker();
std::atomic<bool> active_;
std::vector<DNSServiceRef> clients;
};
#endif

View file

@ -0,0 +1,44 @@
#ifndef PUBLISH_MDNS_H
#define PUBLISH_MDNS_H
#include <string>
#include <vector>
struct mDNSService
{
mDNSService(const std::string& name, size_t port) : name_(name), port_(port)
{
}
std::string name_;
size_t port_;
};
class PublishmDNS
{
public:
PublishmDNS(const std::string& serviceName) : serviceName_(serviceName)
{
}
virtual ~PublishmDNS()
{
}
virtual void publish(const std::vector<mDNSService>& services) = 0;
protected:
std::string serviceName_;
};
#if defined(HAS_AVAHI)
#include "publishAvahi.h"
typedef PublishAvahi PublishZeroConf;
#elif defined(HAS_BONJOUR)
#include "publishBonjour.h"
typedef PublishBonjour PublishZeroConf;
#endif
#endif

View file

@ -29,7 +29,9 @@
#include "message/message.h" #include "message/message.h"
#include "encoder/encoderFactory.h" #include "encoder/encoderFactory.h"
#include "streamServer.h" #include "streamServer.h"
#include "publishAvahi.h" #if defined(HAS_AVAHI) || defined(HAS_BONJOUR)
#include "publishZeroConf/publishmDNS.h"
#endif
#include "config.h" #include "config.h"
#include "common/log.h" #include "common/log.h"
@ -43,6 +45,9 @@ using namespace popl;
int main(int argc, char* argv[]) int main(int argc, char* argv[])
{ {
#ifdef MACOS
#pragma message "Warning: the macOS support is experimental and might not be maintained"
#endif
try try
{ {
StreamServerSettings settings; StreamServerSettings settings;
@ -142,12 +147,10 @@ int main(int argc, char* argv[])
setpriority(PRIO_PROCESS, 0, processPriority); setpriority(PRIO_PROCESS, 0, processPriority);
logS(kLogNotice) << "daemon started" << std::endl; logS(kLogNotice) << "daemon started" << std::endl;
} }
#if defined(HAS_AVAHI) || defined(HAS_BONJOUR)
PublishAvahi publishAvahi("Snapcast"); PublishZeroConf publishZeroConfg("Snapcast");
std::vector<AvahiService> services; publishZeroConfg.publish({mDNSService("_snapcast._tcp", settings.port), mDNSService("_snapcast-jsonrpc._tcp", settings.controlPort)});
services.push_back(AvahiService("_snapcast._tcp", settings.port)); #endif
services.push_back(AvahiService("_snapcast-jsonrpc._tcp", settings.controlPort));
publishAvahi.publish(services);
if (settings.bufferMs < 400) if (settings.bufferMs < 400)
settings.bufferMs = 400; settings.bufferMs = 400;
@ -181,7 +184,3 @@ int main(int argc, char* argv[])
return 0; return 0;
} }