diff --git a/audio_alsa.c b/audio_alsa.c index 372f84d1..5705380e 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -74,11 +74,13 @@ audio_output audio_alsa = { static pthread_mutex_t alsa_mutex = PTHREAD_MUTEX_INITIALIZER; +static snd_output_t *output = NULL; static unsigned int desired_sample_rate; static enum sps_format_t sample_format; static snd_pcm_t *alsa_handle = NULL; static snd_pcm_hw_params_t *alsa_params = NULL; +static snd_pcm_sw_params_t *alsa_swparams = NULL; static snd_ctl_t *ctl = NULL; static snd_ctl_elem_id_t *elem_id = NULL; static snd_mixer_t *alsa_mix_handle = NULL; @@ -134,6 +136,7 @@ static void help(void) { void set_alsa_out_dev(char *dev) { alsa_out_dev = dev; } +// assuming pthread cancellation is disabled int open_mixer() { int response = 0; if (hardware_mixer) { @@ -177,6 +180,7 @@ int open_mixer() { return response; } +// assuming pthread cancellation is disabled void close_mixer() { if (alsa_mix_handle) { snd_mixer_close(alsa_mix_handle); @@ -184,6 +188,7 @@ void close_mixer() { } } +// assuming pthread cancellation is disabled void do_snd_mixer_selem_set_playback_dB_all(snd_mixer_elem_t *mix_elem, double vol) { if (snd_mixer_selem_set_playback_dB_all(mix_elem, vol, 0) != 0) { debug(1, "Can't set playback volume accurately to %f dB.", vol); @@ -194,6 +199,9 @@ void do_snd_mixer_selem_set_playback_dB_all(snd_mixer_elem_t *mix_elem, double v } static int init(int argc, char **argv) { + // for debugging + snd_output_stdio_attach(&output, stdout, 0); + // debug(2,"audio_alsa init called."); int response = 0; // this will be what we return to the caller. const char *str; @@ -405,6 +413,8 @@ static int init(int argc, char **argv) { debug(1, "alsa: output device name is \"%s\".", alsa_out_dev); if (hardware_mixer) { + int oldState; + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable if (alsa_mix_dev == NULL) alsa_mix_dev = alsa_out_dev; @@ -491,6 +501,7 @@ static int init(int argc, char **argv) { } debug_mutex_unlock(&alsa_mutex, 3); // release the mutex pthread_cleanup_pop(0); + pthread_setcancelstate(oldState, NULL); } else { debug(1, "alsa: no hardware mixer selected."); } @@ -504,7 +515,8 @@ static void deinit(void) { stop(); } -int open_alsa_device(void) { +// assuming pthread cancellation is disabled +int actual_open_alsa_device(void) { // the alsa mutex is already acquired when this is called const snd_pcm_uframes_t minimal_buffer_headroom = 352 * 2; // we accept this much headroom in the hardware buffer, but we'll @@ -536,6 +548,7 @@ int open_alsa_device(void) { return (-10); snd_pcm_hw_params_alloca(&alsa_params); + snd_pcm_sw_params_alloca(&alsa_swparams); ret = snd_pcm_hw_params_any(alsa_handle, alsa_params); if (ret < 0) { @@ -692,6 +705,28 @@ int open_alsa_device(void) { return -9; } + ret = snd_pcm_sw_params_current(alsa_handle, alsa_swparams); + if (ret < 0) { + warn("audio_alsa: Unable to get current sw parameters for device \"%s\": %s.", alsa_out_dev, + snd_strerror(ret)); + return -10; + } + + ret = snd_pcm_sw_params_set_tstamp_mode(alsa_handle, alsa_swparams, SND_PCM_TSTAMP_ENABLE); + if (ret < 0) { + warn("audio_alsa: Can't enable timestamp mode of device: \"%s\": %s.", alsa_out_dev, + snd_strerror(ret)); + return -11; + } + + /* write the sw parameters */ + ret = snd_pcm_sw_params(alsa_handle, alsa_swparams); + if (ret < 0) { + warn("audio_alsa: Unable to set software parameters of device: \"%s\": %s.", alsa_out_dev, + snd_strerror(ret)); + return -12; + } + if (actual_buffer_length < config.audio_backend_buffer_desired_length + minimal_buffer_headroom) { /* // the dac buffer is too small, so let's try to set it @@ -847,10 +882,18 @@ int open_alsa_device(void) { break; } } - return 0; } +int open_alsa_device(void) { + int result; + int oldState; + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable + result = actual_open_alsa_device(); + pthread_setcancelstate(oldState, NULL); + return result; +} + static void start(int i_sample_rate, int i_sample_format) { // debug(2,"audio_alsa start called."); if (i_sample_rate == 0) @@ -867,6 +910,43 @@ static void start(int i_sample_rate, int i_sample_format) { measurement_data_is_valid = 0; } +// assuming pthread cancellation is disabled +int my_snd_pcm_delay(snd_pcm_t *pcm, snd_pcm_sframes_t *delayp) { + int ret; + snd_pcm_status_t *alsa_snd_pcm_status; + snd_pcm_status_alloca(&alsa_snd_pcm_status); + + struct timespec tn; // time now + snd_htimestamp_t update_timestamp; // actually a struct timespec + + ret = snd_pcm_status(pcm, alsa_snd_pcm_status); + if (ret) { + *delayp = 0; + return ret; + } + + snd_pcm_state_t state = snd_pcm_status_get_state(alsa_snd_pcm_status); + if (state != SND_PCM_STATE_RUNNING) { + *delayp = 0; + return -EIO; // might be a better code than this... + } + + clock_gettime(CLOCK_MONOTONIC, &tn); + snd_pcm_status_get_htstamp(alsa_snd_pcm_status, &update_timestamp); + + uint64_t t1 = tn.tv_sec * (uint64_t)1000000000 + tn.tv_nsec; + uint64_t t2 = update_timestamp.tv_sec * (uint64_t)1000000000 + update_timestamp.tv_nsec; + uint64_t delta = t1 - t2; + + uint64_t frames_played_since_last_interrupt = + ((uint64_t)desired_sample_rate * delta) / 1000000000; + snd_pcm_sframes_t frames_played_since_last_interrupt_sized = frames_played_since_last_interrupt; + + *delayp = + snd_pcm_status_get_delay(alsa_snd_pcm_status) - frames_played_since_last_interrupt_sized; + return 0; +} + int delay(long *the_delay) { // snd_pcm_sframes_t is a signed long -- hence the return of a "long" int reply = 0; @@ -874,7 +954,9 @@ int delay(long *the_delay) { if (alsa_handle == NULL) { return -ENODEV; } else { - pthread_cleanup_debug_mutex_lock(&alsa_mutex, 10000, 0); + int oldState; + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable + pthread_cleanup_debug_mutex_lock(&alsa_mutex, 10000, 1); int derr; snd_pcm_state_t dac_state = snd_pcm_state(alsa_handle); if (dac_state == SND_PCM_STATE_RUNNING) { @@ -926,6 +1008,7 @@ int delay(long *the_delay) { // here, occasionally pretend there's a problem with pcm_get_delay() // if ((random() % 100000) < 3) // keep it pretty rare // reply = -EIO; // pretend something bad has happened + pthread_setcancelstate(oldState, NULL); return reply; } } @@ -945,10 +1028,13 @@ int get_rate_information(uint64_t *elapsed_time, uint64_t *frames_played) { static int play(void *buf, int samples) { // debug(3,"audio_alsa play called."); + int oldState; + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable int ret = 0; if (alsa_handle == NULL) { + pthread_cleanup_debug_mutex_lock(&alsa_mutex, 10000, 1); - ret = open_alsa_device(); + ret = actual_open_alsa_device(); if (ret == 0) { if (audio_alsa.volume) do_volume(set_volume); @@ -1003,7 +1089,7 @@ static int play(void *buf, int samples) { frame_index++; if ((frame_index == start_measurement_from_this_frame) || (frame_index % 32 == 0)) { long fl = 0; - err2 = snd_pcm_delay(alsa_handle, &fl); + err2 = my_snd_pcm_delay(alsa_handle, &fl); if (err2 != 0) { debug(1, "Error %d in delay in play(): \"%s\". Delay reported is %d frames.", err2, snd_strerror(err2), fl); @@ -1048,13 +1134,14 @@ static int play(void *buf, int samples) { debug_mutex_unlock(&alsa_mutex, 0); pthread_cleanup_pop(0); // release the mutex } + pthread_setcancelstate(oldState, NULL); return ret; } static void flush(void) { // debug(2,"audio_alsa flush called."); int oldState; - pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable pthread_cleanup_debug_mutex_lock(&alsa_mutex, 10000, 1); int derr; do_mute(1); @@ -1095,6 +1182,8 @@ static void parameters(audio_parameters *info) { } void do_volume(double vol) { // caller is assumed to have the alsa_mutex when using this function + int oldState; + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable debug(3, "Setting volume db to %f.", vol); set_volume = vol; if (volume_set_request && (open_mixer() == 1)) { @@ -1126,6 +1215,7 @@ void do_volume(double vol) { // caller is assumed to have the alsa_mutex when us volume_set_request = 0; // any external request that has been made is now satisfied close_mixer(); } + pthread_setcancelstate(oldState, NULL); } void volume(double vol) { @@ -1166,6 +1256,9 @@ static void mute(int mute_state_requested) { void do_mute(int mute_state_requested) { + int oldState; + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable + // if a mute is requested now, then // if an external mute request is in place, leave everything muted // otherwise, if an external mute request is pending, action it @@ -1206,4 +1299,5 @@ void do_mute(int mute_state_requested) { } } mute_request_pending = 0; + pthread_setcancelstate(oldState, NULL); } diff --git a/common.c b/common.c index 8cbcd0d4..264ef499 100644 --- a/common.c +++ b/common.c @@ -739,6 +739,38 @@ void command_start(void) { } } } +void command_execute(const char *command) { + // this has a cancellation point if waiting is enabled + if (command) { + /*Spawn a child to run the program.*/ + pid_t pid = fork(); + if (pid == 0) { /* child process */ + int argC; + char **argV; + if (poptParseArgvString(command, &argC, (const char ***)&argV) != + 0) // note that argV should be free()'d after use, but we expect this fork to exit + // eventually. + debug(1, "Can't decipher command arguments in \"%s\".", command); + else { + // debug(1,"Executing command %s",command); + execv(argV[0], argV); + warn("Execution of command \"%s\" failed to start", command); + debug(1, "Error executing command \"%s\".", command); + exit(127); /* only if execv fails */ + } + } else { + if (config.cmd_blocking) { /* pid!=0 means parent process and if blocking is true, wait for + process to finish */ + pid_t rc = waitpid(pid, 0, 0); /* wait for child to exit */ + if (rc != pid) { + warn("Execution of command \"%s\" returned an error.", command); + debug(1, "Command \"%s\" finished with error %d", command, errno); + } + } + // debug(1,"Continue after on-unfixable command"); + } + } +} void command_stop(void) { // this has a cancellation point if waiting is enabled @@ -1142,10 +1174,10 @@ int sps_pthread_mutex_timedlock(pthread_mutex_t *mutex, useconds_t dally_time, // this is not pthread_cancellation safe because is contains a cancellation point int oldState; pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); - useconds_t time_to_wait = dally_time; + int time_to_wait = dally_time; int r = pthread_mutex_trylock(mutex); while ((r == EBUSY) && (time_to_wait > 0)) { - useconds_t st = time_to_wait; + int st = time_to_wait; if (st > 1000) st = 1000; sps_nanosleep(0, st * 1000); // this contains a cancellation point diff --git a/common.h b/common.h index ece21b1d..a9f4ab7d 100644 --- a/common.h +++ b/common.h @@ -148,7 +148,7 @@ typedef struct { int debugger_show_relative_time; // in the debug message, display the time since the last one int statistics_requested, use_negotiated_latencies; enum playback_mode_type playback_mode; - char *cmd_start, *cmd_stop, *cmd_set_volume; + char *cmd_start, *cmd_stop, *cmd_set_volume, *cmd_unfixable; int cmd_blocking, cmd_start_returns_output; double tolerance; // allow this much drift before attempting to correct it enum stuffing_type packet_stuffing; @@ -296,6 +296,7 @@ int config_set_lookup_bool(config_t *cfg, char *where, int *dst); void command_start(void); void command_stop(void); +void command_execute(const char *command); void command_set_volume(double volume); int mkpath(const char *path, mode_t mode); diff --git a/configure.ac b/configure.ac index ca23c4e4..8f3248ab 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ # Process this file with autoconf to produce a configure script. AC_PREREQ([2.50]) -AC_INIT([shairport-sync], [3.3d31], [mikebrady@eircom.net]) +AC_INIT([shairport-sync], [3.3d34], [mikebrady@eircom.net]) AM_INIT_AUTOMAKE AC_CONFIG_SRCDIR([shairport.c]) AC_CONFIG_HEADERS([config.h]) diff --git a/player.h b/player.h index deb787c6..9346ef2b 100644 --- a/player.h +++ b/player.h @@ -82,13 +82,14 @@ typedef struct { char *auth_nonce; // the session nonce, if needed stream_cfg stream; SOCKADDR remote, local; - int stop; - int running; + volatile int stop; + volatile int running; + volatile uint64_t watchdog_bark_time; + volatile int watchdog_barks; // number of times the watchdog has timed out and done something + time_t playstart; pthread_t thread, timer_requester, rtp_audio_thread, rtp_control_thread, rtp_timing_thread, player_watchdog_thread; - int64_t watchdog_bark_time; - // pthread_t *ptp; // buffers to delete on exit signed short *tbuf; diff --git a/rtp.c b/rtp.c index 1da6097f..ac1a43b6 100644 --- a/rtp.c +++ b/rtp.c @@ -482,7 +482,13 @@ void *rtp_control_receiver(void *arg) { pthread_exit(NULL); } +void rtp_timing_sender_cleanup_handler(void *arg) { + rtsp_conn_info *conn = (rtsp_conn_info *)arg; + debug(3, "Connection %d: Timing Sender Cleanup.", conn->connection_number); +} + void *rtp_timing_sender(void *arg) { + pthread_cleanup_push(rtp_timing_sender_cleanup_handler, arg); rtsp_conn_info *conn = (rtsp_conn_info *)arg; struct timing_request { char leader; @@ -542,6 +548,7 @@ void *rtp_timing_sender(void *arg) { usleep(3000000); } debug(3, "rtp_timing_sender thread interrupted. This should never happen."); + pthread_cleanup_pop(0); // don't execute anything here. pthread_exit(NULL); } diff --git a/rtsp.c b/rtsp.c index a5079616..158b3263 100644 --- a/rtsp.c +++ b/rtsp.c @@ -257,7 +257,7 @@ int pc_queue_get_item(pc_queue *the_queue, void *the_stuff) { int have_player(rtsp_conn_info *conn) { int response = 0; debug_mutex_lock(&playing_conn_lock, 1000000, 3); - if (playing_conn == conn) // this connection definitely doesn't have the play lock + if (playing_conn == conn) // this connection definitely has the play lock response = 1; debug_mutex_unlock(&playing_conn_lock, 3); return response; @@ -283,11 +283,25 @@ void *player_watchdog_thread_code(void *arg) { uint64_t ct = config.timeout; // go from int to 64-bit int if (time_since_last_bark >= ct) { - debug(1, "Connection %d: As Yeats almost said, \"Too long a silence / can make a stone " - "of the heart\".", - conn->connection_number); - conn->stop = 1; - pthread_cancel(conn->thread); + conn->watchdog_barks++; + if (conn->watchdog_barks == 1) { + debug(1, "Connection %d: As Yeats almost said, \"Too long a silence / can make a stone " + "of the heart\".", + conn->connection_number); + conn->stop = 1; + pthread_cancel(conn->thread); + } else if (conn->watchdog_barks == 3) { + if (config.cmd_unfixable) { + warn("Connection %d: An unfixable error has been detected. Executing the " + "\"run_this_if_an_unfixable_error_is_detected\" command.", + conn->connection_number); + command_execute(config.cmd_unfixable); + } else { + warn("Connection %d: An unfixable error has been detected. \"No " + "run_this_if_an_unfixable_error_is_detected\" command provided -- nothing done.", + conn->connection_number); + } + } } } } @@ -970,7 +984,8 @@ void handle_setup(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { if (resp->respcode != 200) { debug(1, "Connection %d: SETUP error -- releasing the player lock.", conn->connection_number); debug_mutex_lock(&playing_conn_lock, 1000000, 3); - playing_conn = NULL; // this connection definitely doesn't have the play lock + if (playing_conn == conn) // if we have the player + playing_conn = NULL; // let it go debug_mutex_unlock(&playing_conn_lock, 3); } @@ -1630,7 +1645,7 @@ static void handle_announce(rtsp_conn_info *conn, rtsp_message *req, rtsp_messag debug_mutex_unlock(&playing_conn_lock, 3); if (should_wait) { - useconds_t time_remaining = 3000000; + int time_remaining = 3000000; // must be signed, as it could go negative... while ((time_remaining > 0) && (have_the_player == 0)) { debug_mutex_lock(&playing_conn_lock, 1000000, 3); // get it @@ -1829,7 +1844,8 @@ out: debug(1, "Connection %d: Error in handling ANNOUNCE. Unlocking the play lock.", conn->connection_number); debug_mutex_lock(&playing_conn_lock, 1000000, 3); // get it - playing_conn = NULL; + if (playing_conn == conn) // if we managed to acquire it + playing_conn = NULL; // let it go debug_mutex_unlock(&playing_conn_lock, 3); } } @@ -2123,9 +2139,9 @@ void rtsp_conversation_thread_cleanup_function(void *arg) { debug(3, "Connection %d: Checking play lock.", conn->connection_number); debug_mutex_lock(&playing_conn_lock, 1000000, 3); // get it - if (playing_conn == conn) { + if (playing_conn == conn) { // if it's ours debug(2, "Connection %d: Unlocking play lock.", conn->connection_number); - playing_conn = NULL; + playing_conn = NULL; // let it go } debug_mutex_unlock(&playing_conn_lock, 3); @@ -2142,7 +2158,8 @@ void msg_cleanup_function(void *arg) { static void *rtsp_conversation_thread_func(void *pconn) { rtsp_conn_info *conn = pconn; - // create the watchdog mutex and start the watchdog thread; + // create the watchdog mutex, initialise the watchdog time and start the watchdog thread; + conn->watchdog_bark_time = get_absolute_time_in_fp(); pthread_mutex_init(&conn->watchdog_mutex, NULL); pthread_create(&conn->player_watchdog_thread, NULL, &player_watchdog_thread_code, (void *)conn); @@ -2392,6 +2409,11 @@ void rtsp_listen_loop(void) { if (setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, (const char *)&tv, sizeof tv) == -1) debug(1, "Error %d setting send timeout for rtsp writeback.", errno); + tv.tv_sec = 30; // 30 seconds read timeout + tv.tv_usec = 0; + if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (const char *)&tv, sizeof tv) == -1) + debug(1, "Error %d setting send timeout for rtsp writeback.", errno); + #ifdef IPV6_V6ONLY // some systems don't support v4 access on v6 sockets, but some do. // since we need to account for two sockets we might as well diff --git a/scripts/shairport-sync.conf b/scripts/shairport-sync.conf index 55ac4cbf..97431d79 100644 --- a/scripts/shairport-sync.conf +++ b/scripts/shairport-sync.conf @@ -53,6 +53,7 @@ sessioncontrol = { // run_this_before_play_begins = "/full/path/to/application and args"; // make sure the application has executable permission. If it's a script, include the shebang (#!/bin/...) on the first line // run_this_after_play_ends = "/full/path/to/application and args"; // make sure the application has executable permission. If it's a script, include the shebang (#!/bin/...) on the first line +// run_this_if_an_unfixable_error_is_detected = "/full/path/to/application and args"; // if a problem occurs that can't be cleared. It might restart the whole device. // wait_for_completion = "no"; // set to "yes" to get Shairport Sync to wait until the "run_this..." applications have terminated before continuing // allow_session_interruption = "no"; // set to "yes" to allow another device to interrupt Shairport Sync while it's playing from an existing audio source // session_timeout = 120; // wait for this number of seconds after a source disappears before terminating the session and becoming available again. diff --git a/shairport.c b/shairport.c index 11f085b1..65c0bd68 100644 --- a/shairport.c +++ b/shairport.c @@ -721,6 +721,11 @@ int parse_options(int argc, char **argv) { config.cmd_stop = (char *)str; } + if (config_lookup_string(config.cfg, + "sessioncontrol.run_this_if_an_unfixable_error_is_detected", &str)) { + config.cmd_unfixable = (char *)str; + } + if (config_lookup_string(config.cfg, "sessioncontrol.wait_for_completion", &str)) { if (strcasecmp(str, "no") == 0) config.cmd_blocking = 0;