From f1f59099e510d867ce529c6b9a63b973b5a89ded Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 31 Oct 2022 09:42:49 +0000 Subject: [PATCH 01/51] Try AC_CHECK_LIB is PKG_CHECK_MODULES can't find libavcodec --- configure.ac | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index b65aad38..b88b18cc 100644 --- a/configure.ac +++ b/configure.ac @@ -420,7 +420,10 @@ if test "x$with_airplay_2" = "xyes" ; then PKG_CHECK_MODULES([libavutil], [libavutil],[CFLAGS="${libavutil_CFLAGS} ${CFLAGS}" LIBS="${libavutil_LIBS} ${LIBS}"],[ AC_MSG_ERROR(AirPlay 2 support requires libavutil -- libavutil-dev suggested)]) PKG_CHECK_MODULES([libavcodec], [libavcodec],[CFLAGS="${libavcodec_CFLAGS} ${CFLAGS}" LIBS="${libavcodec_LIBS} ${LIBS}"],[ - AC_MSG_ERROR(AirPlay 2 support requires libavcodec -- libavcodec-dev suggested)]) + AC_CHECK_LIB([avcodec], [avcodec_find_decoder], [], [ + AC_MSG_ERROR(AirPlay 2 support requires libavcodec -- libavcodec-dev suggested) + ]) + ]) AC_CHECK_LIB([avformat],[avformat_new_stream], [], [AC_MSG_ERROR([AirPlay 2 support requires libavformat -- libavformat-dev suggested])]) AC_CHECK_LIB([swresample],[swr_convert], [], [AC_MSG_ERROR([AirPlay 2 support requires libswresample -- libswresample-dev suggested])]) AC_CHECK_LIB([uuid],[uuid_generate], [], [AC_MSG_ERROR([AirPlay 2 support requires the uuid library -- uuid-dev suggested])]) From 6ac12d9e5c2c3bcf5434d259a63d17934c367b88 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 31 Oct 2022 10:13:48 +0000 Subject: [PATCH 02/51] Update check_ap2_systemd_full.yml Ask for Ubuntu 22.04 explicitly --- .github/workflows/check_ap2_systemd_full.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check_ap2_systemd_full.yml b/.github/workflows/check_ap2_systemd_full.yml index 3461a4f9..780dd3a4 100644 --- a/.github/workflows/check_ap2_systemd_full.yml +++ b/.github/workflows/check_ap2_systemd_full.yml @@ -7,7 +7,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 From 694e187d79e923b792a255f5fd7965666496275b Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 31 Oct 2022 10:17:51 +0000 Subject: [PATCH 03/51] Update check_ap2_systemd_basic.yml Ask for Ubuntu 22.04 explicitly --- .github/workflows/check_ap2_systemd_basic.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check_ap2_systemd_basic.yml b/.github/workflows/check_ap2_systemd_basic.yml index e4ddc504..3359bace 100644 --- a/.github/workflows/check_ap2_systemd_basic.yml +++ b/.github/workflows/check_ap2_systemd_basic.yml @@ -7,7 +7,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 From 0390d13cca5153b9d53c1eb28a0793bb48171fbc Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 31 Oct 2022 10:22:10 +0000 Subject: [PATCH 04/51] Use Ubuntu 22.04 gitub action runners to get around an apparent problem. --- .github/workflows/check_ap2_systemd_full_build_folder.yml | 2 +- .github/workflows/check_ap2_systemv_full.yml | 2 +- .github/workflows/check_classic_systemd_basic.yml | 2 +- .github/workflows/check_classic_systemd_full.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/check_ap2_systemd_full_build_folder.yml b/.github/workflows/check_ap2_systemd_full_build_folder.yml index a877fc35..f3b743b5 100644 --- a/.github/workflows/check_ap2_systemd_full_build_folder.yml +++ b/.github/workflows/check_ap2_systemd_full_build_folder.yml @@ -7,7 +7,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/check_ap2_systemv_full.yml b/.github/workflows/check_ap2_systemv_full.yml index 8397ecb0..d3ff26f8 100644 --- a/.github/workflows/check_ap2_systemv_full.yml +++ b/.github/workflows/check_ap2_systemv_full.yml @@ -7,7 +7,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/check_classic_systemd_basic.yml b/.github/workflows/check_classic_systemd_basic.yml index dc2e9ec4..5988e965 100644 --- a/.github/workflows/check_classic_systemd_basic.yml +++ b/.github/workflows/check_classic_systemd_basic.yml @@ -7,7 +7,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/check_classic_systemd_full.yml b/.github/workflows/check_classic_systemd_full.yml index bf3ad651..bbc7c02e 100644 --- a/.github/workflows/check_classic_systemd_full.yml +++ b/.github/workflows/check_classic_systemd_full.yml @@ -7,7 +7,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 From de44134991ec89458a887944e86e08fe74ad4b60 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 3 Nov 2022 12:02:10 +0000 Subject: [PATCH 05/51] Add another file to look for and delete when cleaning up. --- BUILD.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/BUILD.md b/BUILD.md index 6db01e3f..527a41a3 100644 --- a/BUILD.md +++ b/BUILD.md @@ -23,8 +23,9 @@ Do this until no more copies of `shairport-sync` are found. You should also remove any of the following service files that may be present: * `/etc/systemd/system/shairport-sync.service` * `/etc/systemd/user/shairport-sync.service` +* `/lib/systemd/system/shairport-sync.service` * `/lib/systemd/user/shairport-sync.service` -* `/etc/init.d/shairport-sync` +* `/etc/init.d/shairport-sync` New service files will be installed if necessary at the `# make install` stage. #### Reboot after Cleaning Up From 085bf128fe7f0ba15729d9d2cd99630e3b9188b9 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 3 Nov 2022 15:04:32 +0000 Subject: [PATCH 06/51] Fix a long-standing bug which didn't close the socket used for the RTSP connection, so potentially exhausting the sockets available. --- rtsp.c | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/rtsp.c b/rtsp.c index 52add733..b162fbac 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1341,14 +1341,11 @@ enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn, rtsp_mes nread = read_from_rtsp_connection(conn, buf + inbuf, buflen - inbuf); if (nread == 0) { - // a blocking read that returns zero means eof -- implies connection closed - - debug(1, "Connection %d: Closed by client: from %s:%u to self at %s:%u.", - conn->connection_number, - conn->client_ip_string, conn->client_rtsp_port, - conn->self_ip_string, conn->self_rtsp_port); - conn->fd = 0; + // a blocking read that returns zero means eof -- implies connection closed by client + debug(1, "Connection %d: RTSP connection closed by client.", + conn->connection_number); reply = rtsp_read_request_response_channel_closed; + // Note: the socket will be closed when the thread exits goto shutdown; } @@ -4984,26 +4981,32 @@ void rtsp_conversation_thread_cleanup_function(void *arg) { } #endif - debug(3, "Connection %d terminating:Closing timing, control and audio sockets...", + debug(3, "Connection %d: terminating -- closing timing, control and audio sockets...", conn->connection_number); if (conn->control_socket) { + debug(3, "Connection %d: terminating -- closing control_socket %d.", + conn->connection_number, conn->control_socket); close(conn->control_socket); + conn->control_socket = 0; } if (conn->timing_socket) { + debug(3, "Connection %d: terminating -- closing timing_socket %d.", + conn->connection_number, conn->timing_socket); close(conn->timing_socket); + conn->timing_socket = 0; } if (conn->audio_socket) { + debug(3, "Connection %d: terminating -- closing audio_socket %d.", + conn->connection_number, conn->audio_socket); close(conn->audio_socket); + conn->audio_socket = 0; } - if (conn->fd > 0) { - debug(3, "Connection %d terminating: closing fd %d.", conn->connection_number, conn->fd); - close(conn->fd); - debug(3, "Connection %d terminating: closed fd %d.", conn->connection_number, conn->fd); - - debug(1, "Connection %d Closed by self: from %s:%u to self at %s:%u.", - conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, + debug(1, "Connection %d: terminating -- closing RTSP connection socket %d: from %s:%u to self at %s:%u.", + conn->connection_number, conn->fd, conn->client_ip_string, conn->client_rtsp_port, conn->self_ip_string, conn->self_rtsp_port); + close(conn->fd); + conn->fd = 0; } if (conn->auth_nonce) { free(conn->auth_nonce); From f75c7ba3834f93d89d90ebc300e8ea7bb92261ca Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 3 Nov 2022 15:24:07 +0000 Subject: [PATCH 07/51] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index b9dc5013..1840b2b0 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,12 @@ +Version 4.1-dev-772-g085bf128 +==== +**Bug Fix** +* Fix a long-standing bug whereby the socket used for the RTSP connection wasn't closed when the connection was closed by the client, potentially exhausting the sockets available. Many thanks to [Lars Strojny](https://github.com/lstrojny) for the report: [#1567](https://github.com/mikebrady/shairport-sync/issues/1567). + +**Enhancements** +* Small documentation updates. +* More use of GitHub actions to check standard builds. + Version 4.1-dev-756-g0a8b1231 ==== **Docker Updates** From 3e4b67e97de4abc9776ec91f184aef631d5a447d Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 7 Nov 2022 11:33:05 +0000 Subject: [PATCH 08/51] Add two new metadata items -- the player's service name, e.g. "Den Speakers" and the name of the current client, if any, e.g. "Joe's iPhone". --- dbus-service.c | 3 +++ metadata_hub.c | 9 +++++++++ metadata_hub.h | 3 +++ mqtt.c | 3 +++ org.gnome.ShairportSync.xml | 2 ++ rtsp.c | 3 +++ shairport.c | 4 ++++ 7 files changed, 27 insertions(+) diff --git a/dbus-service.c b/dbus-service.c index c6cc2ac7..b8cfea16 100644 --- a/dbus-service.c +++ b/dbus-service.c @@ -68,6 +68,7 @@ void dbus_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)) argc->airplay_volume); shairport_sync_remote_control_set_client(shairportSyncRemoteControlSkeleton, argc->client_ip); + shairport_sync_remote_control_set_client_name(shairportSyncRemoteControlSkeleton, argc->client_name); // although it's a DACP server, the server is in fact, part of the the AirPlay "client" (their // term). @@ -1019,6 +1020,8 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name // NULL); #endif + shairport_sync_set_service_name(SHAIRPORT_SYNC(shairportSyncSkeleton), config.service_name); + #ifdef CONFIG_AIRPLAY_2 shairport_sync_set_protocol(SHAIRPORT_SYNC(shairportSyncSkeleton), "AirPlay 2"); #else diff --git a/metadata_hub.c b/metadata_hub.c index b66035a0..9ad7052d 100644 --- a/metadata_hub.c +++ b/metadata_hub.c @@ -102,6 +102,7 @@ void run_metadata_watchers(void) { // turn off changed flags metadata_store.cover_art_pathname_changed = 0; metadata_store.client_ip_changed = 0; + metadata_store.client_name_changed = 0; metadata_store.server_ip_changed = 0; metadata_store.progress_string_changed = 0; metadata_store.item_id_changed = 0; @@ -549,6 +550,14 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin } free(cs); break; + case 'snam': + cs = strndup(data, length); + if (string_update(&metadata_store.client_name, &metadata_store.client_name_changed, cs)) { + changed = 1; + debug(1, "MH Client Name set to: \"%s\"", metadata_store.client_name); + } + free(cs); + break; case 'prgr': cs = strndup(data, length); if (string_update(&metadata_store.progress_string, &metadata_store.progress_string_changed, diff --git a/metadata_hub.h b/metadata_hub.h index bdbbf321..bb57a8d5 100644 --- a/metadata_hub.h +++ b/metadata_hub.h @@ -43,6 +43,9 @@ typedef struct metadata_bundle { // server int client_ip_changed; + char *client_name; // the name of the client device, if available + int client_name_changed; + char *server_ip; // IP number used by Shairport Sync int server_ip_changed; diff --git a/mqtt.c b/mqtt.c index a007cbc6..b12eb97d 100644 --- a/mqtt.c +++ b/mqtt.c @@ -219,6 +219,9 @@ void mqtt_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t le case 'svip': mqtt_publish("server_ip", data, length); break; + case 'svna': + mqtt_publish("service_name", data, length); + break; } } } diff --git a/org.gnome.ShairportSync.xml b/org.gnome.ShairportSync.xml index 1051bab6..c9f5868f 100644 --- a/org.gnome.ShairportSync.xml +++ b/org.gnome.ShairportSync.xml @@ -24,6 +24,7 @@ + @@ -49,6 +50,7 @@ + diff --git a/rtsp.c b/rtsp.c index b162fbac..0a940b05 100644 --- a/rtsp.c +++ b/rtsp.c @@ -2864,6 +2864,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) "and open a TCP port.", conn->connection_number); conn->airplay_stream_category = unspecified_stream_category; + // figure out what category of stream it is, by looking at the plist plist_t timingProtocol = plist_dict_get_item(messagePlist, "timingProtocol"); if (timingProtocol != NULL) { @@ -3669,6 +3670,8 @@ void handle_set_parameter_parameter(rtsp_conn_info *conn, rtsp_message *req, // play session is stopped. // `svip` -- the payload is the IP number of the server, i.e. the player itself. // Can be an IPv4 or an IPv6 number. +// `svna` -- the payload is the service name of the player, i.e. the name by +// which it is seen in the AirPlay menu. // `disc` -- the payload is the IP number of the client, i.e. the sender of audio. // Can be an IPv4 or an IPv6 number. This is an AirPlay-2-only message. // It is sent when a client has been disconnected. diff --git a/shairport.c b/shairport.c index f8f85ad9..86c01e83 100644 --- a/shairport.c +++ b/shairport.c @@ -2537,6 +2537,10 @@ int main(int argc, char **argv) { } #endif +#ifdef CONFIG_METADATA + send_ssnc_metadata('svna', config.service_name, strlen(config.service_name), 1); +#endif + activity_monitor_start(); // not yet for AP2 pthread_create(&rtsp_listener_thread, NULL, &rtsp_listen_loop, NULL); atexit(exit_rtsp_listener); From 0389e7de6bfe41e0034273cb74ec0d173a1c94aa Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 7 Nov 2022 11:45:19 +0000 Subject: [PATCH 09/51] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 1840b2b0..39e345d8 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,11 @@ +Version 4.1-dev-774-g3e4b67e9 +==== +**Metadata Enhancements** +* Add the service name, e.g. `"Den Speakers"` as metadata. This has the code `"svna"` in the metadata pipe and the name `"service_name"` in the MQTT interface. +* Add two properties to the D-Bus metadata interface: + 1. `ServiceName` -- see above. + 2. `ClientName` -- the name of the current client, e.g. `"Joes's iPad"`. + Version 4.1-dev-772-g085bf128 ==== **Bug Fix** From 27754a6240c74da9934dd88855784c9632314ec5 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 12 Nov 2022 12:04:14 +0000 Subject: [PATCH 10/51] Add the configuration file and the sample configuratioon file to the image. --- docker/Dockerfile | 2 ++ docker/classic/Dockerfile | 2 ++ 2 files changed, 4 insertions(+) diff --git a/docker/Dockerfile b/docker/Dockerfile index 11647acd..b32c7bdf 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -89,6 +89,8 @@ RUN apk -U add \ COPY --from=builder /shairport-sync/build/install/usr/local/bin/shairport-sync /usr/local/bin/shairport-sync COPY --from=builder /usr/local/bin/nqptp /usr/local/bin/nqptp COPY --from=builder /usr/local/lib/libalac.* /usr/local/lib/ +COPY --from=builder /shairport-sync/build/install/etc/shairport-sync.conf /etc/ +COPY --from=builder /shairport-sync/build/install/etc/shairport-sync.conf.sample /etc/ COPY --from=builder /shairport-sync/build/install/etc/dbus-1/system.d/shairport-sync-dbus.conf /etc/dbus-1/system.d/ COPY --from=builder /shairport-sync/build/install/etc/dbus-1/system.d/shairport-sync-mpris.conf /etc/dbus-1/system.d/ diff --git a/docker/classic/Dockerfile b/docker/classic/Dockerfile index 3010ca11..36f0b9c4 100644 --- a/docker/classic/Dockerfile +++ b/docker/classic/Dockerfile @@ -68,6 +68,8 @@ RUN apk -U add \ # Copy build files. COPY --from=builder /shairport-sync/build/install/usr/local/bin/shairport-sync /usr/local/bin/shairport-sync COPY --from=builder /usr/local/lib/libalac.* /usr/local/lib/ +COPY --from=builder /shairport-sync/build/install/etc/shairport-sync.conf /etc/ +COPY --from=builder /shairport-sync/build/install/etc/shairport-sync.conf.sample /etc/ COPY --from=builder /shairport-sync/build/install/etc/dbus-1/system.d/shairport-sync-dbus.conf /etc/dbus-1/system.d/ COPY --from=builder /shairport-sync/build/install/etc/dbus-1/system.d/shairport-sync-mpris.conf /etc/dbus-1/system.d/ From c82e49ca25a44fa44dcd7c791932fadc9afa3ab9 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 12 Nov 2022 12:10:25 +0000 Subject: [PATCH 11/51] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 39e345d8..341a3bf9 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version 4.1-dev-776-g27754a62 +==== +**Docker Enhancements** +* Place the configuration file and the sample configuration file in the image at `/etc/shairport-sync.conf` and `/etc/shairport-sync.conf.sample`. Thanks to [inDomus](https://github.com/KC-inDomus) and [tamasfodor1988](https://github.com/tamasfodor1988) for the report, Issue [#1575](https://github.com/mikebrady/shairport-sync/issues/1575). + Version 4.1-dev-774-g3e4b67e9 ==== **Metadata Enhancements** From b38a52b40b5c4dfb2d3e168952da086b3fa510f6 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 12 Nov 2022 12:10:43 +0000 Subject: [PATCH 12/51] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 341a3bf9..49f21269 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,7 +1,7 @@ Version 4.1-dev-776-g27754a62 ==== **Docker Enhancements** -* Place the configuration file and the sample configuration file in the image at `/etc/shairport-sync.conf` and `/etc/shairport-sync.conf.sample`. Thanks to [inDomus](https://github.com/KC-inDomus) and [tamasfodor1988](https://github.com/tamasfodor1988) for the report, Issue [#1575](https://github.com/mikebrady/shairport-sync/issues/1575). +* Place the configuration file and the sample configuration file in the Docker image at `/etc/shairport-sync.conf` and `/etc/shairport-sync.conf.sample`. Thanks to [inDomus](https://github.com/KC-inDomus) and [tamasfodor1988](https://github.com/tamasfodor1988) for the report, Issue [#1575](https://github.com/mikebrady/shairport-sync/issues/1575). Version 4.1-dev-774-g3e4b67e9 ==== From d67909b837feb9367f0731b70017c8fecf3ec5d9 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 12 Nov 2022 16:42:25 +0000 Subject: [PATCH 13/51] Add new metadata item: sps:songdatakind, derived from the asdk metadata token. It seems to indicate a timed item (0) or an untimed stream (1). Add output format, output frame rate, stream type (Realtime/Buffered/Classic) properties. Update MQTT appropriately. --- dacp.c | 6 +++-- dbus-service.c | 28 +++++++++++++++++--- metadata_hub.c | 32 ++++++++++++++++++++--- metadata_hub.h | 15 +++++++++-- mpris-service.c | 4 +-- mqtt.c | 14 +++++++++- org.gnome.ShairportSync.xml | 1 + player.c | 52 +++++++++++++++++++++++++------------ shairport.c | 7 +++++ 9 files changed, 128 insertions(+), 31 deletions(-) diff --git a/dacp.c b/dacp.c index 73164ae2..d557de24 100644 --- a/dacp.c +++ b/dacp.c @@ -803,8 +803,8 @@ void *dacp_monitor_thread_code(__attribute__((unused)) void *na) { case 'canp': // nowplaying 4 ids: dbid, plid, playlistItem, itemid (from mellowware // see reference above) debug(2, "DACP Composite ID seen"); - if (memcmp(metadata_store.item_composite_id, sp - item_size, - sizeof(metadata_store.item_composite_id)) != 0) { + if ((metadata_store.item_composite_id_is_valid == 0) || (memcmp(metadata_store.item_composite_id, sp - item_size, + sizeof(metadata_store.item_composite_id)) != 0)) { memcpy(metadata_store.item_composite_id, sp - item_size, sizeof(metadata_store.item_composite_id)); char st[33]; @@ -817,6 +817,7 @@ void *dacp_monitor_thread_code(__attribute__((unused)) void *na) { *pt = 0; debug(2, "Item composite ID changed to 0x%s.", st); metadata_store.item_composite_id_changed = 1; + metadata_store.item_composite_id_is_valid = 1; } break; case 'astm': @@ -826,6 +827,7 @@ void *dacp_monitor_thread_code(__attribute__((unused)) void *na) { if (ui != metadata_store.songtime_in_milliseconds) { metadata_store.songtime_in_milliseconds = ui; metadata_store.songtime_in_milliseconds_changed = 1; + metadata_store.songtime_in_milliseconds_is_valid = 1; debug(2, "DACP Song Time set to: \"%u\"", metadata_store.songtime_in_milliseconds); } diff --git a/dbus-service.c b/dbus-service.c index b8cfea16..51c1b128 100644 --- a/dbus-service.c +++ b/dbus-service.c @@ -96,6 +96,18 @@ void dbus_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)) } } + if (argc->stream_type) { + // debug(1, "Check stream type"); + th = shairport_sync_remote_control_get_stream_type(shairportSyncRemoteControlSkeleton); + if ((th == NULL) || (strcasecmp(th, argc->stream_type) != 0)) { + // debug(1, "Stream type string should be changed"); + shairport_sync_remote_control_set_stream_type(shairportSyncRemoteControlSkeleton, + argc->stream_type); + } + } + + + switch (argc->player_state) { case PS_NOT_AVAILABLE: shairport_sync_remote_control_set_player_state(shairportSyncRemoteControlSkeleton, @@ -202,8 +214,8 @@ void dbus_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)) g_variant_builder_add(dict_builder, "{sv}", "mpris:artUrl", artUrl); } - // Add in the Track ID based on the 'mper' metadata if it is non-zero - if (argc->item_id != 0) { + // Add in the Track ID based on the 'mper' metadata if it is valid + if (argc->item_id_is_valid != 0) { char trackidstring[128]; snprintf(trackidstring, sizeof(trackidstring), "/org/gnome/ShairportSync/%" PRIX64 "", argc->item_id); @@ -211,6 +223,14 @@ void dbus_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)) g_variant_builder_add(dict_builder, "{sv}", "mpris:trackid", trackid); } + // Add in the Song Data Kind based on the 'asdk' metadata if it is valid + // It seems that this is 0 for a timed play, e.g. a track or an album, but is 1 for an untimed play, such as a stream. + + if (argc->song_data_kind_is_valid != 0) { + GVariant *songdatakind = g_variant_new_uint32(argc->song_data_kind); + g_variant_builder_add(dict_builder, "{sv}", "sps:songdatakind", songdatakind); + } + // Add the track name if it exists if (argc->track_name) { GVariant *track_name = g_variant_new("s", argc->track_name); @@ -241,11 +261,11 @@ void dbus_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)) g_variant_builder_add(dict_builder, "{sv}", "xesam:genre", genre); } - if (argc->songtime_in_milliseconds) { + if (argc->songtime_in_milliseconds_is_valid != 0) { uint64_t track_length_in_microseconds = argc->songtime_in_milliseconds; track_length_in_microseconds *= 1000; // to microseconds in 64-bit precision // Make up the track name and album name - // debug(1, "Set tracklength to %lu.", track_length_in_microseconds); + // debug(1, "Set tracklength to %" PRId64 ".", track_length_in_microseconds); GVariant *tracklength = g_variant_new("x", track_length_in_microseconds); g_variant_builder_add(dict_builder, "{sv}", "mpris:length", tracklength); } diff --git a/metadata_hub.c b/metadata_hub.c index 9ad7052d..29190f90 100644 --- a/metadata_hub.c +++ b/metadata_hub.c @@ -110,6 +110,7 @@ void run_metadata_watchers(void) { metadata_store.artist_name_changed = 0; metadata_store.album_artist_name_changed = 0; metadata_store.album_name_changed = 0; + metadata_store.song_data_kind_changed = 0; metadata_store.track_name_changed = 0; metadata_store.genre_changed = 0; metadata_store.comment_changed = 0; @@ -348,6 +349,19 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin char *cs; if (type == 'core') { switch (code) { + case 'asdk': { + // get the one-byte number as an unsigned number + int song_data_kind = data[0]; // one byte + song_data_kind = song_data_kind & 0xFF; // unsigned + debug(2, "MH Song Data Kind seen: \"%d\" of length %u.", song_data_kind, length); + if ((song_data_kind != metadata_store.song_data_kind) || (metadata_store.song_data_kind_is_valid == 0)) { + metadata_store.song_data_kind = song_data_kind; + metadata_store.song_data_kind_changed = 1; + metadata_store.song_data_kind_is_valid = 1; + debug(2, "MH Song Data Kind set to: \"%d\"", metadata_store.song_data_kind); + metadata_packet_item_changed = 1; + } + } break; case 'mper': { // get the 64-bit number as a uint64_t by reading two uint32_t s and combining them uint64_t vl = ntohl(*(uint32_t *)data); // get the high order 32 bits @@ -355,10 +369,10 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin uint64_t ul = ntohl(*(uint32_t *)(data + sizeof(uint32_t))); // and the low order 32 bits vl = vl + ul; debug(2, "MH Item ID seen: \"%" PRIx64 "\" of length %u.", vl, length); - if (vl != metadata_store.item_id) { + if ((vl != metadata_store.item_id) || (metadata_store.item_id_is_valid == 0)) { metadata_store.item_id = vl; metadata_store.item_id_changed = 1; - metadata_store.item_id_received = 1; + metadata_store.item_id_is_valid = 1; debug(2, "MH Item ID set to: \"%" PRIx64 "\"", metadata_store.item_id); metadata_packet_item_changed = 1; } @@ -366,9 +380,10 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin case 'astm': { uint32_t ui = ntohl(*(uint32_t *)data); debug(2, "MH Song Time seen: \"%u\" of length %u.", ui, length); - if (ui != metadata_store.songtime_in_milliseconds) { + if ((ui != metadata_store.songtime_in_milliseconds) || (metadata_store.songtime_in_milliseconds_is_valid == 0)) { metadata_store.songtime_in_milliseconds = ui; metadata_store.songtime_in_milliseconds_changed = 1; + metadata_store.songtime_in_milliseconds_is_valid = 1; debug(2, "MH Song Time set to: \"%u\"", metadata_store.songtime_in_milliseconds); metadata_packet_item_changed = 1; } @@ -554,7 +569,7 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin cs = strndup(data, length); if (string_update(&metadata_store.client_name, &metadata_store.client_name_changed, cs)) { changed = 1; - debug(1, "MH Client Name set to: \"%s\"", metadata_store.client_name); + debug(2, "MH Client Name set to: \"%s\"", metadata_store.client_name); } free(cs); break; @@ -567,6 +582,15 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin } free(cs); break; + case 'styp': + cs = strndup(data, length); + if (string_update(&metadata_store.stream_type, &metadata_store.stream_type_changed, + cs)) { + changed = 1; + debug(2, "MH Stream Type set to: \"%s\"", metadata_store.stream_type); + } + free(cs); + break; case 'svip': cs = strndup(data, length); if (string_update(&metadata_store.server_ip, &metadata_store.server_ip_changed, cs)) { diff --git a/metadata_hub.h b/metadata_hub.h index bb57a8d5..730e6e8c 100644 --- a/metadata_hub.h +++ b/metadata_hub.h @@ -48,6 +48,9 @@ typedef struct metadata_bundle { char *server_ip; // IP number used by Shairport Sync int server_ip_changed; + + char *stream_type; // Realtime or Buffered + int stream_type_changed; char *progress_string; // progress string, emitted by the source from time to time int progress_string_changed; @@ -70,15 +73,21 @@ typedef struct metadata_bundle { char *cover_art_pathname; int cover_art_pathname_changed; - + uint64_t item_id; // seems to be a track ID -- see itemid in DACP.c int item_id_changed; - int item_id_received; // important for deciding if the track information should be ignored. + int item_id_is_valid; unsigned char item_composite_id[16]; // seems to be nowplaying 4 ids: dbid, plid, playlistItem, itemid int item_composite_id_changed; + int item_composite_id_is_valid; + + int song_data_kind; + int song_data_kind_changed; + int song_data_kind_is_valid; + char *track_name; int track_name_changed; @@ -123,6 +132,8 @@ typedef struct metadata_bundle { uint32_t songtime_in_milliseconds; int songtime_in_milliseconds_changed; + int songtime_in_milliseconds_is_valid; + // end diff --git a/mpris-service.c b/mpris-service.c index 8416df95..b31d179d 100644 --- a/mpris-service.c +++ b/mpris-service.c @@ -174,7 +174,7 @@ void mpris_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused) } // Add in the Track ID based on the 'mper' metadata if it is non-zero - if (argc->item_id != 0) { + if (argc->item_id_is_valid != 0) { char trackidstring[128]; snprintf(trackidstring, sizeof(trackidstring), "/org/gnome/ShairportSync/%" PRIX64 "", argc->item_id); @@ -212,7 +212,7 @@ void mpris_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused) g_variant_builder_add(dict_builder, "{sv}", "xesam:genre", genre); } - if (argc->songtime_in_milliseconds) { + if (argc->songtime_in_milliseconds_is_valid) { uint64_t track_length_in_microseconds = argc->songtime_in_milliseconds; track_length_in_microseconds *= 1000; // to microseconds in 64-bit precision // Make up the track name and album name diff --git a/mqtt.c b/mqtt.c index b12eb97d..cb426bdf 100644 --- a/mqtt.c +++ b/mqtt.c @@ -178,7 +178,10 @@ void mqtt_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t le case 'asal': mqtt_publish("songalbum", data, length); break; - case 'clip': + case 'asdk': + mqtt_publish("songdatakind", data, length); // 0 seem to be a timed item, 1 an untimed stream + break; + case 'clip': mqtt_publish("client_ip", data, length); break; case 'cdid': @@ -193,6 +196,12 @@ void mqtt_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t le case 'daid': mqtt_publish("dacp_id", data, length); break; + case 'ofmt': + mqtt_publish("output_format", data, length); + break; + case 'ofps': + mqtt_publish("output_frame_rate", data, length); + break; case 'pbeg': mqtt_publish("play_start", data, length); break; @@ -216,6 +225,9 @@ void mqtt_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t le case 'snam': mqtt_publish("client_name", data, length); break; + case 'styp': + mqtt_publish("stream_type", data, length); + break; case 'svip': mqtt_publish("server_ip", data, length); break; diff --git a/org.gnome.ShairportSync.xml b/org.gnome.ShairportSync.xml index c9f5868f..47aa3537 100644 --- a/org.gnome.ShairportSync.xml +++ b/org.gnome.ShairportSync.xml @@ -51,6 +51,7 @@ + diff --git a/player.c b/player.c index 5aad9153..c6432eec 100644 --- a/player.c +++ b/player.c @@ -363,20 +363,32 @@ static void terminate_decoders(rtsp_conn_info *conn) { #endif } +uint64_t buffers_allocated = 0; +uint64_t buffers_released = 0; static void init_buffer(rtsp_conn_info *conn) { // debug(1,"input_bytes_per_frame: %d.", conn->input_bytes_per_frame); // debug(1,"input_bit_depth: %d.", conn->input_bit_depth); int i; - for (i = 0; i < BUFFER_FRAMES; i++) + for (i = 0; i < BUFFER_FRAMES; i++) { // conn->audio_buffer[i].data = malloc(conn->input_bytes_per_frame * // conn->max_frames_per_packet); - conn->audio_buffer[i].data = malloc(8 * conn->max_frames_per_packet); // todo + void *allocation = malloc(8 * conn->max_frames_per_packet); + if (allocation == NULL) { + die("could not allocate memory for audio buffers. %" PRId64 " buffers allocated, %" PRId64 " buffers released.", buffers_allocated, buffers_released); + } else { + conn->audio_buffer[i].data = allocation; + buffers_allocated++; + } + } + debug(1, "%" PRId64 " buffers allocated, %" PRId64 " buffers released.", buffers_allocated, buffers_released); } static void free_audio_buffers(rtsp_conn_info *conn) { int i; - for (i = 0; i < BUFFER_FRAMES; i++) + for (i = 0; i < BUFFER_FRAMES; i++) { free(conn->audio_buffer[i].data); + buffers_released++; + } } int first_possibly_missing_frame = -1; @@ -2725,23 +2737,31 @@ void *player_thread_func(void *arg) { sync_error = 0; // say the error was fixed! } // since this is the first frame of audio, inform the user if requested... - if (config.statistics_requested) { + #ifdef CONFIG_AIRPLAY_2 - if (conn->airplay_stream_type == realtime_stream) { - if (conn->airplay_type == ap_1) - inform("Connection %d: Playback Started -- AirPlay 1 Compatible.", - conn->connection_number); - else - inform("Connection %d: Playback Started -- AirPlay 2 Realtime.", - conn->connection_number); + if (conn->airplay_stream_type == realtime_stream) { + if (conn->airplay_type == ap_1) { + send_ssnc_metadata('styp', "Classic", strlen("Classic"), 1); + if (config.statistics_requested) + inform("Connection %d: Playback Started at frame %" PRId64 " -- AirPlay 1 Compatible.", + conn->connection_number, inframe->given_timestamp); } else { - inform("Connection %d: Playback Started -- AirPlay 2 Buffered.", - conn->connection_number); + send_ssnc_metadata('styp', "Realtime", strlen("Realtime"), 1); + if (config.statistics_requested) + inform("Connection %d: Playback Started at frame %" PRId64 " -- AirPlay 2 Realtime.", + conn->connection_number, inframe->given_timestamp); } -#else - inform("Connection %d: Playback Started -- AirPlay 1.", conn->connection_number); -#endif + } else { + send_ssnc_metadata('styp', "Buffered", strlen("Buffered"), 1); + if (config.statistics_requested) + inform("Connection %d: Playback Started at frame %" PRId64 " -- AirPlay 2 Buffered.", + conn->connection_number, inframe->given_timestamp); } +#else + send_ssnc_metadata('styp', "Classic", strlen("Classic"), 1); + if (config.statistics_requested) + inform("Connection %d: Playback Started at frame %" PRId64 " -- AirPlay 1.", conn->connection_number, inframe->given_timestamp); +#endif } // not too sure if abs() is implemented for int64_t, so we'll do it manually int64_t abs_sync_error = sync_error; diff --git a/shairport.c b/shairport.c index 86c01e83..7083b99f 100644 --- a/shairport.c +++ b/shairport.c @@ -2539,6 +2539,13 @@ int main(int argc, char **argv) { #ifdef CONFIG_METADATA send_ssnc_metadata('svna', config.service_name, strlen(config.service_name), 1); + char buffer[256] = ""; + snprintf(buffer, sizeof(buffer), "%d", + config.output_rate); + send_ssnc_metadata('ofps', buffer, strlen(buffer), 1); + snprintf(buffer, sizeof(buffer), "%s", + sps_format_description_string(config.output_format)); + send_ssnc_metadata('ofmt', buffer, strlen(buffer), 1); #endif activity_monitor_start(); // not yet for AP2 From 61865bf36fbbfebecc65bd917db9607631aacc25 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 12 Nov 2022 16:57:12 +0000 Subject: [PATCH 14/51] Fix compilation bug when metadata isn't included in the build. --- new-rtsp.c | 5564 ++++++++++++++++++++++++++++++++++++++++++++++++++++ player.c | 8 + 2 files changed, 5572 insertions(+) create mode 100644 new-rtsp.c diff --git a/new-rtsp.c b/new-rtsp.c new file mode 100644 index 00000000..8d11fbc7 --- /dev/null +++ b/new-rtsp.c @@ -0,0 +1,5564 @@ +/* + * RTSP protocol handler. This file is part of Shairport Sync + * Copyright (c) James Laird 2013 + + * Modifications associated with audio synchronization, multithreading and + * metadata handling copyright (c) Mike Brady 2014-2022 + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "activity_monitor.h" +#include "config.h" + +#ifdef CONFIG_OPENSSL +#include +#endif + +#ifdef CONFIG_MBEDTLS +#include +#include +#endif + +#ifdef CONFIG_POLARSSL +#include +#endif + +#include "common.h" +#include "player.h" +#include "rtp.h" +#include "rtsp.h" + +#ifdef CONFIG_METADATA_HUB +#include "metadata_hub.h" +#endif + +#ifdef CONFIG_MQTT +#include "mqtt.h" +#endif + +#ifdef AF_INET6 +#define INETx_ADDRSTRLEN INET6_ADDRSTRLEN +#else +#define INETx_ADDRSTRLEN INET_ADDRSTRLEN +#endif + +#ifdef CONFIG_AIRPLAY_2 +#include "pair_ap/pair.h" +#include "plist/plist.h" +#include "plist_xml_strings.h" +#include "ptp-utilities.h" +#endif + +#ifdef CONFIG_DBUS_INTERFACE +#include "dbus-service.h" +#endif + +#include "mdns.h" + +// mDNS advertisement strings + +// Create these strings and then keep them updated. +// When necessary, update the mDNS service records, using e.g. Avahi +// from these sources. + +char *txt_records[64]; +char *secondary_txt_records[64]; + +char firmware_version[64]; +char ap1_featuresString[64]; +char pkString[128]; +#ifdef CONFIG_AIRPLAY_2 +char deviceIdString[64]; +char featuresString[64]; +char statusflagsString[32]; +char piString[128]; +char gidString[128]; +#endif + +#define METADATA_SNDBUF (4 * 1024 * 1024) + +enum rtsp_read_request_response { + rtsp_read_request_response_ok, + rtsp_read_request_response_immediate_shutdown_requested, + rtsp_read_request_response_bad_packet, + rtsp_read_request_response_channel_closed, + rtsp_read_request_response_read_error, + rtsp_read_request_response_error +}; + +rtsp_conn_info *playing_conn; +rtsp_conn_info **conns; + +int metadata_running = 0; + +// always lock this when accessing the playing conn value +pthread_mutex_t playing_conn_lock = PTHREAD_MUTEX_INITIALIZER; + +// always lock this when accessing the list of connection threads +pthread_mutex_t conns_lock = PTHREAD_MUTEX_INITIALIZER; + +// every time we want to retain or release a reference count, lock it with this +// if a reference count is read as zero, it means the it's being deallocated. +static pthread_mutex_t reference_counter_lock = PTHREAD_MUTEX_INITIALIZER; + +// only one thread is allowed to use the player at once. +// it monitors the request variable (at least when interrupted) +// static pthread_mutex_t playing_mutex = PTHREAD_MUTEX_INITIALIZER; +// static int please_shutdown = 0; +// static pthread_t playing_thread = 0; + +int RTSP_connection_index = 1; + +#ifdef CONFIG_METADATA +typedef struct { + pthread_mutex_t pc_queue_lock; + pthread_cond_t pc_queue_item_added_signal; + pthread_cond_t pc_queue_item_removed_signal; + char *name; + size_t item_size; // number of bytes in each item + uint32_t count; // number of items in the queue + uint32_t capacity; // maximum number of items + uint32_t toq; // first item to take + uint32_t eoq; // free space at end of queue + void *items; // a pointer to where the items are actually stored +} pc_queue; // producer-consumer queue +#endif + +static int msg_indexes = 1; + +typedef struct { + int index_number; + uint32_t referenceCount; // we might start using this... + unsigned int nheaders; + char *name[16]; + char *value[16]; + + uint32_t contentlength; + char *content; + + // for requests + char method[16]; + char path[256]; + + // for responses + int respcode; +} rtsp_message; + +#ifdef CONFIG_AIRPLAY_2 + +int add_pstring_to_malloc(const char *s, void **allocation, size_t *size) { + int response = 0; + void *p = *allocation; + if (p == NULL) { + p = malloc(strlen(s) + 1); + if (p == NULL) { + debug(1, "error allocating memory"); + } else { + *allocation = p; + *size = *size + strlen(s) + 1; + uint8_t *b = (uint8_t *)p; + *b = strlen(s); + p = p + 1; + memcpy(p, s, strlen(s)); + response = 1; + } + } else { + p = realloc(p, *size + strlen(s) + 1); + if (p == NULL) { // assuming we never allocate a zero byte space + debug(1, "error reallocating memory"); + } else { + *allocation = p; + uint8_t *b = (uint8_t *)p + *size; + *b = strlen(s); + p = p + *size + 1; + memcpy(p, s, strlen(s)); + *size = *size + strlen(s) + 1; + response = 1; + } + } + return response; +} + +static void pkString_make(char *str, size_t str_size, const char *device_id) { + uint8_t public_key[32]; + if (str_size < 2 * sizeof(public_key) + 1) { + warn("Insufficient string size"); + str[0] = '\0'; + return; + } + pair_public_key_get(PAIR_SERVER_HOMEKIT, public_key, device_id); + char *ptr = str; + for (size_t i = 0; i < sizeof(public_key); i++) + ptr += sprintf(ptr, "%02x", public_key[i]); +} +#endif + +#ifdef CONFIG_AIRPLAY_2 +void build_bonjour_strings(rtsp_conn_info *conn) { +#else +void build_bonjour_strings(__attribute((unused)) rtsp_conn_info *conn) { +#endif + + int entry_number = 0; + + // make up a firmware version +#ifdef CONFIG_USE_GIT_VERSION_STRING + if (git_version_string[0] != '\0') + snprintf(firmware_version, sizeof(firmware_version), "fv=%s", git_version_string); + else +#endif + snprintf(firmware_version, sizeof(firmware_version), "fv=%s", PACKAGE_VERSION); + +#ifdef CONFIG_AIRPLAY_2 + uint64_t features_hi = config.airplay_features; + features_hi = (features_hi >> 32) & 0xffffffff; + uint64_t features_lo = config.airplay_features; + features_lo = features_lo & 0xffffffff; + snprintf(ap1_featuresString, sizeof(ap1_featuresString), "ft=0x%" PRIX64 ",0x%" PRIX64 "", + features_lo, features_hi); + snprintf(pkString, sizeof(pkString), "pk="); + pkString_make(pkString + strlen("pk="), sizeof(pkString) - strlen("pk="), + config.airplay_device_id); + + txt_records[entry_number++] = "cn=0,1"; + txt_records[entry_number++] = "da=true"; + txt_records[entry_number++] = "et=0,4"; + txt_records[entry_number++] = ap1_featuresString; + txt_records[entry_number++] = firmware_version; + txt_records[entry_number++] = "md=2"; + txt_records[entry_number++] = "am=Shairport Sync"; + txt_records[entry_number++] = "sf=0x4"; + txt_records[entry_number++] = "tp=UDP"; + txt_records[entry_number++] = "vn=65537"; + txt_records[entry_number++] = "vs=366.0"; + txt_records[entry_number++] = pkString; + txt_records[entry_number++] = NULL; + +#else + // here, just replicate what happens in mdns.h when using those #defines + txt_records[entry_number++] = "sf=0x4"; + txt_records[entry_number++] = firmware_version; + txt_records[entry_number++] = "am=ShairportSync"; + txt_records[entry_number++] = "vs=105.1"; + txt_records[entry_number++] = "tp=TCP,UDP"; + txt_records[entry_number++] = "vn=65537"; +#ifdef CONFIG_METADATA + if (config.get_coverart == 0) + txt_records[entry_number++] = "md=0,2"; + else + txt_records[entry_number++] = "md=0,1,2"; +#endif + txt_records[entry_number++] = "ss=16"; + txt_records[entry_number++] = "sr=44100"; + txt_records[entry_number++] = "da=true"; + txt_records[entry_number++] = "sv=false"; + txt_records[entry_number++] = "et=0,1"; + txt_records[entry_number++] = "ek=1"; + txt_records[entry_number++] = "cn=0,1"; + txt_records[entry_number++] = "ch=2"; + txt_records[entry_number++] = "txtvers=1"; + if (config.password == 0) + txt_records[entry_number++] = "pw=false"; + else + txt_records[entry_number++] = "pw=true"; + txt_records[entry_number++] = NULL; +#endif + +#ifdef CONFIG_AIRPLAY_2 + // make up a secondary set of text records + entry_number = 0; + + secondary_txt_records[entry_number++] = "srcvers=366.0"; + snprintf(deviceIdString, sizeof(deviceIdString), "deviceid=%s", config.airplay_device_id); + secondary_txt_records[entry_number++] = deviceIdString; + snprintf(featuresString, sizeof(featuresString), "features=0x%" PRIX64 ",0x%" PRIX64 "", + features_lo, features_hi); + secondary_txt_records[entry_number++] = featuresString; + snprintf(statusflagsString, sizeof(statusflagsString), "flags=0x%" PRIX32, + config.airplay_statusflags); + + secondary_txt_records[entry_number++] = statusflagsString; + secondary_txt_records[entry_number++] = "protovers=1.1"; + secondary_txt_records[entry_number++] = "acl=0"; + secondary_txt_records[entry_number++] = "rsf=0x0"; + secondary_txt_records[entry_number++] = firmware_version; + secondary_txt_records[entry_number++] = "model=Shairport Sync"; + snprintf(piString, sizeof(piString), "pi=%s", config.airplay_pi); + secondary_txt_records[entry_number++] = piString; + if ((conn != NULL) && (conn->airplay_gid != 0)) { + snprintf(gidString, sizeof(gidString), "gid=%s", conn->airplay_gid); + } else { + snprintf(gidString, sizeof(gidString), "gid=%s", config.airplay_pi); + } + secondary_txt_records[entry_number++] = gidString; + if ((conn != NULL) && (conn->groupContainsGroupLeader != 0)) + secondary_txt_records[entry_number++] = "gcgl=1"; + else + secondary_txt_records[entry_number++] = "gcgl=0"; + if ((conn != NULL) && (conn->airplay_gid != 0)) // if it's in a group + secondary_txt_records[entry_number++] = "isGroupLeader=0"; + secondary_txt_records[entry_number++] = pkString; + secondary_txt_records[entry_number++] = NULL; +#endif +} + +#ifdef CONFIG_METADATA +typedef struct { + uint32_t type; + uint32_t code; + char *data; + uint32_t length; + rtsp_message *carrier; +} metadata_package; + +void pc_queue_init(pc_queue *the_queue, char *items, size_t item_size, uint32_t number_of_items, + const char *name) { + if (name) + debug(2, "Creating metadata queue \"%s\".", name); + else + debug(1, "Creating an unnamed metadata queue."); + pthread_mutex_init(&the_queue->pc_queue_lock, NULL); + pthread_cond_init(&the_queue->pc_queue_item_added_signal, NULL); + pthread_cond_init(&the_queue->pc_queue_item_removed_signal, NULL); + the_queue->item_size = item_size; + the_queue->items = items; + the_queue->count = 0; + the_queue->capacity = number_of_items; + the_queue->toq = 0; + the_queue->eoq = 0; + if (name == NULL) + the_queue->name = NULL; + else + the_queue->name = strdup(name); +} + +void pc_queue_delete(pc_queue *the_queue) { + if (the_queue->name) + debug(2, "Deleting metadata queue \"%s\".", the_queue->name); + else + debug(1, "Deleting an unnamed metadata queue."); + if (the_queue->name != NULL) + free(the_queue->name); + // debug(2, "destroying pc_queue_item_removed_signal"); + pthread_cond_destroy(&the_queue->pc_queue_item_removed_signal); + // debug(2, "destroying pc_queue_item_added_signal"); + pthread_cond_destroy(&the_queue->pc_queue_item_added_signal); + // debug(2, "destroying pc_queue_lock"); + pthread_mutex_destroy(&the_queue->pc_queue_lock); + // debug(2, "destroying signals and locks done"); +} + +int send_metadata(uint32_t type, uint32_t code, char *data, uint32_t length, rtsp_message *carrier, + int block); + +int send_ssnc_metadata(uint32_t code, char *data, uint32_t length, int block) { + return send_metadata('ssnc', code, data, length, NULL, block); +} + +void pc_queue_cleanup_handler(void *arg) { + // debug(1, "pc_queue_cleanup_handler called."); + pc_queue *the_queue = (pc_queue *)arg; + int rc = pthread_mutex_unlock(&the_queue->pc_queue_lock); + if (rc) + debug(1, "Error unlocking for pc_queue_add_item or pc_queue_get_item."); +} + +int pc_queue_add_item(pc_queue *the_queue, const void *the_stuff, int block) { + int response = 0; + int rc; + if (the_queue) { + if (block == 0) { + rc = debug_mutex_lock(&the_queue->pc_queue_lock, 10000, 2); + if (rc == EBUSY) + return EBUSY; + } else + rc = pthread_mutex_lock(&the_queue->pc_queue_lock); + if (rc) + debug(1, "Error locking for pc_queue_add_item"); + pthread_cleanup_push(pc_queue_cleanup_handler, (void *)the_queue); + // leave this out if you want this to return if the queue is already full + // irrespective of the block flag. + /* + while (the_queue->count == the_queue->capacity) { + rc = pthread_cond_wait(&the_queue->pc_queue_item_removed_signal, + &the_queue->pc_queue_lock); if (rc) debug(1, "Error waiting for item to be removed"); + } + */ + if (the_queue->count < the_queue->capacity) { + uint32_t i = the_queue->eoq; + void *p = the_queue->items + the_queue->item_size * i; + // void * p = &the_queue->qbase + the_queue->item_size*the_queue->eoq; + memcpy(p, the_stuff, the_queue->item_size); + + // update the pointer + i++; + if (i == the_queue->capacity) + // fold pointer if necessary + i = 0; + the_queue->eoq = i; + the_queue->count++; + // debug(2,"metadata queue+ \"%s\" %d/%d.", the_queue->name, the_queue->count, + // the_queue->capacity); + if (the_queue->count == the_queue->capacity) + debug(3, "metadata queue \"%s\": is now full with %d items in it!", the_queue->name, + the_queue->count); + rc = pthread_cond_signal(&the_queue->pc_queue_item_added_signal); + if (rc) + debug(1, "metadata queue \"%s\": error signalling after pc_queue_add_item", + the_queue->name); + } else { + response = EWOULDBLOCK; // a bit arbitrary, this. + debug(3, + "metadata queue \"%s\": is already full with %d items in it. Not adding this item to " + "the queue.", + the_queue->name, the_queue->count); + } + pthread_cleanup_pop(1); // unlock the queue lock. + } else { + debug(1, "Adding an item to a NULL queue"); + } + return response; +} + +int pc_queue_get_item(pc_queue *the_queue, void *the_stuff) { + int rc; + if (the_queue) { + rc = pthread_mutex_lock(&the_queue->pc_queue_lock); + if (rc) + debug(1, "metadata queue \"%s\": error locking for pc_queue_get_item", the_queue->name); + pthread_cleanup_push(pc_queue_cleanup_handler, (void *)the_queue); + while (the_queue->count == 0) { + rc = pthread_cond_wait(&the_queue->pc_queue_item_added_signal, &the_queue->pc_queue_lock); + if (rc) + debug(1, "metadata queue \"%s\": error waiting for item to be added", the_queue->name); + } + uint32_t i = the_queue->toq; + // void * p = &the_queue->qbase + the_queue->item_size*the_queue->toq; + void *p = the_queue->items + the_queue->item_size * i; + memcpy(the_stuff, p, the_queue->item_size); + + // update the pointer + i++; + if (i == the_queue->capacity) + // fold pointer if necessary + i = 0; + the_queue->toq = i; + the_queue->count--; + debug(3, "metadata queue- \"%s\" %d/%d.", the_queue->name, the_queue->count, + the_queue->capacity); + rc = pthread_cond_signal(&the_queue->pc_queue_item_removed_signal); + if (rc) + debug(1, "metadata queue \"%s\": error signalling after pc_queue_get_item", the_queue->name); + pthread_cleanup_pop(1); // unlock the queue lock. + } else { + debug(1, "Removing an item from a NULL queue"); + } + return 0; +} + +#endif + +void lock_player() { debug_mutex_lock(&playing_conn_lock, 1000000, 3); } + +void unlock_player() { debug_mutex_unlock(&playing_conn_lock, 3); } + +int have_play_lock(rtsp_conn_info *conn) { + int response = 0; + lock_player(); + if (playing_conn == conn) // this connection definitely has the play lock + response = 1; + unlock_player(); + return response; +} + +/* +// return 0 if the playing_conn isn't already locked by someone else and if +// it belongs to the conn passed in. +// remember to release it! +int try_to_get_and_lock_playing_conn(rtsp_conn_info **conn) { + int response = -1; + if (pthread_mutex_trylock(&playing_conn_lock) == 0) { + response = 0; + *conn = playing_conn; + } + return response; +} + + +void release_hold_on_play_lock(__attribute__((unused)) rtsp_conn_info *conn) { + pthread_mutex_unlock(&playing_conn_lock); +} + +*/ + +// make conn no longer the playing_conn +void release_play_lock(rtsp_conn_info *conn) { + if (conn != NULL) + debug(2, "Connection %d: release play lock.", conn->connection_number); + else + debug(2, "Release play lock."); + lock_player(); + if (playing_conn == conn) { // if we have the player + if (conn != NULL) + debug(2, "Connection %d: play lock released.", conn->connection_number); + else + debug(2, "Play lock released."); + playing_conn = NULL; // let it go + } + unlock_player(); +} + +// make conn the playing_conn, and kill the current session if permitted +int get_play_lock(rtsp_conn_info *conn, int allow_session_interruption) { + if (conn != NULL) + debug(2, "Connection %d: request play lock.", conn->connection_number); + else if (playing_conn != NULL) + debug(2, "Connection %d: request release.", playing_conn->connection_number); + else + debug(2, "Request release of non-existent player."); + // returns -1 if it failed, 0 if it succeeded and 1 if it succeeded but + // interrupted an existing session + int response = 0; + + int have_the_player = 0; + int should_wait = 0; // this will be true if you're trying to break in to the current session + int interrupting_current_session = 0; + + // try to become the current playing_conn + + lock_player(); // don't let it change while we are looking at it... + + if (playing_conn == NULL) { + playing_conn = conn; + have_the_player = 1; + } else if (playing_conn == conn) { + have_the_player = 1; + if (conn != NULL) + warn("Duplicate attempt to acquire the player by the same connection, by the look of it!"); + } else if (playing_conn->stop) { + debug(1, "Connection %d: Waiting for Connection %d to stop playing.", conn->connection_number, + playing_conn->connection_number); + should_wait = 1; + } else if (allow_session_interruption != 0) { + debug(2, "Asking Connection %d to stop playing.", playing_conn->connection_number); + playing_conn->stop = 1; + interrupting_current_session = 1; + should_wait = 1; + pthread_cancel(playing_conn->thread); // asking the RTSP thread to exit + } + unlock_player(); + + if (should_wait) { + int time_remaining = 3000000; // must be signed, as it could go negative... + + while ((time_remaining > 0) && (have_the_player == 0)) { + lock_player(); // get it + if (playing_conn == NULL) { + playing_conn = conn; + have_the_player = 1; + } + unlock_player(); + + if (have_the_player == 0) { + usleep(100000); + time_remaining -= 100000; + } + } + if ((have_the_player == 1) && (interrupting_current_session == 1)) { + if (conn != NULL) + debug(2, "Connection %d: Got player lock", conn->connection_number); + else + debug(2, "Player released."); + response = 1; + } else { + debug(2, "Connection %d: failed to get player lock after waiting.", conn->connection_number); + response = -1; + } + } + + if ((have_the_player == 1) && (interrupting_current_session == 0)) { + if (conn != NULL) + debug(2, "Connection %d: Got player lock.", conn->connection_number); + else + debug(2, "Player released."); + response = 0; + } + return response; +} + +void player_watchdog_thread_cleanup_handler(void *arg) { + rtsp_conn_info *conn = (rtsp_conn_info *)arg; + debug(3, "Connection %d: Watchdog Exit.", conn->connection_number); +} + +void *player_watchdog_thread_code(void *arg) { + pthread_cleanup_push(player_watchdog_thread_cleanup_handler, arg); + rtsp_conn_info *conn = (rtsp_conn_info *)arg; + do { + usleep(2000000); // check every two seconds + // debug(3, "Connection %d: Check the thread is doing something...", conn->connection_number); +#ifdef CONFIG_AIRPLAY_2 + if ((config.dont_check_timeout == 0) && (config.timeout != 0) && (conn->airplay_type == ap_1)) { +#else + if ((config.dont_check_timeout == 0) && (config.timeout != 0)) { +#endif + debug_mutex_lock(&conn->watchdog_mutex, 1000, 0); + uint64_t last_watchdog_bark_time = conn->watchdog_bark_time; + debug_mutex_unlock(&conn->watchdog_mutex, 0); + if (last_watchdog_bark_time != 0) { + uint64_t time_since_last_bark = + (get_absolute_time_in_ns() - last_watchdog_bark_time) / 1000000000; + uint64_t ct = config.timeout; // go from int to 64-bit int + + if (time_since_last_bark >= ct) { + conn->watchdog_barks++; + if (conn->watchdog_barks == 1) { + // debuglev = 3; // tell us everything. + 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) && (conn->unfixable_error_reported == 0)) { + conn->unfixable_error_reported = 1; + command_execute(config.cmd_unfixable, "unable_to_cancel_play_session", 1); + } else { + die("an unrecoverable error, \"unable_to_cancel_play_session\", has been detected.", + conn->connection_number); + } + } + } + } + } + } while (1); + pthread_cleanup_pop(0); // should never happen + pthread_exit(NULL); +} + +// keep track of the threads we have spawned so we can join() them +static int nconns = 0; +static void track_thread(rtsp_conn_info *conn) { + debug_mutex_lock(&conns_lock, 1000000, 3); + // look for an empty slot first + int i = 0; + int found = 0; + while ((i < nconns) && (found == 0)) { + if (conns[i] == NULL) + found = 1; + else + i++; + } + if (found != 0) { + conns[i] = conn; + } else { + // make space for a new element + conns = realloc(conns, sizeof(rtsp_conn_info *) * (nconns + 1)); + if (conns) { + conns[nconns] = conn; + nconns++; + } else { + die("could not reallocate memory for conns"); + } + } + debug_mutex_unlock(&conns_lock, 3); +} + +// note: connection numbers start at 1, so an except_this_one value of zero means "all threads" +void cancel_all_RTSP_threads(airplay_stream_c stream_category, int except_this_one) { + // if the stream category is unspecified_stream_category + // all categories are elegible for cancellation + // otherwise just the category itself + debug_mutex_lock(&conns_lock, 1000000, 3); + int i; + for (i = 0; i < nconns; i++) { + if ((conns[i] != NULL) && (conns[i]->running != 0) && + (conns[i]->connection_number != except_this_one) && + ((stream_category == unspecified_stream_category) || + (stream_category == conns[i]->airplay_stream_category))) { + pthread_cancel(conns[i]->thread); + debug(2, "Connection %d: cancelled.", conns[i]->connection_number); + } + } + for (i = 0; i < nconns; i++) { + if ((conns[i] != NULL) && (conns[i]->running != 0) && + (conns[i]->connection_number != except_this_one) && + ((stream_category == unspecified_stream_category) || + (stream_category == conns[i]->airplay_stream_category))) { + pthread_join(conns[i]->thread, NULL); + debug(2, "Connection %d: joined.", conns[i]->connection_number); + free(conns[i]); + conns[i] = NULL; + } + } + debug_mutex_unlock(&conns_lock, 3); +} + +void cleanup_threads(void) { + + void *retval; + int i; + // debug(2, "culling threads."); + debug_mutex_lock(&conns_lock, 1000000, 3); + for (i = 0; i < nconns; i++) { + if ((conns[i] != NULL) && (conns[i]->running == 0)) { + debug(3, "found RTSP connection thread %d in a non-running state.", + conns[i]->connection_number); + pthread_join(conns[i]->thread, &retval); + debug(2, "Connection %d: deleted in cleanup.", conns[i]->connection_number); + free(conns[i]); + conns[i] = NULL; + } + } + debug_mutex_unlock(&conns_lock, 3); +} + +// park a null at the line ending, and return the next line pointer +// accept \r, \n, or \r\n +static char *nextline(char *in, int inbuf) { + char *out = NULL; + while (inbuf) { + if (*in == '\r') { + *in++ = 0; + out = in; + inbuf--; + } + if ((*in == '\n') && (inbuf)) { + *in++ = 0; + out = in; + } + + if (out) + break; + + in++; + inbuf--; + } + return out; +} + +void msg_retain(rtsp_message *msg) { + int rc = pthread_mutex_lock(&reference_counter_lock); + if (rc) + debug(1, "Error %d locking reference counter lock"); + if (msg > (rtsp_message *)0x00010000) { + msg->referenceCount++; + debug(3, "msg_free increment reference counter message %d to %d.", msg->index_number, + msg->referenceCount); + // debug(1,"msg_retain -- item %d reference count %d.", msg->index_number, msg->referenceCount); + rc = pthread_mutex_unlock(&reference_counter_lock); + if (rc) + debug(1, "Error %d unlocking reference counter lock"); + } else { + debug(1, "invalid rtsp_message pointer 0x%x passed to retain", (uintptr_t)msg); + } +} + +rtsp_message *msg_init(void) { + rtsp_message *msg = malloc(sizeof(rtsp_message)); + if (msg) { + memset(msg, 0, sizeof(rtsp_message)); + msg->referenceCount = 1; // from now on, any access to this must be protected with the lock + msg->index_number = msg_indexes++; + debug(3, "msg_init message %d", msg->index_number); + } else { + die("msg_init -- can not allocate memory for rtsp_message %d.", msg_indexes); + } + // debug(1,"msg_init -- create item %d.", msg->index_number); + return msg; +} + +int msg_add_header(rtsp_message *msg, char *name, char *value) { + if (msg->nheaders >= sizeof(msg->name) / sizeof(char *)) { + warn("too many headers?!"); + return 1; + } + + msg->name[msg->nheaders] = strdup(name); + msg->value[msg->nheaders] = strdup(value); + msg->nheaders++; + + return 0; +} + +char *msg_get_header(rtsp_message *msg, char *name) { + unsigned int i; + for (i = 0; i < msg->nheaders; i++) + if (!strcasecmp(msg->name[i], name)) + return msg->value[i]; + return NULL; +} + +void _debug_print_msg_headers(const char *filename, const int linenumber, int level, + rtsp_message *msg) { + unsigned int i; + if (msg->respcode != 0) + _debug(filename, linenumber, level, " Response Code: %d.", msg->respcode); + for (i = 0; i < msg->nheaders; i++) { + _debug(filename, linenumber, level, " Type: \"%s\", content: \"%s\"", msg->name[i], + msg->value[i]); + } +} + +/* +static void debug_print_msg_content(int level, rtsp_message *msg) { + if (msg->contentlength) { + char *obf = malloc(msg->contentlength * 2 + 1); + if (obf) { + char *obfp = obf; + int obfc; + for (obfc = 0; obfc < msg->contentlength; obfc++) { + snprintf(obfp, 3, "%02X", msg->content[obfc]); + obfp += 2; + }; + *obfp = 0; + debug(level, "Content (hex): \"%s\"", obf); + free(obf); + } else { + debug(level, "Can't allocate space for debug buffer"); + } + } else { + debug(level, "No content"); + } +} +*/ + +void msg_free(rtsp_message **msgh) { + debug_mutex_lock(&reference_counter_lock, 1000, 0); + if (*msgh > (rtsp_message *)0x00010000) { + rtsp_message *msg = *msgh; + msg->referenceCount--; + if (msg->referenceCount) + debug(3, "msg_free decrement reference counter message %d to %d", msg->index_number, + msg->referenceCount); + if (msg->referenceCount == 0) { + unsigned int i; + for (i = 0; i < msg->nheaders; i++) { + free(msg->name[i]); + free(msg->value[i]); + } + if (msg->content) + free(msg->content); + // debug(1,"msg_free item %d -- free.",msg->index_number); + uintptr_t index = (msg->index_number) & 0xFFFF; + if (index == 0) + index = 0x10000; // ensure it doesn't fold to zero. + *msgh = + (rtsp_message *)(index); // put a version of the index number of the freed message in here + debug(3, "msg_free freed message %d", msg->index_number); + free(msg); + } else { + // debug(1,"msg_free item %d -- decrement reference to + // %d.",msg->index_number,msg->referenceCount); + } + } else if (*msgh != NULL) { + debug(1, + "msg_free: error attempting to free an allocated but already-freed rtsp_message, number " + "%d.", + (uintptr_t)*msgh); + } + debug_mutex_unlock(&reference_counter_lock, 0); +} + +int msg_handle_line(rtsp_message **pmsg, char *line) { + rtsp_message *msg = *pmsg; + + if (!msg) { + msg = msg_init(); + *pmsg = msg; + char *sp, *p; + sp = NULL; // this is to quieten a compiler warning + + debug(3, "RTSP Message Received: \"%s\".", line); + + p = strtok_r(line, " ", &sp); + if (!p) + goto fail; + strncpy(msg->method, p, sizeof(msg->method) - 1); + + p = strtok_r(NULL, " ", &sp); + if (!p) + goto fail; + strncpy(msg->path, p, sizeof(msg->path) - 1); + + p = strtok_r(NULL, " ", &sp); + if (!p) + goto fail; + if ((strcmp(p, "RTSP/1.0")) && (strcmp(p, "HTTP/1.1"))) { + debug(1,"can't find a RTSP or HTTP header in the following."); + + char obf[4096]; + char *obfp = obf; + int obfc; + for (obfc=0;obfc<20;obfc++) { + snprintf(obfp, 3, "%02X", line[obfc]); + obfp+=2; + }; + *obfp=0; + debug(1,"Funny Packet Received: \"%s\"",obf); + goto fail; + } + + return -1; + } + + if (strlen(line)) { + char *p; + p = strstr(line, ": "); + if (!p) { + warn("bad header: >>%s<<", line); + goto fail; + } + *p = 0; + p += 2; + msg_add_header(msg, line, p); + debug(3, " %s: %s.", line, p); + return -1; + } else { + char *cl = msg_get_header(msg, "Content-Length"); + if (cl) + return atoi(cl); + else + return 0; + } + +fail: + debug(3, "msg_handle_line fail"); + msg_free(pmsg); + *pmsg = NULL; + return 0; +} + +#ifdef CONFIG_AIRPLAY_2 + +void add_flush_request(int flushNow, uint32_t flushFromSeq, uint32_t flushFromTS, + uint32_t flushUntilSeq, uint32_t flushUntilTS, rtsp_conn_info *conn) { + // immediate flush requests are added sequentially. Don't know how more than one could arise, TBH + flush_request_t **t = &conn->flush_requests; + int done = 0; + do { + flush_request_t *u = *t; + if ((u == NULL) || ((u->flushNow == 0) && (flushNow != 0)) || + (flushFromSeq < u->flushFromSeq) || + ((flushFromSeq == u->flushFromSeq) && (flushFromTS < u->flushFromTS))) { + flush_request_t *n = (flush_request_t *)calloc(sizeof(flush_request_t), 1); + n->flushNow = flushNow; + n->flushFromSeq = flushFromSeq; + n->flushFromTS = flushFromTS; + n->flushUntilSeq = flushUntilSeq; + n->flushUntilTS = flushUntilTS; + n->next = u; + *t = n; + done = 1; + } else { + t = &u->next; + } + } while (done == 0); +} + +void display_all_flush_requests(rtsp_conn_info *conn) { + if (conn->flush_requests == NULL) { + debug(1, "No flush requests."); + } else { + flush_request_t *t = conn->flush_requests; + do { + if (t->flushNow) { + debug(1, "immediate flush to untilSeq: %u, untilTS: %u.", t->flushUntilSeq, + t->flushUntilTS); + } else { + debug(1, "fromSeq: %u, fromTS: %u, to untilSeq: %u, untilTS: %u.", t->flushFromSeq, + t->flushFromTS, t->flushUntilSeq, t->flushUntilTS); + } + t = t->next; + } while (t != NULL); + } +} + +int rtsp_message_contains_plist(rtsp_message *message) { + int reply = 0; // assume there is no plist in the message + if ((message->contentlength >= strlen("bplist00")) && + (strstr(message->content, "bplist00") == message->content)) + reply = 1; + return reply; +} + +plist_t plist_from_rtsp_content(rtsp_message *message) { + plist_t the_plist = NULL; + if (rtsp_message_contains_plist(message)) { + plist_from_memory(message->content, message->contentlength, &the_plist); + } + return the_plist; +} + +char *plist_content(plist_t the_plist) { + // caller must free the returned character buffer + // convert it to xml format + uint32_t size; + char *plist_out = NULL; + plist_to_xml(the_plist, &plist_out, &size); + + // put it into a NUL-terminated string + char *reply = malloc(size + 1); + if (reply) { + memcpy(reply, plist_out, size); + reply[size] = '\0'; + } + if (the_plist) + plist_free(the_plist); + if (plist_out) + free(plist_out); + return reply; +} + +// caller must free the returned character buffer +char *rtsp_plist_content(rtsp_message *message) { + char *reply = NULL; + // first, check if it has binary plist content + if (rtsp_message_contains_plist(message)) { + // get the plist from the content + + plist_t the_plist = plist_from_rtsp_content(message); + + // convert it to xml format + uint32_t size; + char *plist_out = NULL; + plist_to_xml(the_plist, &plist_out, &size); + + // put it into a NUL-terminated string + reply = malloc(size + 1); + if (reply) { + memcpy(reply, plist_out, size); + reply[size] = '\0'; + } + if (the_plist) + plist_free(the_plist); + if (plist_out) + free(plist_out); + } + return reply; +} + +#endif + +void _debug_log_rtsp_message(const char *filename, const int linenumber, int level, char *prompt, + rtsp_message *message) { + if (level > debuglev) + return; + if ((prompt) && (*prompt != '\0')) // okay to pass NULL or an empty list... + _debug(filename, linenumber, level, prompt); + _debug_print_msg_headers(filename, linenumber, level, message); +#ifdef CONFIG_AIRPLAY_2 + char *plist_content = rtsp_plist_content(message); + if (plist_content) { + _debug(filename, linenumber, level, " Content Plist (as XML):\n--\n%s--", plist_content); + free(plist_content); + } else +#endif + { + char *content = message->content; + char obf[4096]; + char *obfp = obf; + uint32_t obfc; + for (obfc=0; obfccontentlength; obfc++) { + snprintf(obfp, 3, "%02X", content[obfc]); + obfp+=2; + }; + *obfp=0; + _debug(filename, linenumber, level, " Cannot find a plist. Content length: %d, content \"%s\".", + message->contentlength, obf); + + } +} + +#define debug_log_rtsp_message(level, prompt, message) \ + _debug_log_rtsp_message(__FILE__, __LINE__, level, prompt, message) +#define debug_print_msg_headers(level, message) \ + _debug_print_msg_headers(__FILE__, __LINE__, level, message) + +#ifdef CONFIG_AIRPLAY_2 +static void buf_add(sized_buffer *buf, uint8_t *in, size_t in_len) { + if (buf->length + in_len > buf->size) { + buf->size = buf->length + in_len + 2048; // Extra legroom to avoid future memcpy's + uint8_t *new = malloc(buf->size); + memcpy(new, buf->data, buf->length); + free(buf->data); + buf->data = new; + } + memcpy(buf->data + buf->length, in, in_len); + buf->length += in_len; +} + +static void buf_drain(sized_buffer *buf, ssize_t len) { + if (len < 0 || (size_t)len >= buf->length) { + free(buf->data); + memset(buf, 0, sizeof(sized_buffer)); + return; + } + memmove(buf->data, buf->data + len, buf->length - len); + buf->length -= len; +} + +static size_t buf_remove(sized_buffer *buf, uint8_t *out, size_t out_len) { + size_t bytes = (buf->length > out_len) ? out_len : buf->length; + memcpy(out, buf->data, bytes); + buf_drain(buf, bytes); + return bytes; +} + +static ssize_t read_encrypted(int fd, pair_cipher_bundle *ctx, void *buf, size_t count) { + uint8_t in[4096]; + uint8_t *plain; + size_t plain_len; + + // If there is leftover decoded content from the last pass just return that + if (ctx->plaintext_read_buffer.length > 0) { + return buf_remove(&ctx->plaintext_read_buffer, buf, count); + } + + do { + ssize_t got = read(fd, in, sizeof(in)); + if (got <= 0) + return got; + buf_add(&ctx->encrypted_read_buffer, in, got); + + ssize_t consumed = pair_decrypt(&plain, &plain_len, ctx->encrypted_read_buffer.data, + ctx->encrypted_read_buffer.length, ctx->cipher_ctx); + if (consumed < 0) + return -1; + buf_drain(&ctx->encrypted_read_buffer, consumed); + } while (plain_len == 0); + + // Fast path, avoids some memcpy + allocs in case of the normal, small message + /* if (ctx->plaintext_read_buffer.len == 0 && plain_len < count) { + memcpy(buf, plain, plain_len); + free(plain); + return plain_len; + } + */ + buf_add(&ctx->plaintext_read_buffer, plain, plain_len); + free(plain); + + return buf_remove(&ctx->plaintext_read_buffer, buf, count); +} + +static ssize_t write_encrypted(int fd, pair_cipher_bundle *ctx, const void *buf, size_t count) { + uint8_t *encrypted; + size_t encrypted_len; + + ssize_t ret = pair_encrypt(&encrypted, &encrypted_len, buf, count, ctx->cipher_ctx); + if (ret < 0) { + debug(1, pair_cipher_errmsg(ctx->cipher_ctx)); + return -1; + } + + size_t remain = encrypted_len; + while (remain > 0) { + ssize_t wrote = write(fd, encrypted + (encrypted_len - remain), remain); + if (wrote <= 0) { + free(encrypted); + return wrote; + } + remain -= wrote; + } + free(encrypted); + return count; +} + +/* +static ssize_t write_encrypted(rtsp_conn_info *conn, const void *buf, size_t count) { + uint8_t *encrypted; + size_t encrypted_len; + + ssize_t ret = + pair_encrypt(&encrypted, &encrypted_len, buf, count, conn->ap2_pairing_context.cipher_ctx); + if (ret < 0) { + debug(1, pair_cipher_errmsg(conn->ap2_pairing_context.cipher_ctx)); + return -1; + } + + size_t remain = encrypted_len; + while (remain > 0) { + ssize_t wrote = write(conn->fd, encrypted + (encrypted_len - remain), remain); + if (wrote <= 0) { + free(encrypted); + return wrote; + } + remain -= wrote; + } + free(encrypted); + return count; +} +*/ +#endif + +ssize_t timed_read_from_rtsp_connection(rtsp_conn_info *conn, uint64_t wait_time, void *buf, + size_t count) { + // note: a wait time of zero means wait forever + ssize_t result = 0; // closed + if (conn->fd > 0) { + int64_t remaining_time = 0; + uint64_t time_to_wait_to = get_absolute_time_in_ns();; + time_to_wait_to = time_to_wait_to + wait_time; + + // remaining_time will be zero if wait_time is zero + if (wait_time != 0) { + remaining_time = time_to_wait_to - get_absolute_time_in_ns(); + } + do { + struct timeval tv; + tv.tv_sec = remaining_time / 1000000000; // seconds + tv.tv_usec = (remaining_time % 1000000000) / 1000; // microseconds + if (setsockopt(conn->fd, SOL_SOCKET, SO_RCVTIMEO, (const char *)&tv, sizeof tv) != 0) { + char errorstring[1024]; + strerror_r(errno, (char *)errorstring, sizeof(errorstring)); + debug(1, "could not set time limit on timed_read_from_rtsp_connection -- error %d \"%s\".", + errno, errorstring); + } + +#ifdef CONFIG_AIRPLAY_2 + if (conn->ap2_pairing_context.control_cipher_bundle.cipher_ctx) { + conn->ap2_pairing_context.control_cipher_bundle.is_encrypted = 1; + result = read_encrypted(conn->fd, &conn->ap2_pairing_context.control_cipher_bundle, buf, count); + } else { + result = read(conn->fd, buf, count); + } +#else + result = read(conn->fd, buf, count); +#endif + if (wait_time != 0) + remaining_time = time_to_wait_to - get_absolute_time_in_ns(); + if (((result == -1) && ((errno == EAGAIN) || (errno == EWOULDBLOCK))) && (remaining_time > 0)) + debug(1,"remaining time on a timed read is %" PRId64 " ns.", remaining_time); + } while (((result == -1) && ((errno == EAGAIN) || (errno == EWOULDBLOCK))) && (remaining_time > 0)); + + } else { + debug(1,"Connection %d: attempt to read from a closed RTSP connection.",conn->connection_number); + } + return result; +} + +#ifdef CONFIG_AIRPLAY_2 +void set_client_as_ptp_clock(rtsp_conn_info *conn) { + char timing_list_message[4096] = ""; + strncat(timing_list_message, "T ", sizeof(timing_list_message) - 1 - strlen(timing_list_message)); + strncat(timing_list_message, (const char *)&conn->client_ip_string, + sizeof(timing_list_message) - 1 - strlen(timing_list_message)); + ptp_send_control_message_string(timing_list_message); +} + +void clear_ptp_clock() { + ptp_send_control_message_string("T"); +} +#endif + +ssize_t read_from_rtsp_connection(rtsp_conn_info *conn, void *buf, size_t count) { + // first try to read with a timeout, to see if there is any traffic... + // ssize_t response = timed_read_from_rtsp_connection(conn, 20000000000L, buf, count); + // actually don't use a timeout -- OwnTone doesn't supply regular traffic. + ssize_t response = timed_read_from_rtsp_connection(conn, 0, buf, count); + if ((response == -1) && ((errno == EAGAIN) || (errno == EWOULDBLOCK))) { + if (conn->rtsp_link_is_idle == 0) { + debug(1, "Connection %d: RTSP connection is idle.", conn->connection_number); + conn->rtsp_link_is_idle = 1; + conn->udp_clock_sender_is_initialised = 0; + conn->udp_clock_is_initialised = 0; + } + response = timed_read_from_rtsp_connection(conn, 0, buf, count); + } + if (conn->rtsp_link_is_idle == 1) { + conn->rtsp_link_is_idle = 0; + debug(1, "Connection %d: RTSP connection traffic has resumed.", conn->connection_number); +#ifdef CONFIG_AIRPLAY_2 + if (conn->airplay_stream_type == realtime_stream) { + conn->last_anchor_info_is_valid = 0; + conn->anchor_remote_info_is_valid = 0; + conn->first_packet_timestamp = 0; + conn->input_frame_rate_starting_point_is_valid = 0; + ab_resync(conn); + } +#else + conn->anchor_remote_info_is_valid = 0; + conn->local_to_remote_time_difference_measurement_time = 0; + conn->local_to_remote_time_difference = 0; + conn->first_packet_timestamp = 0; + conn->input_frame_rate_starting_point_is_valid = 0; + ab_resync(conn); +#endif + } + return response; +} + +enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn, rtsp_message **the_packet) { + + *the_packet = NULL; // need this for error handling + + enum rtsp_read_request_response reply = rtsp_read_request_response_ok; + ssize_t buflen = 4096; +#ifdef CONFIG_METADATA + if ((config.metadata_enabled != 0) && (config.get_coverart != 0)) + buflen = 1024 * 256; // big enough for typical picture data, which will be base64 encoded +#endif + int release_buffer = 0; // on exit, don't deallocate the buffer if everything was okay + char *buf = malloc(buflen + 1); // add a NUL at the end + if (!buf) { + warn("Connection %d: rtsp_read_request: can't get a buffer.", conn->connection_number); + return (rtsp_read_request_response_error); + } + pthread_cleanup_push(malloc_cleanup, buf); + ssize_t nread; + ssize_t inbuf = 0; + int msg_size = -1; + + while (msg_size < 0) { + if (conn->stop != 0) { + debug(3, "Connection %d: shutdown requested.", conn->connection_number); + reply = rtsp_read_request_response_immediate_shutdown_requested; + goto shutdown; + } + + nread = read_from_rtsp_connection(conn, buf + inbuf, buflen - inbuf); + + if (nread == 0) { + // a blocking read that returns zero means eof -- implies connection closed by client + debug(1, "Connection %d: RTSP connection closed by client.", + conn->connection_number); + reply = rtsp_read_request_response_channel_closed; + // Note: the socket will be closed when the thread exits + goto shutdown; + } + + if (nread < 0) { + if (errno == EINTR) + continue; + if (errno == EAGAIN) { + debug(1, "Connection %d: getting Error 11 -- EAGAIN from a blocking read!", + conn->connection_number); + continue; + } + if (errno != ECONNRESET) { + char errorstring[1024]; + strerror_r(errno, (char *)errorstring, sizeof(errorstring)); + if (errno != 0) + debug(2, "Connection %d: rtsp_read_request_response_read_error %d: \"%s\".", + conn->connection_number, errno, (char *)errorstring); + } + reply = rtsp_read_request_response_read_error; + goto shutdown; + } +/* + // this outputs the message received + { + void *pt = malloc(nread+1); + memset(pt, 0, nread+1); + memcpy(pt, buf + inbuf, nread); + debug(1, "Incoming string on RTSP port: \"%s\"",pt); + free(pt); + } + */ + + inbuf += nread; + + char *next; + while (msg_size < 0 && (next = nextline(buf, inbuf))) { + msg_size = msg_handle_line(the_packet, buf); + + if (!(*the_packet)) { + debug(1, "Connection %d: rtsp_read_request can't find an RTSP header.", + conn->connection_number); + reply = rtsp_read_request_response_bad_packet; + goto shutdown; + } + + inbuf -= next - buf; + if (inbuf) + memmove(buf, next, inbuf); + } + } + + if (msg_size > buflen) { + buf = realloc(buf, msg_size + 1); + if (!buf) { + warn("Connection %d: too much content.", conn->connection_number); + reply = rtsp_read_request_response_error; + goto shutdown; + } + buflen = msg_size; + } + + uint64_t threshold_time = + get_absolute_time_in_ns() + ((uint64_t)15000000000); // i.e. fifteen seconds from now + int warning_message_sent = 0; + + // const size_t max_read_chunk = 1024 * 1024 / 16; + while (inbuf < msg_size) { + + // we are going to read the stream in chunks and time how long it takes to + // do so. + // If it's taking too long, (and we find out about it), we will send an + // error message as + // metadata + + if (warning_message_sent == 0) { + uint64_t time_now = get_absolute_time_in_ns(); + if (time_now > threshold_time) { // it's taking too long + debug(1, "Error receiving metadata from source -- transmission seems " + "to be stalled."); +#ifdef CONFIG_METADATA + send_ssnc_metadata('stal', NULL, 0, 1); +#endif + warning_message_sent = 1; + } + } + + if (conn->stop != 0) { + debug(1, "RTSP shutdown requested."); + reply = rtsp_read_request_response_immediate_shutdown_requested; + goto shutdown; + } + size_t read_chunk = msg_size - inbuf; + // if (read_chunk > max_read_chunk) + // read_chunk = max_read_chunk; + // usleep(80000); // wait about 80 milliseconds between reads of up to max_read_chunk + nread = read_from_rtsp_connection(conn, buf + inbuf, read_chunk); + if (!nread) { + reply = rtsp_read_request_response_error; + goto shutdown; + } + if (nread < 0) { + if (errno == EINTR) + continue; + if (errno == EAGAIN) { + debug(1, "Getting Error 11 -- EAGAIN from a blocking read!"); + continue; + } + if (errno != ECONNRESET) { + char errorstring[1024]; + strerror_r(errno, (char *)errorstring, sizeof(errorstring)); + debug(1, "Connection %d: rtsp_read_request_response_read_error %d: \"%s\".", + conn->connection_number, errno, (char *)errorstring); + } + reply = rtsp_read_request_response_read_error; + goto shutdown; + } + inbuf += nread; + } + + rtsp_message *msg = *the_packet; + msg->contentlength = inbuf; + msg->content = buf; + char *jp = inbuf + buf; + *jp = '\0'; + *the_packet = msg; +shutdown: + if (reply != rtsp_read_request_response_ok) { + msg_free(the_packet); + release_buffer = 1; // allow the buffer to be released + } + pthread_cleanup_pop(release_buffer); + return reply; +} + +int msg_write_response(rtsp_conn_info *conn, rtsp_message *resp) { + char pkt[4096]; + int pktfree = sizeof(pkt); + char *p = pkt; + int n; + unsigned int i; + + struct response_t { + int code; + char *string; + }; + + struct response_t responses[] = {{200, "OK"}, + {400, "Bad Request"}, + {403, "Unauthorized"}, + {404, "Not Found"}, + {451, "Unavailable"}, + {456, "Header Field Not Valid for Resource"}, + {470, "Connection Authorization Required"}, + {500, "Internal Server Error"}, + {501, "Not Implemented"}}; + // 451 is really "Unavailable For Legal Reasons"! + int found = 0; + char *respcode_text = "Unauthorized"; + for (i = 0; i < sizeof(responses) / sizeof(struct response_t); i++) { + if (resp->respcode == responses[i].code) { + found = 1; + respcode_text = responses[i].string; + } + } + + if (found == 0) + debug(1, "can't find text for response code %d. Using \"%s\" instead.", resp->respcode, + respcode_text); + + n = snprintf(p, pktfree, "RTSP/1.0 %d %s\r\n", resp->respcode, respcode_text); + pktfree -= n; + p += n; + + for (i = 0; i < resp->nheaders; i++) { + // debug(3, " %s: %s.", resp->name[i], resp->value[i]); + n = snprintf(p, pktfree, "%s: %s\r\n", resp->name[i], resp->value[i]); + pktfree -= n; + p += n; + if (pktfree <= 1024) { + debug(1, "Attempted to write overlong RTSP packet 1"); + return -1; + } + } + + // Here, if there's content, write the Content-Length header ... + + if (resp->contentlength) { + debug(2, "Responding with content of length %d", resp->contentlength); + n = snprintf(p, pktfree, "Content-Length: %d\r\n", resp->contentlength); + pktfree -= n; + p += n; + if (pktfree <= 1024) { + debug(1, "Attempted to write overlong RTSP packet 2"); + return -2; + } + } + + n = snprintf(p, pktfree, "\r\n"); + pktfree -= n; + p += n; + + if (resp->contentlength) { + memcpy(p, resp->content, resp->contentlength); + pktfree -= resp->contentlength; + p += resp->contentlength; + } + + if (pktfree <= 1024) { + debug(1, "Attempted to write overlong RTSP packet 3"); + return -3; + } + + // here, if the link is encrypted, better do it + +#ifdef CONFIG_AIRPLAY_2 + ssize_t reply; + if (conn->ap2_pairing_context.control_cipher_bundle.is_encrypted) { + reply = + write_encrypted(conn->fd, &conn->ap2_pairing_context.control_cipher_bundle, pkt, p - pkt); + } else { + reply = write(conn->fd, pkt, p - pkt); + } +#else + ssize_t reply = write(conn->fd, pkt, p - pkt); +#endif + + if (reply == -1) { + char errorstring[1024]; + strerror_r(errno, (char *)errorstring, sizeof(errorstring)); + debug(1, "msg_write_response error %d: \"%s\".", errno, (char *)errorstring); + return -4; + } + if (reply != p - pkt) { + debug(1, "msg_write_response error -- requested bytes: %d not fully written: %d.", p - pkt, + reply); + return -5; + } + return 0; +} + +char *get_category_string(airplay_stream_c cat) { + char *category; + switch (cat) { + case unspecified_stream_category: + category = "unspecified stream"; + break; + case ptp_stream: + category = "PTP stream"; + break; + case ntp_stream: + category = "NTP stream"; + break; + case remote_control_stream: + category = "Remote Control stream"; + break; + default: + category = "Unexpected stream code"; + break; + } + return category; +} + +void handle_record_2(rtsp_conn_info *conn, __attribute((unused)) rtsp_message *req, + rtsp_message *resp) { + debug(2, "Connection %d: RECORD on %s", conn->connection_number, + get_category_string(conn->airplay_stream_category)); + // debug_log_rtsp_message(1, "RECORD incoming message", req); + resp->respcode = 200; +} + +void handle_record(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { + debug(2, "Connection %d: RECORD", conn->connection_number); + if (have_play_lock(conn)) { + if (conn->player_thread) + warn("Connection %d: RECORD: Duplicate RECORD message -- ignored", conn->connection_number); + else { + debug(1, "Connection %d: Classic AirPlay connection from %s:%u to self at %s:%u.", + conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, + conn->self_ip_string, conn->self_rtsp_port); + activity_monitor_signify_activity(1); + player_prepare_to_play(conn); + player_play(conn); // the thread better be 0 + } + + resp->respcode = 200; + // I think this is for telling the client what the absolute minimum latency + // actually is, + // and when the client specifies a latency, it should be added to this figure. + + // Thus, [the old version of] AirPlay's latency figure of 77175, when added to 11025 gives you + // exactly 88200 + // and iTunes' latency figure of 88553, when added to 11025 gives you 99578, + // pretty close to the 99400 we guessed. + + msg_add_header(resp, "Audio-Latency", "11025"); + + char *p; + uint32_t rtptime = 0; + char *hdr = msg_get_header(req, "RTP-Info"); + + if (hdr) { + // debug(1,"FLUSH message received: \"%s\".",hdr); + // get the rtp timestamp + p = strstr(hdr, "rtptime="); + if (p) { + p = strchr(p, '='); + if (p) { + rtptime = uatoi(p + 1); // unsigned integer -- up to 2^32-1 + // rtptime--; + // debug(1,"RTSP Flush Requested by handle_record: %u.",rtptime); + player_flush(rtptime, conn); + } + } + } + } else { + warn("Connection %d RECORD received without having the player (no ANNOUNCE?)", + conn->connection_number); + resp->respcode = 451; + } +} + +#ifdef CONFIG_AIRPLAY_2 + + +void handle_get_info(__attribute((unused)) rtsp_conn_info *conn, rtsp_message *req, + rtsp_message *resp) { + debug_log_rtsp_message(2, "GET /info:", req); + if (rtsp_message_contains_plist(req)) { // it's stage one + // get version of AirPlay -- it might be too old. Not using it yet. + char *hdr = msg_get_header(req, "User-Agent"); + if (hdr) { + if (strstr(hdr, "AirPlay/") == hdr) { + hdr = hdr + strlen("AirPlay/"); + // double airplay_version = 0.0; + // airplay_version = atof(hdr); + debug(2, "Connection %d: GET_INFO: Source AirPlay Version is: %s.", conn->connection_number, + hdr); + } + } + plist_t info_plist = NULL; + plist_from_memory(req->content, req->contentlength, &info_plist); + + plist_t qualifier = plist_dict_get_item(info_plist, "qualifier"); + if (qualifier == NULL) { + debug(1, "GET /info Stage 1: plist->qualifier was NULL"); + goto user_fail; + } + if (plist_array_get_size(qualifier) < 1) { + debug(1, "GET /info Stage 1: plist->qualifier array length < 1"); + goto user_fail; + } + plist_t qualifier_array_value = plist_array_get_item(qualifier, 0); + char *qualifier_array_val_cstr; + plist_get_string_val(qualifier_array_value, &qualifier_array_val_cstr); + if (qualifier_array_val_cstr == NULL) { + debug(1, "GET /info Stage 1: first item in qualifier array not a string"); + goto user_fail; + } + debug(2, "GET /info Stage 1: qualifier: %s", qualifier_array_val_cstr); + plist_free(info_plist); + free(qualifier_array_val_cstr); + + // uint8_t bt_addr[6] = {0xB8, 0x27, 0xEB, 0xB7, 0xD4, 0x0E}; + plist_t response_plist = NULL; + plist_from_xml((const char *)plists_get_info_response_xml, plists_get_info_response_xml_len, + &response_plist); + if (response_plist == NULL) { + debug(1, "GET /info Stage 1: response plist not created from XML!"); + } else { + void *qualifier_response_data = NULL; + size_t qualifier_response_data_length = 0; + if (add_pstring_to_malloc("acl=0", &qualifier_response_data, + &qualifier_response_data_length) == 0) + debug(1, "Problem"); + if (add_pstring_to_malloc(deviceIdString, &qualifier_response_data, + &qualifier_response_data_length) == 0) + debug(1, "Problem"); + if (add_pstring_to_malloc(featuresString, &qualifier_response_data, + &qualifier_response_data_length) == 0) + debug(1, "Problem"); + if (add_pstring_to_malloc("rsf=0x0", &qualifier_response_data, + &qualifier_response_data_length) == 0) + debug(1, "Problem"); + if (add_pstring_to_malloc("flags=0x4", &qualifier_response_data, + &qualifier_response_data_length) == 0) + debug(1, "Problem"); + if (add_pstring_to_malloc("model=Shairport Sync", &qualifier_response_data, + &qualifier_response_data_length) == 0) + debug(1, "Problem"); + if (add_pstring_to_malloc("manufacturer=", &qualifier_response_data, + &qualifier_response_data_length) == 0) + debug(1, "Problem"); + if (add_pstring_to_malloc("serialNumber=", &qualifier_response_data, + &qualifier_response_data_length) == 0) + debug(1, "Problem"); + if (add_pstring_to_malloc("protovers=1.1", &qualifier_response_data, + &qualifier_response_data_length) == 0) + debug(1, "Problem"); + if (add_pstring_to_malloc("srcvers=366.0", &qualifier_response_data, + &qualifier_response_data_length) == 0) + debug(1, "Problem"); + if (add_pstring_to_malloc(piString, &qualifier_response_data, + &qualifier_response_data_length) == 0) + debug(1, "Problem"); + if (add_pstring_to_malloc(gidString, &qualifier_response_data, + &qualifier_response_data_length) == 0) + debug(1, "Problem"); + if (add_pstring_to_malloc("gcgl=0", &qualifier_response_data, + &qualifier_response_data_length) == 0) + debug(1, "Problem"); + snprintf(pkString, sizeof(pkString), "pk="); + pkString_make(pkString + strlen("pk="), sizeof(pkString) - strlen("pk="), + config.airplay_device_id); + if (add_pstring_to_malloc(pkString, &qualifier_response_data, + &qualifier_response_data_length) == 0) + debug(1, "Problem"); + // debug(1,"qualifier_response_data_length: %u.", qualifier_response_data_length); + + plist_dict_set_item(response_plist, "txtAirPlay", + plist_new_data(qualifier_response_data, qualifier_response_data_length)); + + plist_dict_set_item(response_plist, "features", plist_new_uint(config.airplay_features)); + plist_dict_set_item(response_plist, "statusFlags", + plist_new_uint(config.airplay_statusflags)); + plist_dict_set_item(response_plist, "deviceID", plist_new_string(config.airplay_device_id)); + plist_dict_set_item(response_plist, "pi", plist_new_string(config.airplay_pi)); + plist_dict_set_item(response_plist, "name", plist_new_string(config.service_name)); + char *vs = get_version_string(); + // plist_dict_set_item(response_plist, "model", plist_new_string(vs)); + plist_dict_set_item(response_plist, "model", plist_new_string("Shairport Sync")); + free(vs); + // pkString_make(pkString, sizeof(pkString), config.airplay_device_id); + // plist_dict_set_item(response_plist, "pk", plist_new_string(pkString)); + plist_to_bin(response_plist, &resp->content, &resp->contentlength); + if (resp->contentlength == 0) + debug(1, "GET /info Stage 1: response bplist not created!"); + plist_free(response_plist); + free(qualifier_response_data); + } + msg_add_header(resp, "Content-Type", "application/x-apple-binary-plist"); + debug_log_rtsp_message(2, "GET /info Stage 1 Response:", resp); + resp->respcode = 200; + return; + + user_fail: + resp->respcode = 400; + return; + } else { // stage two + plist_t response_plist = NULL; + plist_from_xml((const char *)plists_get_info_response_xml, plists_get_info_response_xml_len, + &response_plist); + plist_dict_set_item(response_plist, "features", plist_new_uint(config.airplay_features)); + plist_dict_set_item(response_plist, "statusFlags", plist_new_uint(config.airplay_statusflags)); + plist_dict_set_item(response_plist, "deviceID", plist_new_string(config.airplay_device_id)); + plist_dict_set_item(response_plist, "pi", plist_new_string(config.airplay_pi)); + plist_dict_set_item(response_plist, "name", plist_new_string(config.service_name)); + char *vs = get_version_string(); + // plist_dict_set_item(response_plist, "model", plist_new_string(vs)); + plist_dict_set_item(response_plist, "model", plist_new_string("Shairport Sync")); + free(vs); + // pkString_make(pkString, sizeof(pkString), config.airplay_device_id); + // plist_dict_set_item(response_plist, "pk", plist_new_string(pkString)); + plist_to_bin(response_plist, &resp->content, &resp->contentlength); + plist_free(response_plist); + msg_add_header(resp, "Content-Type", "application/x-apple-binary-plist"); + debug_log_rtsp_message(2, "GET /info Stage 2 Response", resp); + resp->respcode = 200; + return; + } +} + +void handle_flushbuffered(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { + debug(3, "Connection %d: FLUSHBUFFERED %s : Content-Length %d", conn->connection_number, + req->path, req->contentlength); + debug_log_rtsp_message(2, "FLUSHBUFFERED request", req); + + uint64_t flushFromSeq = 0; + uint64_t flushFromTS = 0; + uint64_t flushUntilSeq = 0; + uint64_t flushUntilTS = 0; + int flushFromValid = 0; + plist_t messagePlist = plist_from_rtsp_content(req); + if (messagePlist != NULL) { + plist_t item = plist_dict_get_item(messagePlist, "flushFromSeq"); + if (item == NULL) { + debug(2, "Can't find a flushFromSeq"); + } else { + flushFromValid = 1; + plist_get_uint_val(item, &flushFromSeq); + debug(2, "flushFromSeq is %" PRId64 ".", flushFromSeq); + } + + item = plist_dict_get_item(messagePlist, "flushFromTS"); + if (item == NULL) { + if (flushFromValid != 0) + debug(1, "flushFromSeq without flushFromTS!"); + else + debug(2, "Can't find a flushFromTS"); + } else { + plist_get_uint_val(item, &flushFromTS); + if (flushFromValid == 0) + debug(1, "flushFromTS without flushFromSeq!"); + debug(2, "flushFromTS is %" PRId64 ".", flushFromTS); + } + + item = plist_dict_get_item(messagePlist, "flushUntilSeq"); + if (item == NULL) { + debug(1, "Can't find the flushUntilSeq"); + } else { + plist_get_uint_val(item, &flushUntilSeq); + debug(2, "flushUntilSeq is %" PRId64 ".", flushUntilSeq); + } + + item = plist_dict_get_item(messagePlist, "flushUntilTS"); + if (item == NULL) { + debug(1, "Can't find the flushUntilTS"); + } else { + plist_get_uint_val(item, &flushUntilTS); + debug(2, "flushUntilTS is %" PRId64 ".", flushUntilTS); + } + + debug_mutex_lock(&conn->flush_mutex, 1000, 1); + // a flush with from... components will not be followed by a setanchor (i.e. a play) + // if it's a flush that will be followed by a setanchor (i.e. a play) then stop play now. + if (flushFromValid == 0) + conn->ap2_play_enabled = 0; + + // add the exact request as made to the linked list (not used for anything but diagnostics now) + // int flushNow = 0; + // if (flushFromValid == 0) + // flushNow = 1; + // add_flush_request(flushNow, flushFromSeq, flushFromTS, flushUntilSeq, flushUntilTS, conn); + + // now, if it's an immediate flush, replace the existing request, if any + // but it if's a deferred flush and there is an existing deferred request, + // only update the flushUntil stuff -- that seems to preserve + // the intended semantics + + // so, always replace these + conn->ap2_flush_until_sequence_number = flushUntilSeq; + conn->ap2_flush_until_rtp_timestamp = flushUntilTS; + + if ((conn->ap2_flush_requested != 0) && (conn->ap2_flush_from_valid != 0) && + (flushFromValid != 0)) { + // if there is a request already, and it's a deferred request, and the current request is also + // deferred... do nothing! -- leave the starting point in place. Yeah, yeah, we know de Morgan's + // Law, but this seems clearer + } else { + conn->ap2_flush_from_sequence_number = flushFromSeq; + conn->ap2_flush_from_rtp_timestamp = flushFromTS; + } + + conn->ap2_flush_from_valid = flushFromValid; + conn->ap2_flush_requested = 1; + + // reflect the possibly updated flush request + // add_flush_request(flushNow, conn->ap2_flush_from_sequence_number, + // conn->ap2_flush_from_rtp_timestamp, conn->ap2_flush_until_sequence_number, + // conn->ap2_flush_until_rtp_timestamp, conn); + + debug_mutex_unlock(&conn->flush_mutex, 3); + + if (flushFromValid) + debug(2, "Deferred Flush Requested"); + else + debug(2, "Immediate Flush Requested"); + + plist_free(messagePlist); + // display_all_flush_requests(conn); + } + + resp->respcode = 200; +} + +void handle_setrate(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { + debug(3, "Connection %d: SETRATE %s : Content-Length %d", conn->connection_number, req->path, + req->contentlength); + debug_log_rtsp_message(2, "SETRATE request -- unimplemented", req); + resp->respcode = 501; // Not Implemented +} + +void handle_unimplemented_ap1(__attribute((unused)) rtsp_conn_info *conn, rtsp_message *req, + rtsp_message *resp) { + debug_log_rtsp_message(1, "request not recognised for AirPlay 1 operation", req); + resp->respcode = 501; +} + +void handle_setrateanchori(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { + debug(3, "Connection %d: SETRATEANCHORI %s :: Content-Length %d", conn->connection_number, + req->path, req->contentlength); + + plist_t messagePlist = plist_from_rtsp_content(req); + + if (messagePlist != NULL) { + pthread_cleanup_push(plist_cleanup, (void *)messagePlist); + plist_t item = plist_dict_get_item(messagePlist, "networkTimeSecs"); + if (item != NULL) { + plist_t item_2 = plist_dict_get_item(messagePlist, "networkTimeTimelineID"); + if (item_2 == NULL) { + debug(1, "Can't identify the Clock ID of the player."); + } else { + uint64_t nid; + plist_get_uint_val(item_2, &nid); + debug(2, "networkTimeTimelineID \"%" PRIx64 "\".", nid); + conn->networkTimeTimelineID = nid; + } + uint64_t networkTimeSecs; + plist_get_uint_val(item, &networkTimeSecs); + debug(2, "anchor networkTimeSecs is %" PRIu64 ".", networkTimeSecs); + + item = plist_dict_get_item(messagePlist, "networkTimeFrac"); + uint64_t networkTimeFrac; + plist_get_uint_val(item, &networkTimeFrac); + debug(2, "anchor networkTimeFrac is 0%" PRIu64 ".", networkTimeFrac); + // it looks like the networkTimeFrac is a fraction where the msb is work 1/2, the + // next 1/4 and so on + // now, convert the network time and fraction into nanoseconds + networkTimeFrac = networkTimeFrac >> 32; // reduce precision to about 1/4 nanosecond + networkTimeFrac = networkTimeFrac * 1000000000; + networkTimeFrac = networkTimeFrac >> 32; // we should now be left with the ns + + networkTimeSecs = networkTimeSecs * 1000000000; // turn the whole seconds into ns + uint64_t anchorTimeNanoseconds = networkTimeSecs + networkTimeFrac; + + debug(2, "anchorTimeNanoseconds looks like %" PRIu64 ".", anchorTimeNanoseconds); + + item = plist_dict_get_item(messagePlist, "rtpTime"); + uint64_t rtpTime; + + plist_get_uint_val(item, &rtpTime); + // debug(1, "anchor rtpTime is %" PRId64 ".", rtpTime); + uint32_t anchorRTPTime = rtpTime; + + int32_t added_latency = (int32_t)(config.audio_backend_latency_offset * conn->input_rate); + // debug(1,"anchorRTPTime: %" PRIu32 ", added latency: %" PRId32 ".", anchorRTPTime, + // added_latency); + set_ptp_anchor_info(conn, conn->networkTimeTimelineID, anchorRTPTime - added_latency, + anchorTimeNanoseconds); + } + + item = plist_dict_get_item(messagePlist, "rate"); + if (item != NULL) { + uint64_t rate; + plist_get_uint_val(item, &rate); + debug(3, "anchor rate 0x%016" PRIx64 ".", rate); + debug_mutex_lock(&conn->flush_mutex, 1000, 1); + pthread_cleanup_push(mutex_unlock, &conn->flush_mutex); + conn->ap2_rate = rate; + if ((rate & 1) != 0) { + debug(2, "Connection %d: Start playing, with anchor clock %" PRIx64 ".", + conn->connection_number, conn->networkTimeTimelineID); + activity_monitor_signify_activity(1); + +#ifdef CONFIG_METADATA + send_ssnc_metadata('pres', NULL, 0, 1); // resume -- contains cancellation points +#endif + conn->ap2_play_enabled = 1; + } else { + debug(2, "Connection %d: Stop playing.", conn->connection_number); + conn->ap2_play_enabled = 0; + activity_monitor_signify_activity(0); + reset_anchor_info(conn); +#ifdef CONFIG_METADATA + send_ssnc_metadata('paus', NULL, 0, 1); // pause -- contains cancellation points +#endif + if (config.output->stop) { + debug(2, "Connection %d: Stop the output backend.", conn->connection_number); + config.output->stop(); + } + } + pthread_cleanup_pop(1); // unlock the conn->flush_mutex + } + pthread_cleanup_pop(1); // plist_free the messagePlist; + } else { + debug(1, "missing plist!"); + } + resp->respcode = 200; +} + +void handle_get(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { + debug(2, "Connection %d: GET %s :: Content-Length %d", conn->connection_number, req->path, + req->contentlength); + debug_log_rtsp_message(2, "GET request", req); + if (strcmp(req->path, "/info") == 0) { + handle_get_info(conn, req, resp); + } else { + debug(1, "Unhandled GET, path \"%s\".", req->path); + resp->respcode = 501; // Not Implemented + } +} + +#else +void handle_get(__attribute((unused)) rtsp_conn_info *conn, __attribute((unused)) rtsp_message *req, + __attribute((unused)) rtsp_message *resp) { + debug(1, "Connection %d: GET %s Content-Length %d", conn->connection_number, req->path, + req->contentlength); + resp->respcode = 500; +} + +void handle_post(rtsp_conn_info *conn, rtsp_message *req, + __attribute((unused)) rtsp_message *resp) { + debug(1, "Connection %d: POST %s Content-Length %d", conn->connection_number, req->path, + req->contentlength); + resp->respcode = 500; +} +#endif + +#ifdef CONFIG_AIRPLAY_2 +struct pairings { + char device_id[PAIR_AP_DEVICE_ID_LEN_MAX]; + uint8_t public_key[32]; + + struct pairings *next; +} * pairings; + +static struct pairings *pairing_find(const char *device_id) { + for (struct pairings *pairing = pairings; pairing; pairing = pairing->next) { + if (strcmp(device_id, pairing->device_id) == 0) + return pairing; + } + return NULL; +} + +static void pairing_add(uint8_t public_key[32], const char *device_id) { + struct pairings *pairing = calloc(1, sizeof(struct pairings)); + snprintf(pairing->device_id, sizeof(pairing->device_id), "%s", device_id); + memcpy(pairing->public_key, public_key, sizeof(pairing->public_key)); + + pairing->next = pairings; + pairings = pairing; +} + +static void pairing_remove(struct pairings *pairing) { + if (pairing == pairings) { + pairings = pairing->next; + } else { + struct pairings *iter; + for (iter = pairings; iter && (iter->next != pairing); iter = iter->next) + ; /* EMPTY */ + + if (iter) + iter->next = pairing->next; + } + + free(pairing); +} + +static int pairing_add_cb(uint8_t public_key[32], const char *device_id, + void *cb_arg __attribute__((unused))) { + debug(1, "pair-add cb for %s", device_id); + + struct pairings *pairing = pairing_find(device_id); + if (pairing) { + memcpy(pairing->public_key, public_key, sizeof(pairing->public_key)); + return 0; + } + + pairing_add(public_key, device_id); + return 0; +} + +static int pairing_remove_cb(uint8_t public_key[32] __attribute__((unused)), const char *device_id, + void *cb_arg __attribute__((unused))) { + debug(1, "pair-remove cb for %s", device_id); + + struct pairings *pairing = pairing_find(device_id); + if (!pairing) { + debug(1, "pair-remove callback for unknown device"); + return -1; + } + + pairing_remove(pairing); + return 0; +} + +static void pairing_list_cb(pair_cb enum_cb, void *enum_cb_arg, + void *cb_arg __attribute__((unused))) { + debug(2, "pair-list cb"); + + for (struct pairings *pairing = pairings; pairing; pairing = pairing->next) { + enum_cb(pairing->public_key, pairing->device_id, enum_cb_arg); + } +} + +void handle_pair_add(rtsp_conn_info *conn __attribute__((unused)), rtsp_message *req, + rtsp_message *resp) { + uint8_t *body = NULL; + size_t body_len = 0; + int ret = pair_add(PAIR_SERVER_HOMEKIT, &body, &body_len, pairing_add_cb, NULL, + (const uint8_t *)req->content, req->contentlength); + if (ret < 0) { + debug(1, "pair-add returned an error"); + resp->respcode = 451; + return; + } + resp->content = (char *)body; // these will be freed when the data is sent + resp->contentlength = body_len; + msg_add_header(resp, "Content-Type", "application/octet-stream"); + debug_log_rtsp_message(2, "pair-add response", resp); +} + +void handle_pair_list(rtsp_conn_info *conn __attribute__((unused)), rtsp_message *req, + rtsp_message *resp) { + uint8_t *body = NULL; + size_t body_len = 0; + int ret = pair_list(PAIR_SERVER_HOMEKIT, &body, &body_len, pairing_list_cb, NULL, + (const uint8_t *)req->content, req->contentlength); + if (ret < 0) { + debug(1, "pair-list returned an error"); + resp->respcode = 451; + return; + } + resp->content = (char *)body; // these will be freed when the data is sent + resp->contentlength = body_len; + msg_add_header(resp, "Content-Type", "application/octet-stream"); + debug_log_rtsp_message(2, "pair-list response", resp); +} + +void handle_pair_remove(rtsp_conn_info *conn __attribute__((unused)), rtsp_message *req, + rtsp_message *resp) { + uint8_t *body = NULL; + size_t body_len = 0; + int ret = pair_remove(PAIR_SERVER_HOMEKIT, &body, &body_len, pairing_remove_cb, NULL, + (const uint8_t *)req->content, req->contentlength); + if (ret < 0) { + debug(1, "pair-remove returned an error"); + resp->respcode = 451; + return; + } + resp->content = (char *)body; // these will be freed when the data is sent + resp->contentlength = body_len; + msg_add_header(resp, "Content-Type", "application/octet-stream"); + debug_log_rtsp_message(2, "pair-remove response", resp); +} + +void handle_pair_verify(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { + int ret; + uint8_t *body = NULL; + size_t body_len = 0; + struct pair_result *result; + debug(2, "Connection %d: pair-verify Content-Length %d", conn->connection_number, + req->contentlength); + + if (!conn->ap2_pairing_context.verify_ctx) { + conn->ap2_pairing_context.verify_ctx = + pair_verify_new(PAIR_SERVER_HOMEKIT, NULL, NULL, NULL, config.airplay_device_id); + if (!conn->ap2_pairing_context.verify_ctx) { + debug(1, "Error creating verify context"); + resp->respcode = 500; // Internal Server Error + goto out; + } + } + + ret = pair_verify(&body, &body_len, conn->ap2_pairing_context.verify_ctx, + (const uint8_t *)req->content, req->contentlength); + if (ret < 0) { + debug(1, pair_verify_errmsg(conn->ap2_pairing_context.verify_ctx)); + resp->respcode = 470; // Connection Authorization Required + goto out; + } + + ret = pair_verify_result(&result, conn->ap2_pairing_context.verify_ctx); + if (ret == 0 && result->shared_secret_len > 0) { + conn->ap2_pairing_context.control_cipher_bundle.cipher_ctx = + pair_cipher_new(PAIR_SERVER_HOMEKIT, 2, result->shared_secret, result->shared_secret_len); + if (!conn->ap2_pairing_context.control_cipher_bundle.cipher_ctx) { + debug(1, "Error setting up rtsp control channel ciphering\n"); + goto out; + } + } + +out: + resp->content = (char *)body; // these will be freed when the data is sent + resp->contentlength = body_len; + if (body) + msg_add_header(resp, "Content-Type", "application/octet-stream"); + debug_log_rtsp_message(2, "pair-verify response", resp); +} + +void handle_pair_setup(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { + debug_log_rtsp_message(1, "pair-setup", req); + int ret; + uint8_t *body = NULL; + size_t body_len = 0; + struct pair_result *result; + debug(2, "Connection %d: handle_pair-setup Content-Length %d", conn->connection_number, + req->contentlength); + + if (!conn->ap2_pairing_context.setup_ctx) { + conn->ap2_pairing_context.setup_ctx = pair_setup_new(PAIR_SERVER_HOMEKIT, config.airplay_pin, + NULL, NULL, config.airplay_device_id); + if (!conn->ap2_pairing_context.setup_ctx) { + debug(1, "Error creating setup context"); + resp->respcode = 500; // Internal Server Error + goto out; + } + } + + ret = pair_setup(&body, &body_len, conn->ap2_pairing_context.setup_ctx, + (const uint8_t *)req->content, req->contentlength); + if (ret < 0) { + debug(1, pair_setup_errmsg(conn->ap2_pairing_context.setup_ctx)); + resp->respcode = 470; // Connection Authorization Required + goto out; + } + + ret = pair_setup_result(NULL, &result, conn->ap2_pairing_context.setup_ctx); + if (ret == 0 && result->shared_secret_len > 0) { + // Transient pairing completed (pair-setup step 2), prepare encryption, but + // don't activate yet, the response to this request is still plaintext + conn->ap2_pairing_context.control_cipher_bundle.cipher_ctx = + pair_cipher_new(PAIR_SERVER_HOMEKIT, 2, result->shared_secret, result->shared_secret_len); + if (!conn->ap2_pairing_context.control_cipher_bundle.cipher_ctx) { + debug(1, "Error setting up rtsp control channel ciphering\n"); + goto out; + } + } + +out: + resp->content = (char *)body; // these will be freed when the data is sent + resp->contentlength = body_len; + if (body) + msg_add_header(resp, "Content-Type", "application/octet-stream"); + debug_log_rtsp_message(1, "pair-setup response", resp); +} + +void handle_fp_setup(__attribute__((unused)) rtsp_conn_info *conn, rtsp_message *req, + rtsp_message *resp) { + + /* Fairplay magic */ + static uint8_t server_fp_reply1[] = + "\x46\x50\x4c\x59\x03\x01\x02\x00\x00\x00\x00\x82\x02\x00\x0f\x9f\x3f\x9e\x0a" + "\x25\x21\xdb\xdf\x31\x2a\xb2\xbf\xb2\x9e\x8d\x23\x2b\x63\x76\xa8\xc8\x18\x70" + "\x1d\x22\xae\x93\xd8\x27\x37\xfe\xaf\x9d\xb4\xfd\xf4\x1c\x2d\xba\x9d\x1f\x49" + "\xca\xaa\xbf\x65\x91\xac\x1f\x7b\xc6\xf7\xe0\x66\x3d\x21\xaf\xe0\x15\x65\x95" + "\x3e\xab\x81\xf4\x18\xce\xed\x09\x5a\xdb\x7c\x3d\x0e\x25\x49\x09\xa7\x98\x31" + "\xd4\x9c\x39\x82\x97\x34\x34\xfa\xcb\x42\xc6\x3a\x1c\xd9\x11\xa6\xfe\x94\x1a" + "\x8a\x6d\x4a\x74\x3b\x46\xc3\xa7\x64\x9e\x44\xc7\x89\x55\xe4\x9d\x81\x55\x00" + "\x95\x49\xc4\xe2\xf7\xa3\xf6\xd5\xba"; + static uint8_t server_fp_reply2[] = + "\x46\x50\x4c\x59\x03\x01\x02\x00\x00\x00\x00\x82\x02\x01\xcf\x32\xa2\x57\x14" + "\xb2\x52\x4f\x8a\xa0\xad\x7a\xf1\x64\xe3\x7b\xcf\x44\x24\xe2\x00\x04\x7e\xfc" + "\x0a\xd6\x7a\xfc\xd9\x5d\xed\x1c\x27\x30\xbb\x59\x1b\x96\x2e\xd6\x3a\x9c\x4d" + "\xed\x88\xba\x8f\xc7\x8d\xe6\x4d\x91\xcc\xfd\x5c\x7b\x56\xda\x88\xe3\x1f\x5c" + "\xce\xaf\xc7\x43\x19\x95\xa0\x16\x65\xa5\x4e\x19\x39\xd2\x5b\x94\xdb\x64\xb9" + "\xe4\x5d\x8d\x06\x3e\x1e\x6a\xf0\x7e\x96\x56\x16\x2b\x0e\xfa\x40\x42\x75\xea" + "\x5a\x44\xd9\x59\x1c\x72\x56\xb9\xfb\xe6\x51\x38\x98\xb8\x02\x27\x72\x19\x88" + "\x57\x16\x50\x94\x2a\xd9\x46\x68\x8a"; + static uint8_t server_fp_reply3[] = + "\x46\x50\x4c\x59\x03\x01\x02\x00\x00\x00\x00\x82\x02\x02\xc1\x69\xa3\x52\xee" + "\xed\x35\xb1\x8c\xdd\x9c\x58\xd6\x4f\x16\xc1\x51\x9a\x89\xeb\x53\x17\xbd\x0d" + "\x43\x36\xcd\x68\xf6\x38\xff\x9d\x01\x6a\x5b\x52\xb7\xfa\x92\x16\xb2\xb6\x54" + "\x82\xc7\x84\x44\x11\x81\x21\xa2\xc7\xfe\xd8\x3d\xb7\x11\x9e\x91\x82\xaa\xd7" + "\xd1\x8c\x70\x63\xe2\xa4\x57\x55\x59\x10\xaf\x9e\x0e\xfc\x76\x34\x7d\x16\x40" + "\x43\x80\x7f\x58\x1e\xe4\xfb\xe4\x2c\xa9\xde\xdc\x1b\x5e\xb2\xa3\xaa\x3d\x2e" + "\xcd\x59\xe7\xee\xe7\x0b\x36\x29\xf2\x2a\xfd\x16\x1d\x87\x73\x53\xdd\xb9\x9a" + "\xdc\x8e\x07\x00\x6e\x56\xf8\x50\xce"; + static uint8_t server_fp_reply4[] = + "\x46\x50\x4c\x59\x03\x01\x02\x00\x00\x00\x00\x82\x02\x03\x90\x01\xe1\x72\x7e" + "\x0f\x57\xf9\xf5\x88\x0d\xb1\x04\xa6\x25\x7a\x23\xf5\xcf\xff\x1a\xbb\xe1\xe9" + "\x30\x45\x25\x1a\xfb\x97\xeb\x9f\xc0\x01\x1e\xbe\x0f\x3a\x81\xdf\x5b\x69\x1d" + "\x76\xac\xb2\xf7\xa5\xc7\x08\xe3\xd3\x28\xf5\x6b\xb3\x9d\xbd\xe5\xf2\x9c\x8a" + "\x17\xf4\x81\x48\x7e\x3a\xe8\x63\xc6\x78\x32\x54\x22\xe6\xf7\x8e\x16\x6d\x18" + "\xaa\x7f\xd6\x36\x25\x8b\xce\x28\x72\x6f\x66\x1f\x73\x88\x93\xce\x44\x31\x1e" + "\x4b\xe6\xc0\x53\x51\x93\xe5\xef\x72\xe8\x68\x62\x33\x72\x9c\x22\x7d\x82\x0c" + "\x99\x94\x45\xd8\x92\x46\xc8\xc3\x59"; + + static uint8_t server_fp_header[] = "\x46\x50\x4c\x59\x03\x01\x04\x00\x00\x00\x00\x14"; + + resp->respcode = 200; // assume it's handled + + // uint8_t *out; + // size_t out_len; + int version_pos = 4; + int mode_pos = 14; + int type_pos = 5; + int seq_pos = 6; + int setup_message_type = 1; + int setup1_message_seq = 1; + int setup2_message_seq = 3; + int setup2_suffix_len = 20; + // int ret; + + // response and len are dummy values and can be ignored + + // debug(1, "Version: %02x, mode: %02x, type: %02x, seq: %02x", req->content[version_pos], + // req->content[mode_pos], req->content[type_pos], req->content[seq_pos]); + + if (req->content[version_pos] != 3 || req->content[type_pos] != setup_message_type) { + debug(1, "Unsupported FP version."); + } + + char *response = NULL; + size_t len = 0; + + if (req->content[seq_pos] == setup1_message_seq) { + // All replies are the same length. -1 to account for the NUL byte at the end. + len = sizeof(server_fp_reply1) - 1; + + if (req->content[mode_pos] == 0) + response = memdup(server_fp_reply1, len); + if (req->content[mode_pos] == 1) + response = memdup(server_fp_reply2, len); + if (req->content[mode_pos] == 2) + response = memdup(server_fp_reply3, len); + if (req->content[mode_pos] == 3) + response = memdup(server_fp_reply4, len); + + } else if (req->content[seq_pos] == setup2_message_seq) { + // -1 to account for the NUL byte at the end. + len = sizeof(server_fp_header) - 1 + setup2_suffix_len; + response = malloc(len); + if (response) { + memcpy(response, server_fp_header, sizeof(server_fp_header) - 1); + memcpy(response + sizeof(server_fp_header) - 1, + req->content + req->contentlength - setup2_suffix_len, setup2_suffix_len); + } + } + + if (response == NULL) { + debug(1, "Cannot create a response."); + } + + resp->content = response; // these will be freed when the data is sent + resp->contentlength = len; + msg_add_header(resp, "Content-Type", "application/octet-stream"); +} + +/* + Identifier + 21cc689d-d5de-4814-872c-71d1426b57e0 + Enable_HK_Access_Control + + PublicKey + + qXJDhhL5F3OACL+HO7LVLQVdy0OJtavepjpF720PaOQ= + + Device_Name + MyDevice + Access_Control_Level + 0 +*/ +void handle_configure(rtsp_conn_info *conn __attribute__((unused)), + rtsp_message *req __attribute__((unused)), rtsp_message *resp) { + uint8_t public_key[32]; + + pair_public_key_get(PAIR_SERVER_HOMEKIT, public_key, config.airplay_device_id); + + plist_t response_plist = plist_new_dict(); + + plist_dict_set_item(response_plist, "Identifier", plist_new_string(config.airplay_pi)); + plist_dict_set_item(response_plist, "Enable_HK_Access_Control", plist_new_bool(1)); + plist_dict_set_item(response_plist, "PublicKey", + plist_new_data((const char *)public_key, sizeof(public_key))); + plist_dict_set_item(response_plist, "Device_Name", plist_new_string(config.service_name)); + plist_dict_set_item(response_plist, "Access_Control_Level", plist_new_uint(0)); + + plist_to_bin(response_plist, &resp->content, &resp->contentlength); + plist_free(response_plist); + + msg_add_header(resp, "Content-Type", "application/x-apple-binary-plist"); + debug_log_rtsp_message(2, "POST /configure response:", resp); +} + +void handle_feedback(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req, + __attribute__((unused)) rtsp_message *resp) { + debug(2, "Connection %d: POST %s Content-Length %d", conn->connection_number, req->path, + req->contentlength); + debug_log_rtsp_message(2, NULL, req); + if (conn->airplay_stream_category == remote_control_stream) { + plist_t array_plist = plist_new_array(); + + plist_t response_plist = plist_new_dict(); + plist_dict_set_item(response_plist, "streams", array_plist); + + plist_to_bin(response_plist, &resp->content, &resp->contentlength); + plist_free(response_plist); + + msg_add_header(resp, "Content-Type", "application/x-apple-binary-plist"); + debug_log_rtsp_message(2, "FEEDBACK response (remote_control_stream):", resp); + } + + /* not finished yet + plist_t payload_plist = plist_new_dict(); + plist_dict_set_item(payload_plist, "type", plist_new_uint(103)); + plist_dict_set_item(payload_plist, "sr", plist_new_real(44100.0)); + + plist_t array_plist = plist_new_array(); + plist_array_append_item(array_plist, payload_plist); + + plist_t response_plist = plist_new_dict(); + plist_dict_set_item(response_plist, "streams",array_plist); + + plist_to_bin(response_plist, &resp->content, &resp->contentlength); + plist_free(response_plist); + // plist_free(array_plist); + // plist_free(payload_plist); + + msg_add_header(resp, "Content-Type", "application/x-apple-binary-plist"); + debug_log_rtsp_message(2, "FEEDBACK response:", resp); + */ +} + +void handle_command(__attribute__((unused)) rtsp_conn_info *conn, rtsp_message *req, + __attribute__((unused)) rtsp_message *resp) { + debug(2, "Connection %d: POST %s Content-Length %d", conn->connection_number, req->path, + req->contentlength); + debug_log_rtsp_message(2, NULL, req); + if (rtsp_message_contains_plist(req)) { + plist_t command_dict = NULL; + plist_from_memory(req->content, req->contentlength, &command_dict); + if (command_dict != NULL) { + // we have a plist -- try to get the dict item keyed to "updateMRSupportedCommands" + plist_t item = plist_dict_get_item(command_dict, "type"); + if (item != NULL) { + char *typeValue = NULL; + plist_get_string_val(item, &typeValue); + if ((typeValue != NULL) && (strcmp(typeValue, "updateMRSupportedCommands") == 0)) { + item = plist_dict_get_item(command_dict, "params"); + if (item != NULL) { + // the item should be a dict + plist_t item_array = plist_dict_get_item(item, "mrSupportedCommandsFromSender"); + if (item_array != NULL) { + // here we have an array of data items + uint32_t items = plist_array_get_size(item_array); + if (items) { + uint32_t item_number; + for (item_number = 0; item_number < items; item_number++) { + plist_t the_item = plist_array_get_item(item_array, item_number); + char *buff = NULL; + uint64_t length = 0; + plist_get_data_val(the_item, &buff, &length); + // debug(1,"Item %d, length: %" PRId64 " bytes", item_number, length); + if ((buff != NULL) && (length >= strlen("bplist00")) && + (strstr(buff, "bplist00") == buff)) { + // debug(1,"Contains a plist."); + plist_t subsidiary_plist = NULL; + plist_from_memory(buff, length, &subsidiary_plist); + if (subsidiary_plist) { + char *printable_plist = plist_content(subsidiary_plist); + if (printable_plist) { + debug(3, "\n%s", printable_plist); + free(printable_plist); + } else { + debug(1, "Can't print the plist!"); + } + // plist_free(subsidiary_plist); + } else { + debug(1, "Can't access the plist!"); + } + } + if (buff != NULL) + free(buff); + } + } + } else { + debug(1, "POST /command no mrSupportedCommandsFromSender item."); + } + } else { + debug(1, "POST /command no params dict."); + } + resp->respcode = 400; // say it's a bad request + } else { + debug(1, + "POST /command plist type is \"%s\", but \"updateMRSupportedCommands\" expected.", + typeValue); + } + if (typeValue != NULL) + free(typeValue); + } else { + debug(2, "Could not find a \"type\" item."); + } + + plist_free(command_dict); + } else { + debug(1, "POST /command plist cannot be inputted."); + } + } else { + debug(1, "POST /command contains no plist"); + } +} + +void handle_audio_mode(rtsp_conn_info *conn, rtsp_message *req, + __attribute__((unused)) rtsp_message *resp) { + debug(2, "Connection %d: POST %s Content-Length %d", conn->connection_number, req->path, + req->contentlength); + debug_log_rtsp_message(2, NULL, req); +} + +void handle_post(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { + resp->respcode = 200; + if (strcmp(req->path, "/pair-setup") == 0) { + handle_pair_setup(conn, req, resp); + } else if (strcmp(req->path, "/pair-verify") == 0) { + handle_pair_verify(conn, req, resp); + } else if (strcmp(req->path, "/pair-add") == 0) { + handle_pair_add(conn, req, resp); + } else if (strcmp(req->path, "/pair-remove") == 0) { + handle_pair_remove(conn, req, resp); + } else if (strcmp(req->path, "/pair-list") == 0) { + handle_pair_list(conn, req, resp); + } else if (strcmp(req->path, "/fp-setup") == 0) { + handle_fp_setup(conn, req, resp); + } else if (strcmp(req->path, "/configure") == 0) { + handle_configure(conn, req, resp); + } else if (strcmp(req->path, "/feedback") == 0) { + handle_feedback(conn, req, resp); + } else if (strcmp(req->path, "/command") == 0) { + handle_command(conn, req, resp); + } else if (strcmp(req->path, "/audioMode") == 0) { + handle_audio_mode(conn, req, resp); + } else { + debug(1, "Connection %d: Unhandled POST %s Content-Length %d", conn->connection_number, + req->path, req->contentlength); + debug_log_rtsp_message(2, "POST request", req); + } +} + +void handle_setpeers(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { + debug(2, "Connection %d: SETPEERS %s Content-Length %d", conn->connection_number, req->path, + req->contentlength); + debug_log_rtsp_message(2, "SETPEERS request", req); +/* + char timing_list_message[4096]; + timing_list_message[0] = 'T'; + timing_list_message[1] = 0; + + // ensure the client itself is first -- it's okay if it's duplicated later + strncat(timing_list_message, " ", sizeof(timing_list_message) - 1 - strlen(timing_list_message)); + strncat(timing_list_message, (const char *)&conn->client_ip_string, + sizeof(timing_list_message) - 1 - strlen(timing_list_message)); + + plist_t addresses_array = NULL; + plist_from_memory(req->content, req->contentlength, &addresses_array); + uint32_t items = plist_array_get_size(addresses_array); + if (items) { + uint32_t item; + for (item = 0; item < items; item++) { + plist_t n = plist_array_get_item(addresses_array, item); + char *ip_address = NULL; + plist_get_string_val(n, &ip_address); + // debug(1,ip_address); + strncat(timing_list_message, " ", + sizeof(timing_list_message) - 1 - strlen(timing_list_message)); + strncat(timing_list_message, ip_address, + sizeof(timing_list_message) - 1 - strlen(timing_list_message)); + if (ip_address != NULL) + free(ip_address); + } + ptp_send_control_message_string(timing_list_message); + } + plist_free(addresses_array); +*/ + //set_client_as_ptp_clock(conn); + resp->respcode = 200; +} +#endif + +#ifndef CONFIG_AIRPLAY_2 +void handle_options(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req, + rtsp_message *resp) { + debug_log_rtsp_message(2, "OPTIONS request", req); + debug(3, "Connection %d: OPTIONS", conn->connection_number); + resp->respcode = 200; + msg_add_header(resp, "Public", + "ANNOUNCE, SETUP, RECORD, " + "PAUSE, FLUSH, TEARDOWN, " + "OPTIONS, GET_PARAMETER, SET_PARAMETER"); +} +#endif + +#ifdef CONFIG_AIRPLAY_2 + +void handle_options(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req, + rtsp_message *resp) { + debug_log_rtsp_message(2, "OPTIONS request", req); + debug(3, "Connection %d: OPTIONS", conn->connection_number); + resp->respcode = 200; + msg_add_header(resp, "Public", + "ANNOUNCE, SETUP, RECORD, " + "PAUSE, FLUSH, FLUSHBUFFERED, TEARDOWN, " + "OPTIONS, POST, GET, PUT"); +} + +void teardown_phase_one(rtsp_conn_info *conn) { + // this can be called more than once on the same connection -- + // by the player itself but also by the play session being killed + if (conn->player_thread) { + player_stop(conn); // this nulls the player_thread + activity_monitor_signify_activity(0); // inactive, and should be after command_stop() + } + if (conn->session_key) { + free(conn->session_key); + conn->session_key = NULL; + } +} + +void teardown_phase_two(rtsp_conn_info *conn) { + // we are being asked to disconnect + // this can be called more than once on the same connection -- + // by the player itself but also by the play seesion being killed + debug(2, "Connection %d: TEARDOWN %s connection.", conn->connection_number, + get_category_string(conn->airplay_stream_category)); + if (conn->airplay_stream_category == remote_control_stream) { + if (conn->rtp_data_thread) { + debug(2, "Connection %d: TEARDOWN %s Delete Data Thread.", conn->connection_number, + get_category_string(conn->airplay_stream_category)); + pthread_cancel(*conn->rtp_data_thread); + pthread_join(*conn->rtp_data_thread, NULL); + free(conn->rtp_data_thread); + conn->rtp_data_thread = NULL; + } + if (conn->data_socket) { + debug(2, "Connection %d: TEARDOWN %s Close Data Socket.", conn->connection_number, + get_category_string(conn->airplay_stream_category)); + close(conn->data_socket); + conn->data_socket = 0; + } + } + + if (conn->rtp_event_thread) { + debug(2, "Connection %d: TEARDOWN %s Delete Event Thread.", conn->connection_number, + get_category_string(conn->airplay_stream_category)); + pthread_cancel(*conn->rtp_event_thread); + pthread_join(*conn->rtp_event_thread, NULL); + free(conn->rtp_event_thread); + conn->rtp_event_thread = NULL; + } + if (conn->event_socket) { + debug(2, "Connection %d: TEARDOWN %s Close Event Socket.", conn->connection_number, + get_category_string(conn->airplay_stream_category)); + close(conn->event_socket); + conn->event_socket = 0; + } + + // if we are closing a PTP stream only, do this + if (conn->airplay_stream_category == ptp_stream) { + if (conn->airplay_gid != NULL) { + free(conn->airplay_gid); + conn->airplay_gid = NULL; + +#ifdef CONFIG_METADATA + // this is here to ensure it's only performed once during a teardown of a ptp stream + send_ssnc_metadata('disc', conn->client_ip_string, strlen(conn->client_ip_string), 1); +#endif + } + conn->groupContainsGroupLeader = 0; + config.airplay_statusflags &= (0xffffffff - (1 << 11)); // DeviceSupportsRelay + build_bonjour_strings(conn); + debug(2, "Connection %d: TEARDOWN mdns_update on %s.", conn->connection_number, + get_category_string(conn->airplay_stream_category)); + mdns_update(NULL, secondary_txt_records); + if (conn->dacp_active_remote != NULL) { + free(conn->dacp_active_remote); + conn->dacp_active_remote = NULL; + } + release_play_lock(conn); + clear_ptp_clock(); + } +} + +void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req, + rtsp_message *resp) { + + debug_log_rtsp_message(2, "TEARDOWN: ", req); + // if (have_player(conn)) { + resp->respcode = 200; + msg_add_header(resp, "Connection", "close"); + + plist_t messagePlist = plist_from_rtsp_content(req); + if (messagePlist != NULL) { + // now see if the incoming plist contains a "streams" array + plist_t streams = plist_dict_get_item(messagePlist, "streams"); + + if (streams) { + debug(2, "Connection %d: TEARDOWN %s Close the stream.", conn->connection_number, + get_category_string(conn->airplay_stream_category)); + // we are being asked to close a stream + teardown_phase_one(conn); + plist_free(streams); + debug(2, "Connection %d: TEARDOWN %s Close the stream complete", conn->connection_number, + get_category_string(conn->airplay_stream_category)); + } else { + debug(2, "Connection %d: TEARDOWN %s Close the connection.", conn->connection_number, + get_category_string(conn->airplay_stream_category)); + teardown_phase_one(conn); // try to do phase one anyway + teardown_phase_two(conn); + debug(2, "Connection %d: TEARDOWN %s Close the connection complete", conn->connection_number, + get_category_string(conn->airplay_stream_category)); + } + //} else { + // warn("Connection %d TEARDOWN received without having the player (no ANNOUNCE?)", + // conn->connection_number); + // resp->respcode = 451; + + plist_free(messagePlist); + resp->respcode = 200; + } else { + debug(1, "Connection %d: missing plist!", conn->connection_number); + resp->respcode = 451; // don't know what to do here + } + // debug(1,"Bogus exit for valgrind -- remember to comment it out!."); + // exit(EXIT_SUCCESS); // +} +#endif + +void teardown(rtsp_conn_info *conn) { + player_stop(conn); + activity_monitor_signify_activity(0); // inactive, and should be after command_stop() + if (conn->dacp_active_remote != NULL) { + free(conn->dacp_active_remote); + conn->dacp_active_remote = NULL; + } +} + +void handle_teardown(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req, + rtsp_message *resp) { + debug_log_rtsp_message(2, "TEARDOWN request", req); + debug(2, "Connection %d: TEARDOWN", conn->connection_number); + if (have_play_lock(conn)) { + debug( + 3, + "TEARDOWN: synchronously terminating the player thread of RTSP conversation thread %d (2).", + conn->connection_number); + teardown(conn); + release_play_lock(conn); + resp->respcode = 200; + msg_add_header(resp, "Connection", "close"); + debug(3, "TEARDOWN: successful termination of playing thread of RTSP conversation thread %d.", + conn->connection_number); + } else { + warn("Connection %d TEARDOWN received without having the player (no ANNOUNCE?)", + conn->connection_number); + resp->respcode = 451; + } + // debug(1,"Bogus exit for valgrind -- remember to comment it out!."); + // exit(EXIT_SUCCESS); +} + +void handle_flush(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { + debug_log_rtsp_message(2, "FLUSH request", req); + debug(3, "Connection %d: FLUSH", conn->connection_number); + char *p = NULL; + uint32_t rtptime = 0; + char *hdr = msg_get_header(req, "RTP-Info"); + + if (hdr) { + // debug(1,"FLUSH message received: \"%s\".",hdr); + // get the rtp timestamp + p = strstr(hdr, "rtptime="); + if (p) { + p = strchr(p, '='); + if (p) + rtptime = uatoi(p + 1); // unsigned integer -- up to 2^32-1 + } + } + debug(2, "RTSP Flush Requested: %u.", rtptime); + if (have_play_lock(conn)) { +#ifdef CONFIG_METADATA + if (p) + send_metadata('ssnc', 'flsr', p + 1, strlen(p + 1), req, 1); + else + send_metadata('ssnc', 'flsr', NULL, 0, NULL, 0); +#endif + + player_flush(rtptime, conn); // will not crash even it there is no player thread. + resp->respcode = 200; + + } else { + warn("Connection %d FLUSH %u received without having the player", conn->connection_number, + rtptime); + resp->respcode = 451; + } +} + +#ifdef CONFIG_AIRPLAY_2 + +#ifdef CONFIG_METADATA +static void check_and_send_plist_metadata(plist_t messagePlist, const char *plist_key, + uint32_t metadata_code) { + plist_t item = plist_dict_get_item(messagePlist, plist_key); + if (item) { + char *value; + plist_get_string_val(item, &value); + if (value != NULL) { + send_metadata('ssnc', metadata_code, value, strlen(value), NULL, 0); + free(value); + } + } +} +#endif + +void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { + int err; + debug(1, "Connection %d: SETUP (AirPlay 2)", conn->connection_number); + debug_log_rtsp_message(1, "SETUP (AirPlay 2) SETUP incoming message", req); + + plist_t messagePlist = plist_from_rtsp_content(req); + plist_t setupResponsePlist = plist_new_dict(); + resp->respcode = 400; + + // see if we can get a name for the client + char *clientNameString = NULL; + plist_t nameItem = plist_dict_get_item(messagePlist, "name"); + if (nameItem != NULL) { + plist_get_string_val(nameItem, &clientNameString); + } else { + clientNameString = strdup(""); + } + + // see if the incoming plist contains a "streams" array + plist_t streams = plist_dict_get_item(messagePlist, "streams"); + if (streams == NULL) { + // no "streams" plist, so it must (?) be an initial setup + debug(2, + "Connection %d SETUP: No \"streams\" array has been found -- create an event thread " + "and open a TCP port.", + conn->connection_number); + conn->airplay_stream_category = unspecified_stream_category; + // figure out what category of stream it is, by looking at the plist + plist_t timingProtocol = plist_dict_get_item(messagePlist, "timingProtocol"); + if (timingProtocol != NULL) { + char *timingProtocolString = NULL; + plist_get_string_val(timingProtocol, &timingProtocolString); + if (timingProtocolString) { + if (strcmp(timingProtocolString, "PTP") == 0) { + debug(1, "Connection %d: AP2 PTP connection from %s:%u (\"%s\") to self at %s:%u.", + conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, clientNameString, + conn->self_ip_string, conn->self_rtsp_port); + conn->airplay_stream_category = ptp_stream; + conn->timing_type = ts_ptp; +#ifdef CONFIG_METADATA + send_ssnc_metadata('conn', conn->client_ip_string, strlen(conn->client_ip_string), + 1); // before disconnecting an existing play +#endif + get_play_lock(conn, 1); // airplay 2 always allows interruption +#ifdef CONFIG_METADATA + send_ssnc_metadata('clip', conn->client_ip_string, strlen(conn->client_ip_string), 1); + send_ssnc_metadata('svip', conn->self_ip_string, strlen(conn->self_ip_string), 1); +#endif + } else if (strcmp(timingProtocolString, "NTP") == 0) { + debug(1, "Connection %d: SETUP: NTP setup from %s:%u (\"%s\") to self at %s:%u.", + conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, clientNameString, + conn->self_ip_string, conn->self_rtsp_port); + conn->airplay_stream_category = ntp_stream; + conn->timing_type = ts_ntp; + } else if (strcmp(timingProtocolString, "None") == 0) { + debug(3, "Connection %d: SETUP: a \"None\" setup detected from %s:%u (\"%s\") to self at %s:%u.", + conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, clientNameString, + conn->self_ip_string, conn->self_rtsp_port); + // now check to see if it's got the "isRemoteControlOnly" item and check it's true + plist_t isRemoteControlOnly = plist_dict_get_item(messagePlist, "isRemoteControlOnly"); + if (isRemoteControlOnly != NULL) { + uint8_t isRemoteControlOnlyBoolean = 0; + plist_get_bool_val(isRemoteControlOnly, &isRemoteControlOnlyBoolean); + if (isRemoteControlOnlyBoolean != 0) { + debug(1, "Connection %d: Remote Control connection from %s:%u (\"%s\") to self at %s:%u.", + conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, clientNameString, + conn->self_ip_string, conn->self_rtsp_port); + conn->airplay_stream_category = remote_control_stream; + } else { + debug(1, + "Connection %d: SETUP: a \"None\" setup detected, with " + "\"isRemoteControlOnly\" item set to \"false\".", + conn->connection_number); + } + } else { + debug(1, + "Connection %d: SETUP: a \"None\" setup detected, but no " + "\"isRemoteControlOnly\" item detected.", + conn->connection_number); + } + } + + // here, we know it's an initial setup and we know the kind of setup being requested + // if it's a full service PTP stream, we get groupUUID, groupContainsGroupLeader and + // timingPeerList + if (conn->airplay_stream_category == ptp_stream) { + if (ptp_shm_interface_open() != + 0) // it should be open already, but just in case it isn't... + die("Can not access the NQPTP service. Has it stopped running?"); + // clear_ptp_clock(); + debug_log_rtsp_message(2, "SETUP \"PTP\" message", req); + plist_t groupUUID = plist_dict_get_item(messagePlist, "groupUUID"); + if (groupUUID) { + char *gid = NULL; + plist_get_string_val(groupUUID, &gid); + if (gid) { + if (conn->airplay_gid) + free(conn->airplay_gid); + conn->airplay_gid = gid; // it'll be free'd later on... + } else { + debug(1, "Invalid groupUUID"); + } + } else { + debug(1, "No groupUUID in SETUP"); + } + + // now see if the group contains a group leader + plist_t groupContainsGroupLeader = + plist_dict_get_item(messagePlist, "groupContainsGroupLeader"); + if (groupContainsGroupLeader) { + uint8_t value = 0; + plist_get_bool_val(groupContainsGroupLeader, &value); + conn->groupContainsGroupLeader = value; + debug(2, "Updated groupContainsGroupLeader to %u", conn->groupContainsGroupLeader); + } else { + debug(1, "No groupContainsGroupLeader in SETUP"); + } + + char timing_list_message[4096]; + timing_list_message[0] = 'T'; + timing_list_message[1] = 0; + + // ensure the client itself is first -- it's okay if it's duplicated later + strncat(timing_list_message, " ", + sizeof(timing_list_message) - 1 - strlen(timing_list_message)); + strncat(timing_list_message, (const char *)&conn->client_ip_string, + sizeof(timing_list_message) - 1 - strlen(timing_list_message)); + + plist_t timing_peer_info = plist_dict_get_item(messagePlist, "timingPeerInfo"); + if (timing_peer_info) { + // first, get the incoming plist. + plist_t addresses_array = plist_dict_get_item(timing_peer_info, "Addresses"); + if (addresses_array) { + // iterate through the array of items + uint32_t items = plist_array_get_size(addresses_array); + if (items) { + uint32_t item; + for (item = 0; item < items; item++) { + plist_t n = plist_array_get_item(addresses_array, item); + char *ip_address = NULL; + plist_get_string_val(n, &ip_address); + // debug(1, "Timing peer: %s", ip_address); + strncat(timing_list_message, " ", + sizeof(timing_list_message) - 1 - strlen(timing_list_message)); + strncat(timing_list_message, ip_address, + sizeof(timing_list_message) - 1 - strlen(timing_list_message)); + free(ip_address); + } + } else { + debug(1, "SETUP on Connection %d: No timingPeerInfo addresses in the array.", + conn->connection_number); + } + } else { + debug(1, "SETUP on Connection %d: Can't find timingPeerInfo addresses", + conn->connection_number); + } + // make up the timing peer info list part of the response... + // debug(1,"Create timingPeerInfoPlist"); + plist_t timingPeerInfoPlist = plist_new_dict(); + plist_t addresses = plist_new_array(); // to hold the device's interfaces + plist_array_append_item(addresses, plist_new_string(conn->self_ip_string)); + // debug(1,"self ip: \"%s\"", conn->self_ip_string); + + struct ifaddrs *addrs, *iap; + getifaddrs(&addrs); + for (iap = addrs; iap != NULL; iap = iap->ifa_next) { + // debug(1, "Interface index %d, name: \"%s\"",if_nametoindex(iap->ifa_name), + // iap->ifa_name); + if ((iap->ifa_addr) && (iap->ifa_netmask) && (iap->ifa_flags & IFF_UP) && + ((iap->ifa_flags & IFF_LOOPBACK) == 0)) { + char buf[INET6_ADDRSTRLEN + 1]; // +1 for a NUL + memset(buf, 0, sizeof(buf)); + if (iap->ifa_addr->sa_family == AF_INET6) { + struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)(iap->ifa_addr); + inet_ntop(AF_INET6, (void *)&addr6->sin6_addr, buf, sizeof(buf)); + plist_array_append_item(addresses, plist_new_string(buf)); + // debug(1, "Own address IPv6: %s", buf); + + // strncat(timing_list_message, " ", + // sizeof(timing_list_message) - 1 - strlen(timing_list_message)); + // strncat(timing_list_message, buf, + // sizeof(timing_list_message) - 1 - strlen(timing_list_message)); + + } else { + struct sockaddr_in *addr = (struct sockaddr_in *)(iap->ifa_addr); + inet_ntop(AF_INET, (void *)&addr->sin_addr, buf, sizeof(buf)); + plist_array_append_item(addresses, plist_new_string(buf)); + // debug(1, "Own address IPv4: %s", buf); + + // strncat(timing_list_message, " ", + // sizeof(timing_list_message) - 1 - strlen(timing_list_message)); + // strncat(timing_list_message, buf, + // sizeof(timing_list_message) - 1 - strlen(timing_list_message)); + } + } + } + freeifaddrs(addrs); + + // debug(1,"initial timing peer command: \"%s\".", timing_list_message); + //ptp_send_control_message_string(timing_list_message); + set_client_as_ptp_clock(conn); + plist_dict_set_item(timingPeerInfoPlist, "Addresses", addresses); + plist_dict_set_item(timingPeerInfoPlist, "ID", plist_new_string(conn->self_ip_string)); + plist_dict_set_item(setupResponsePlist, "timingPeerInfo", timingPeerInfoPlist); + // get a port to use as an event port + // bind a new TCP port and get a socket + conn->local_event_port = 0; // any port + int err = bind_socket_and_port(SOCK_STREAM, conn->connection_ip_family, + conn->self_ip_string, conn->self_scope_id, + &conn->local_event_port, &conn->event_socket); + if (err) { + die("SETUP on Connection %d: Error %d: could not find a TCP port to use as an event " + "port", + conn->connection_number, err); + } + + debug(2, "Connection %d: TCP PTP event port opened: %u.", conn->connection_number, + conn->local_event_port); + + if (conn->rtp_event_thread != NULL) + debug(1, "previous rtp_event_thread allocation not freed, it seems."); + conn->rtp_event_thread = malloc(sizeof(pthread_t)); + if (conn->rtp_event_thread == NULL) + die("Couldn't allocate space for pthread_t"); + + pthread_create(conn->rtp_event_thread, NULL, &rtp_event_receiver, (void *)conn); + + plist_dict_set_item(setupResponsePlist, "eventPort", + plist_new_uint(conn->local_event_port)); + plist_dict_set_item(setupResponsePlist, "timingPort", plist_new_uint(0)); // dummy + cancel_all_RTSP_threads(unspecified_stream_category, + conn->connection_number); // kill all the other listeners + + config.airplay_statusflags |= 1 << 11; // DeviceSupportsRelay + build_bonjour_strings(conn); + debug(2, "Connection %d: SETUP mdns_update on %s.", conn->connection_number, + get_category_string(conn->airplay_stream_category)); + mdns_update(NULL, secondary_txt_records); + + resp->respcode = 200; + } else { + debug(1, "SETUP on Connection %d: PTP setup -- no timingPeerInfo plist.", + conn->connection_number); + } + +#ifdef CONFIG_METADATA + check_and_send_plist_metadata(messagePlist, "name", 'snam'); + check_and_send_plist_metadata(messagePlist, "deviceID", 'cdid'); + check_and_send_plist_metadata(messagePlist, "model", 'cmod'); + check_and_send_plist_metadata(messagePlist, "macAddress", 'cmac'); +#endif + } else if (conn->airplay_stream_category == ntp_stream) { + debug(1, "SETUP on Connection %d: ntp stream handling is not implemented!", + conn->connection_number, req); + warn("Shairport Sync can not handle NTP streams."); + } else if (conn->airplay_stream_category == remote_control_stream) { + debug_log_rtsp_message(2, "SETUP (no stream) \"isRemoteControlOnly\" message", req); + + // get a port to use as an event port + // bind a new TCP port and get a socket + conn->local_event_port = 0; // any port + int err = bind_socket_and_port(SOCK_STREAM, conn->connection_ip_family, + conn->self_ip_string, conn->self_scope_id, + &conn->local_event_port, &conn->event_socket); + if (err) { + die("SETUP on Connection %d: Error %d: could not find a TCP port to use as an event " + "port", + conn->connection_number, err); + } + + debug(2, "Connection %d SETUP (RC): TCP Remote Control event port opened: %u.", + conn->connection_number, conn->local_event_port); + if (conn->rtp_event_thread != NULL) + debug(1, + "Connection %d SETUP (RC): previous rtp_event_thread allocation not freed, it " + "seems.", + conn->connection_number); + + conn->rtp_event_thread = malloc(sizeof(pthread_t)); + if (conn->rtp_event_thread == NULL) + die("Couldn't allocate space for pthread_t"); + pthread_create(conn->rtp_event_thread, NULL, &rtp_event_receiver, (void *)conn); + plist_dict_set_item(setupResponsePlist, "eventPort", + plist_new_uint(conn->local_event_port)); + plist_dict_set_item(setupResponsePlist, "timingPort", plist_new_uint(0)); + cancel_all_RTSP_threads( + remote_control_stream, + conn->connection_number); // kill all the other remote control listeners + resp->respcode = 200; + } else { + debug(1, "SETUP on Connection %d: an unrecognised \"%s\" setup detected.", + conn->connection_number, timingProtocolString); + warn("Shairport Sync can not handle streams of this type: \"%s\".", timingProtocolString); + } + free(timingProtocolString); + } else { + debug(1, "SETUP on Connection %d: Can't retrieve timingProtocol string in initial SETUP.", + conn->connection_number); + } + } else { + debug(1, + "SETUP on Connection %d: Unrecognised SETUP incoming message from \"%s\": no " + "timingProtocol or streams plist found.", + conn->connection_number, (const char *)conn->client_ip_string); + debug_log_rtsp_message(2, "Unrecognised SETUP incoming message.", req); + warn("Unrecognised SETUP incoming message -- ignored."); + } + } else { + debug(2, "Connection %d: SETUP on %s. A \"streams\" array has been found", + conn->connection_number, get_category_string(conn->airplay_stream_category)); + if (conn->airplay_stream_category == ptp_stream) { + // get stream[0] + plist_t stream0 = plist_array_get_item(streams, 0); + + plist_t streams_array = plist_new_array(); // to hold the ports and stuff + plist_t stream0dict = plist_new_dict(); + + // get the session key -- it must have one + + plist_t item = plist_dict_get_item(stream0, "shk"); // session key + uint64_t item_value = 0; // the length + plist_get_data_val(item, (char **)&conn->session_key, &item_value); + + // more stuff + // set up a UDP control stream and thread and a UDP or TCP audio stream and thread + + // bind a new UDP port and get a socket + conn->local_ap2_control_port = 0; // any port + err = bind_socket_and_port(SOCK_DGRAM, conn->connection_ip_family, conn->self_ip_string, + conn->self_scope_id, &conn->local_ap2_control_port, + &conn->ap2_control_socket); + if (err) { + die("Error %d: could not find a UDP port to use as an ap2_control port", err); + } + debug(2, "Connection %d: UDP control port opened: %u.", conn->connection_number, + conn->local_ap2_control_port); + + pthread_create(&conn->rtp_ap2_control_thread, NULL, &rtp_ap2_control_receiver, (void *)conn); + + // get the DACP-ID and Active Remote for remote control stuff + + char *ar = msg_get_header(req, "Active-Remote"); + if (ar) { + debug(3, "Connection %d: SETUP AP2 -- Active-Remote string seen: \"%s\".", + conn->connection_number, ar); + // get the active remote + if (conn->dacp_active_remote) // this is in case SETUP was previously called + free(conn->dacp_active_remote); + conn->dacp_active_remote = strdup(ar); +#ifdef CONFIG_METADATA + send_metadata('ssnc', 'acre', ar, strlen(ar), req, 1); +#endif + } else { + debug(1, "Connection %d: SETUP AP2 no Active-Remote information the SETUP Record.", + conn->connection_number); + if (conn->dacp_active_remote) { // this is in case SETUP was previously called + free(conn->dacp_active_remote); + conn->dacp_active_remote = NULL; + } + } + + ar = msg_get_header(req, "DACP-ID"); + if (ar) { + debug(3, "Connection %d: SETUP AP2 -- DACP-ID string seen: \"%s\".", + conn->connection_number, ar); + if (conn->dacp_id) // this is in case SETUP was previously called + free(conn->dacp_id); + conn->dacp_id = strdup(ar); +#ifdef CONFIG_METADATA + send_metadata('ssnc', 'daid', ar, strlen(ar), req, 1); +#endif + } else { + debug(1, "Connection %d: SETUP AP2 doesn't include DACP-ID string information.", + conn->connection_number); + if (conn->dacp_id) { // this is in case SETUP was previously called + free(conn->dacp_id); + conn->dacp_id = NULL; + } + } + + // now, get the type of the stream. + item = plist_dict_get_item(stream0, "type"); + item_value = 0; + plist_get_uint_val(item, &item_value); + + switch (item_value) { + case 96: { + debug(1, "Connection %d. AP2 Realtime Audio Stream.", conn->connection_number); + debug_log_rtsp_message(2, "Realtime Audio Stream SETUP incoming message", req); + // get_play_lock(conn); + conn->airplay_stream_type = realtime_stream; + // bind a new UDP port and get a socket + conn->local_realtime_audio_port = 0; // any port + err = bind_socket_and_port(SOCK_DGRAM, conn->connection_ip_family, conn->self_ip_string, + conn->self_scope_id, &conn->local_realtime_audio_port, + &conn->realtime_audio_socket); + if (err) { + die("Error %d: could not find a UDP port to use as a realtime_audio port", err); + } + debug(2, "Connection %d: UDP realtime audio port opened: %u.", conn->connection_number, + conn->local_realtime_audio_port); + + pthread_create(&conn->rtp_realtime_audio_thread, NULL, &rtp_realtime_audio_receiver, + (void *)conn); + + plist_dict_set_item(stream0dict, "type", plist_new_uint(96)); + plist_dict_set_item(stream0dict, "dataPort", + plist_new_uint(conn->local_realtime_audio_port)); + + conn->stream.type = ast_apple_lossless; + debug(3, "An ALAC stream has been detected."); + + // Set reasonable connection defaults + conn->stream.fmtp[0] = 96; + conn->stream.fmtp[1] = 352; + conn->stream.fmtp[2] = 0; + conn->stream.fmtp[3] = 16; + conn->stream.fmtp[4] = 40; + conn->stream.fmtp[5] = 10; + conn->stream.fmtp[6] = 14; + conn->stream.fmtp[7] = 2; + conn->stream.fmtp[8] = 255; + conn->stream.fmtp[9] = 0; + conn->stream.fmtp[10] = 0; + conn->stream.fmtp[11] = 44100; + + // set the parameters of the player (as distinct from the parameters of the decoder -- + // that's done later). + conn->max_frames_per_packet = conn->stream.fmtp[1]; // number of audio frames per packet. + conn->input_rate = conn->stream.fmtp[11]; + conn->input_num_channels = conn->stream.fmtp[7]; + conn->input_bit_depth = conn->stream.fmtp[3]; + conn->input_bytes_per_frame = conn->input_num_channels * ((conn->input_bit_depth + 7) / 8); + debug(2, "Realtime Stream Play"); + activity_monitor_signify_activity(1); + player_prepare_to_play(conn); + player_play(conn); + + conn->rtp_running = 1; // hack! + } break; + case 103: { + debug(1, "Connection %d. AP2 Buffered Audio Stream.", conn->connection_number); + debug_log_rtsp_message(2, "Buffered Audio Stream SETUP incoming message", req); + // get_play_lock(conn); + conn->airplay_stream_type = buffered_stream; + // get needed stuff + + // bind a new TCP port and get a socket + conn->local_buffered_audio_port = 0; // any port + err = bind_socket_and_port(SOCK_STREAM, conn->connection_ip_family, conn->self_ip_string, + conn->self_scope_id, &conn->local_buffered_audio_port, + &conn->buffered_audio_socket); + if (err) { + die("SETUP on Connection %d: Error %d: could not find a TCP port to use as a " + "buffered_audio port", + conn->connection_number, err); + } + + debug(2, "Connection %d: TCP Buffered Audio port opened: %u.", conn->connection_number, + conn->local_buffered_audio_port); + + // hack. + conn->max_frames_per_packet = 352; // number of audio frames per packet. + conn->input_rate = 44100; // we are stuck with this for the moment. + conn->input_num_channels = 2; + conn->input_bit_depth = 16; + conn->input_bytes_per_frame = conn->input_num_channels * ((conn->input_bit_depth + 7) / 8); + activity_monitor_signify_activity(1); + player_prepare_to_play( + conn); // get capabilities of DAC before creating the buffered audio thread + + pthread_create(&conn->rtp_buffered_audio_thread, NULL, &rtp_buffered_audio_processor, + (void *)conn); + + plist_dict_set_item(stream0dict, "type", plist_new_uint(103)); + plist_dict_set_item(stream0dict, "dataPort", + plist_new_uint(conn->local_buffered_audio_port)); + plist_dict_set_item(stream0dict, "audioBufferSize", + plist_new_uint(conn->ap2_audio_buffer_size)); + + // this should be cancelled by an activity_monitor_signify_activity(1) + // call in the SETRATEANCHORI handler, which should come up right away + activity_monitor_signify_activity(0); + player_play(conn); + + conn->rtp_running = 1; // hack! + } break; + default: + debug(1, "SETUP on Connection %d: Unhandled stream type %" PRIu64 ".", + conn->connection_number, item_value); + debug_log_rtsp_message(1, "Unhandled stream type incoming message", req); + } + + plist_dict_set_item(stream0dict, "controlPort", plist_new_uint(conn->local_ap2_control_port)); + + plist_array_append_item(streams_array, stream0dict); + plist_dict_set_item(setupResponsePlist, "streams", streams_array); + resp->respcode = 200; + + } else if (conn->airplay_stream_category == remote_control_stream) { + debug(2, "Connection %d (RC): SETUP: Remote Control Stream received from %s.", + conn->connection_number, conn->client_ip_string); + debug_log_rtsp_message(2, "Remote Control Stream SETUP incoming message", req); + /* + // get a port to use as an data port + // bind a new TCP port and get a socket + conn->local_data_port = 0; // any port + int err = + bind_socket_and_port(SOCK_STREAM, conn->connection_ip_family, conn->self_ip_string, + conn->self_scope_id, &conn->local_data_port, &conn->data_socket); + if (err) { + die("SETUP on Connection %d (RC): Error %d: could not find a TCP port to use as a data " + "port", + conn->connection_number, err); + } + + debug(1, "Connection %d SETUP (RC): TCP Remote Control data port opened: %u.", + conn->connection_number, conn->local_data_port); + if (conn->rtp_data_thread != NULL) + debug(1, "Connection %d SETUP (RC): previous rtp_data_thread allocation not freed, it + seems.", conn->connection_number); conn->rtp_data_thread = malloc(sizeof(pthread_t)); if + (conn->rtp_data_thread == NULL) die("Couldn't allocate space for pthread_t"); + + pthread_create(conn->rtp_data_thread, NULL, &rtp_data_receiver, (void *)conn); + + plist_t coreResponseDict = plist_new_dict(); + plist_dict_set_item(coreResponseDict, "streamID", plist_new_uint(1)); + plist_dict_set_item(coreResponseDict, "type", plist_new_uint(130)); + plist_dict_set_item(coreResponseDict, "dataPort", plist_new_uint(conn->local_data_port)); + + plist_t coreResponseArray = plist_new_array(); + plist_array_append_item(coreResponseArray, coreResponseDict); + plist_dict_set_item(setupResponsePlist, "streams", coreResponseArray); + */ + resp->respcode = 200; + } else { + debug(1, "Connection %d: SETUP: Stream received but no airplay category set. Nothing done.", + conn->connection_number); + } + } + + if (resp->respcode == 200) { + plist_to_bin(setupResponsePlist, &resp->content, &resp->contentlength); + plist_free(setupResponsePlist); + msg_add_header(resp, "Content-Type", "application/x-apple-binary-plist"); + } + plist_free(messagePlist); + if (clientNameString != NULL) + free(clientNameString); + debug_log_rtsp_message(2, " SETUP response", resp); +} +#endif + +void handle_setup(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { + debug(3, "Connection %d: SETUP", conn->connection_number); + resp->respcode = 451; // invalid arguments -- expect them + if (have_play_lock(conn)) { + uint16_t cport, tport; + char *ar = msg_get_header(req, "Active-Remote"); + if (ar) { + debug(2, "Connection %d: SETUP: Active-Remote string seen: \"%s\".", conn->connection_number, + ar); + // get the active remote + if (conn->dacp_active_remote) // this is in case SETUP was previously called + free(conn->dacp_active_remote); + conn->dacp_active_remote = strdup(ar); +#ifdef CONFIG_METADATA + send_metadata('ssnc', 'acre', ar, strlen(ar), req, 1); +#endif + } else { + debug(2, "Connection %d: SETUP: Note: no Active-Remote information seen.", + conn->connection_number); + if (conn->dacp_active_remote) { // this is in case SETUP was previously called + free(conn->dacp_active_remote); + conn->dacp_active_remote = NULL; + } + } + + ar = msg_get_header(req, "DACP-ID"); + if (ar) { + debug(2, "Connection %d: SETUP: DACP-ID string seen: \"%s\".", conn->connection_number, ar); + if (conn->dacp_id) // this is in case SETUP was previously called + free(conn->dacp_id); + conn->dacp_id = strdup(ar); +#ifdef CONFIG_METADATA + send_metadata('ssnc', 'daid', ar, strlen(ar), req, 1); +#endif + } else { + debug(2, "Connection %d: SETUP doesn't include DACP-ID string information.", + conn->connection_number); + if (conn->dacp_id) { // this is in case SETUP was previously called + free(conn->dacp_id); + conn->dacp_id = NULL; + } + } + + char *hdr = msg_get_header(req, "Transport"); + if (hdr) { + char *p; + p = strstr(hdr, "control_port="); + if (p) { + p = strchr(p, '=') + 1; + cport = atoi(p); + + p = strstr(hdr, "timing_port="); + if (p) { + p = strchr(p, '=') + 1; + tport = atoi(p); + + if (conn->rtp_running) { + if ((conn->remote_control_port != cport) || (conn->remote_timing_port != tport)) { + warn("Connection %d: Duplicate SETUP message with different control (old %u, new %u) " + "or " + "timing (old %u, new " + "%u) ports! This is probably fatal!", + conn->connection_number, conn->remote_control_port, cport, + conn->remote_timing_port, tport); + } else { + warn("Connection %d: Duplicate SETUP message with the same control (%u) and timing " + "(%u) " + "ports. This is " + "probably not fatal.", + conn->connection_number, conn->remote_control_port, conn->remote_timing_port); + } + } else { + rtp_setup(&conn->local, &conn->remote, cport, tport, conn); + } + if (conn->local_audio_port != 0) { + + char resphdr[256] = ""; + snprintf(resphdr, sizeof(resphdr), + "RTP/AVP/" + "UDP;unicast;interleaved=0-1;mode=record;control_port=%d;" + "timing_port=%d;server_" + "port=%d", + conn->local_control_port, conn->local_timing_port, conn->local_audio_port); + + msg_add_header(resp, "Transport", resphdr); + + msg_add_header(resp, "Session", "1"); + + resp->respcode = 200; // it all worked out okay + debug(2, + "Connection %d: SETUP DACP-ID \"%s\" from %s to %s with UDP ports Control: " + "%d, Timing: %d and Audio: %d.", + conn->connection_number, conn->dacp_id, &conn->client_ip_string, + &conn->self_ip_string, conn->local_control_port, conn->local_timing_port, + conn->local_audio_port); + + } else { + debug(1, "Connection %d: SETUP seems to specify a null audio port.", + conn->connection_number); + } + } else { + debug(1, "Connection %d: SETUP doesn't specify a timing_port.", conn->connection_number); + } + } else { + debug(1, "Connection %d: SETUP doesn't specify a control_port.", conn->connection_number); + } + } else { + debug(1, "Connection %d: SETUP doesn't contain a Transport header.", conn->connection_number); + } + if (resp->respcode == 200) { +#ifdef CONFIG_METADATA + send_ssnc_metadata('clip', conn->client_ip_string, strlen(conn->client_ip_string), 1); + send_ssnc_metadata('svip', conn->self_ip_string, strlen(conn->self_ip_string), 1); +#endif + } else { + debug(1, "Connection %d: SETUP error -- releasing the player lock.", conn->connection_number); + release_play_lock(conn); + } + + } else { + warn("Connection %d SETUP received without having the player (no ANNOUNCE?)", + conn->connection_number); + } +} + +/* +static void handle_ignore(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { + debug(1, "Connection thread %d: IGNORE", conn->connection_number); + resp->respcode = 200; +} +*/ + +void handle_set_parameter_parameter(rtsp_conn_info *conn, rtsp_message *req, + __attribute__((unused)) rtsp_message *resp) { + + char *cp = req->content; + int cp_left = req->contentlength; + /* + int k = cp_left; + if (k>max_bytes) + k = max_bytes; + for (i = 0; i < k; i++) + snprintf((char *)buf + 2 * i, 3, "%02x", cp[i]); + debug(1, "handle_set_parameter_parameter: \"%s\".",buf); + */ + + char *next; + while (cp_left && cp) { + next = nextline(cp, cp_left); + // note: "next" will return NULL if there is no \r or \n or \r\n at the end of this + // but we are always guaranteed that if cp is not null, it will be pointing to something + // NUL-terminated + + if (next) + cp_left -= (next - cp); + else + cp_left = 0; + + if (!strncmp(cp, "volume: ", strlen("volume: "))) { + float volume = atof(cp + strlen("volume: ")); + debug(2, "Connection %d: request to set AirPlay Volume to: %f.", conn->connection_number, + volume); + // if we are playing, go ahead and change the volume +#ifdef CONFIG_DBUS_INTERFACE + if (dbus_service_is_running()) { + shairport_sync_set_volume(shairportSyncSkeleton, volume); + } else { +#endif + lock_player(); + if (playing_conn == conn) + player_volume(volume, conn); + unlock_player(); +#ifdef CONFIG_DBUS_INTERFACE + } +#endif + } else if (strncmp(cp, "progress: ", strlen("progress: ")) == + 0) { // this can be sent even when metadata is not solicited + +#ifdef CONFIG_METADATA + char *progress = cp + strlen("progress: "); + // debug(2, "progress: \"%s\"",progress); // rtpstampstart/rtpstampnow/rtpstampend 44100 per + // second + send_ssnc_metadata('prgr', progress, strlen(progress), 1); +#endif + + } else { + debug(1, "Connection %d, unrecognised parameter: \"%s\" (%d)\n", conn->connection_number, cp, + strlen(cp)); + } + cp = next; + } +} + +#ifdef CONFIG_METADATA +// Metadata is not used by shairport-sync. +// Instead we send all metadata to a fifo pipe, so that other apps can listen to +// the pipe and use the metadata. + +// We use two 4-character codes to identify each piece of data and we send the +// data itself, if any, +// in base64 form. + +// The first 4-character code, called the "type", is either: +// 'core' for all the regular metadadata coming from iTunes, etc., or +// 'ssnc' (for 'shairport-sync') for all metadata coming from Shairport Sync +// itself, such as +// start/end delimiters, etc. + +// For 'core' metadata, the second 4-character code is the 4-character metadata +// code coming from +// iTunes etc. +// For 'ssnc' metadata, the second 4-character code is used to distinguish the +// messages. + +// Cover art is not tagged in the same way as other metadata, it seems, so is +// sent as an 'ssnc' type +// metadata message with the code 'PICT' +// Here are the 'ssnc' codes defined so far: +// 'PICT' -- the payload is a picture, either a JPEG or a PNG. Check the +// first few bytes to see +// which. +// 'abeg' -- active mode entered. No arguments +// 'aend' -- active mode exited. No arguments +// 'pbeg' -- play stream begin. No arguments +// 'pend' -- play stream end. No arguments +// 'pfls' -- play stream flush. The argument is an unsigned 32-bit +// frame number. It seems that all frames up to but not +// including this frame are to be flushed. +// +// 'prsm' -- play stream resume. No arguments. (deprecated) +// 'paus' -- buffered audio stream paused. No arguments. +// 'pres' -- buffered audio stream resumed. No arguments. +// 'pffr' -- the first frame of a play session has been received and has been validly +// timed. +// 'pvol' -- play volume. The volume is sent as a string -- +// "airplay_volume,volume,lowest_volume,highest_volume" +// volume, lowest_volume and highest_volume are given in dB. +// The "airplay_volume" is what's sent to the player, and is from +// 0.00 down to -30.00, +// with -144.00 meaning mute. +// This is linear on the volume control slider of iTunes or iOS +// AirPlay. +// 'prgr' -- progress -- this is metadata from AirPlay consisting of RTP +// timestamps for the start +// of the current play sequence, the current play point and the end of the +// play sequence. +// I guess the timestamps wrap at 2^32. +// 'mdst' -- a sequence of metadata is about to start; will have, as data, +// the rtptime associated with the metadata, if available +// 'mden' -- a sequence of metadata has ended; will have, as data, the +// rtptime associated with the metadata, if available +// 'pcst' -- a picture is about to be sent; will have, as data, the rtptime +// associated with the picture, if available +// 'pcen' -- a picture has been sent; will have, as data, the rtptime +// associated with the metadata, if available +// 'snam' -- A device -- e.g. "Joe's iPhone" -- has opened a play session. +// Specifically, it's the "X-Apple-Client-Name" string +// 'snua' -- A "user agent" -- e.g. "iTunes/12..." -- has opened a play +// session. Specifically, it's the "User-Agent" string +// The next two two tokens are to facilitate remote control of the source. +// There is some information at http://nto.github.io/AirPlay.html about +// remote control of the source. +// +// 'daid' -- this is the source's DACP-ID (if it has one -- it's not +// guaranteed), useful if you want to remotely control the source. Use this +// string to identify the source's remote control on the network. +// 'acre' -- this is the source's Active-Remote token, necessary if you want +// to send commands to the source's remote control (if it has one). +// `clip` -- the payload is the IP number of the client, i.e. the sender of audio. +// Can be an IPv4 or an IPv6 number. In AirPlay 2 operation, it is sent as soon +// as the client has exclusive access to the player and after any existing +// play session has been interrupted and terminated. +// `conn` -- the payload is the IP number of the client, i.e. the sender of audio. +// Can be an IPv4 or an IPv6 number. This is an AirPlay-2-only message. +// It is sent as soon as the client requests access to the player. +// If Shairport Sync is already playing, this message is sent before the current +// play session is stopped. +// `svip` -- the payload is the IP number of the server, i.e. the player itself. +// Can be an IPv4 or an IPv6 number. +// `disc` -- the payload is the IP number of the client, i.e. the sender of audio. +// Can be an IPv4 or an IPv6 number. This is an AirPlay-2-only message. +// It is sent when a client has been disconnected. +// `dapo` -- the payload is the port number (as text) on the server to which remote +// control commands should be sent. It is 3689 for iTunes but varies for iOS devices. +// `` + +// A special sub-protocol is used for sending large data items over UDP +// If the payload exceeded 4 MB, it is chunked using the following format: +// "ssnc", "chnk", packet_ix, packet_counts, packet_tag, packet_type, chunked_data. +// Notice that the number of items is different to the standard + +// including a simple base64 encoder to minimise malloc/free activity + +// From Stack Overflow, with thanks: +// http://stackoverflow.com/questions/342409/how-do-i-base64-encode-decode-in-c +// minor mods to make independent of C99. +// more significant changes make it not malloc memory +// needs to initialise the docoding table first + +// add _so to end of name to avoid confusion with polarssl's implementation + +static char encoding_table[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'}; + +static size_t mod_table[] = {0, 2, 1}; + +// pass in a pointer to the data, its length, a pointer to the output buffer and +// a pointer to an int +// containing its maximum length +// the actual length will be returned. + +char *base64_encode_so(const unsigned char *data, size_t input_length, char *encoded_data, + size_t *output_length) { + + size_t calculated_output_length = 4 * ((input_length + 2) / 3); + if (calculated_output_length > *output_length) + return (NULL); + *output_length = calculated_output_length; + + size_t i, j; + for (i = 0, j = 0; i < input_length;) { + + uint32_t octet_a = i < input_length ? (unsigned char)data[i++] : 0; + uint32_t octet_b = i < input_length ? (unsigned char)data[i++] : 0; + uint32_t octet_c = i < input_length ? (unsigned char)data[i++] : 0; + + uint32_t triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c; + + encoded_data[j++] = encoding_table[(triple >> 3 * 6) & 0x3F]; + encoded_data[j++] = encoding_table[(triple >> 2 * 6) & 0x3F]; + encoded_data[j++] = encoding_table[(triple >> 1 * 6) & 0x3F]; + encoded_data[j++] = encoding_table[(triple >> 0 * 6) & 0x3F]; + } + + for (i = 0; i < mod_table[input_length % 3]; i++) + encoded_data[*output_length - 1 - i] = '='; + + return encoded_data; +} + +// with thanks! +// + +static int fd = -1; +// static int dirty = 0; + +pc_queue metadata_queue; +#define metadata_queue_size 500 +metadata_package metadata_queue_items[metadata_queue_size]; +pthread_t metadata_thread; + +#ifdef CONFIG_METADATA_HUB +pc_queue metadata_hub_queue; +#define metadata_hub_queue_size 500 +metadata_package metadata_hub_queue_items[metadata_hub_queue_size]; +pthread_t metadata_hub_thread; +#endif + +#ifdef CONFIG_MQTT +pc_queue metadata_mqtt_queue; +#define metadata_mqtt_queue_size 500 +metadata_package metadata_mqtt_queue_items[metadata_mqtt_queue_size]; +pthread_t metadata_mqtt_thread; +#endif + +static int metadata_sock = -1; +static struct sockaddr_in metadata_sockaddr; +static char *metadata_sockmsg; +pc_queue metadata_multicast_queue; +#define metadata_multicast_queue_size 500 +metadata_package metadata_multicast_queue_items[metadata_queue_size]; +pthread_t metadata_multicast_thread; + +void metadata_create_multicast_socket(void) { + if (config.metadata_enabled == 0) + return; + + // Unlike metadata pipe, socket is opened once and stays open, + // so we can call it in create + if (config.metadata_sockaddr && config.metadata_sockport) { + metadata_sock = socket(AF_INET, SOCK_DGRAM, 0); + if (metadata_sock < 0) { + debug(1, "Could not open metadata socket"); + } else { + int buffer_size = METADATA_SNDBUF; + setsockopt(metadata_sock, SOL_SOCKET, SO_SNDBUF, &buffer_size, sizeof(buffer_size)); + bzero((char *)&metadata_sockaddr, sizeof(metadata_sockaddr)); + metadata_sockaddr.sin_family = AF_INET; + metadata_sockaddr.sin_addr.s_addr = inet_addr(config.metadata_sockaddr); + metadata_sockaddr.sin_port = htons(config.metadata_sockport); + metadata_sockmsg = malloc(config.metadata_sockmsglength); + if (metadata_sockmsg) { + memset(metadata_sockmsg, 0, config.metadata_sockmsglength); + } else { + die("Could not malloc metadata multicast socket buffer"); + } + } + } +} + +void metadata_delete_multicast_socket(void) { + if (config.metadata_enabled == 0) + return; + shutdown(metadata_sock, SHUT_RDWR); // we want to immediately deallocate the buffer + close(metadata_sock); + if (metadata_sockmsg) + free(metadata_sockmsg); +} + +void metadata_open(void) { + if (config.metadata_enabled == 0) + return; + + size_t pl = strlen(config.metadata_pipename) + 1; + + char *path = malloc(pl + 1); + snprintf(path, pl + 1, "%s", config.metadata_pipename); + + fd = try_to_open_pipe_for_writing(path); + free(path); +} + +static void metadata_close(void) { + if (fd < 0) + return; + close(fd); + fd = -1; +} + +void metadata_multicast_process(uint32_t type, uint32_t code, char *data, uint32_t length) { + // debug(1, "Process multicast metadata with type %x, code %x and length %u.", type, code, + // length); + if (metadata_sock >= 0 && length < config.metadata_sockmsglength - 8) { + char *ptr = metadata_sockmsg; + uint32_t v; + v = htonl(type); + memcpy(ptr, &v, 4); + ptr += 4; + v = htonl(code); + memcpy(ptr, &v, 4); + ptr += 4; + memcpy(ptr, data, length); + sendto(metadata_sock, metadata_sockmsg, length + 8, 0, (struct sockaddr *)&metadata_sockaddr, + sizeof(metadata_sockaddr)); + } else if (metadata_sock >= 0) { + // send metadata in numbered chunks using the protocol: + // ("ssnc", "chnk", packet_ix, packet_counts, packet_tag, packet_type, chunked_data) + + uint32_t chunk_ix = 0; + if (config.metadata_sockmsglength == 24) + die("A divide by zero almost occurred (config.metadata_sockmsglength = 24)."); + uint32_t chunk_total = length / (config.metadata_sockmsglength - 24); + if (chunk_total * (config.metadata_sockmsglength - 24) < length) { + chunk_total++; + } + uint32_t remaining = length; + uint32_t v; + char *data_crsr = data; + do { + char *ptr = metadata_sockmsg; + memcpy(ptr, "ssncchnk", 8); + ptr += 8; + v = htonl(chunk_ix); + memcpy(ptr, &v, 4); + ptr += 4; + v = htonl(chunk_total); + memcpy(ptr, &v, 4); + ptr += 4; + v = htonl(type); + memcpy(ptr, &v, 4); + ptr += 4; + v = htonl(code); + memcpy(ptr, &v, 4); + ptr += 4; + size_t datalen = remaining; + if (datalen > config.metadata_sockmsglength - 24) { + datalen = config.metadata_sockmsglength - 24; + } + memcpy(ptr, data_crsr, datalen); + data_crsr += datalen; + sendto(metadata_sock, metadata_sockmsg, datalen + 24, 0, + (struct sockaddr *)&metadata_sockaddr, sizeof(metadata_sockaddr)); + chunk_ix++; + remaining -= datalen; + if (remaining == 0) + break; + } while (1); + } +} + +void metadata_process(uint32_t type, uint32_t code, char *data, uint32_t length) { + // debug(1, "Process metadata with type %x, code %x and length %u.", type, code, length); + int ret = 0; + // readers may go away and come back + + if (fd < 0) + metadata_open(); + if (fd < 0) + return; + char thestring[1024]; + snprintf(thestring, 1024, "%x%x%u", type, code, + length); + // ret = non_blocking_write(fd, thestring, strlen(thestring)); + ret = write(fd, thestring, strlen(thestring)); + if (ret < 0) { + // debug(1,"metadata_process error %d exit 1",ret); + return; + } + if ((data != NULL) && (length > 0)) { + snprintf(thestring, 1024, "\n\n"); + // ret = non_blocking_write(fd, thestring, strlen(thestring)); + ret = write(fd, thestring, strlen(thestring)); + if (ret < 0) { + // debug(1,"metadata_process error %d exit 2",ret); + return; + } + // here, we write the data in base64 form using our nice base64 encoder + // but, we break it into lines of 76 output characters, except for the last + // one. + // thus, we send groups of (76/4)*3 = 57 bytes to the encoder at a time + size_t remaining_count = length; + char *remaining_data = data; + // size_t towrite_count; + char outbuf[76]; + while ((remaining_count) && (ret >= 0)) { + size_t towrite_count = remaining_count; + if (towrite_count > 57) + towrite_count = 57; + size_t outbuf_size = 76; // size of output buffer on entry, length of result on exit + if (base64_encode_so((unsigned char *)remaining_data, towrite_count, outbuf, &outbuf_size) == + NULL) + debug(1, "Error encoding base64 data."); + // debug(1,"Remaining count: %d ret: %d, outbuf_size: + // %d.",remaining_count,ret,outbuf_size); + // ret = non_blocking_write(fd, outbuf, outbuf_size); + ret = write(fd, outbuf, outbuf_size); + if (ret < 0) { + // debug(1,"metadata_process error %d exit 3",ret); + return; + } + remaining_data += towrite_count; + remaining_count -= towrite_count; + } + snprintf(thestring, 1024, ""); + // ret = non_blocking_write(fd, thestring, strlen(thestring)); + ret = write(fd, thestring, strlen(thestring)); + if (ret < 0) { + // debug(1,"metadata_process error %d exit 4",ret); + return; + } + } + snprintf(thestring, 1024, "\n"); + // ret = non_blocking_write(fd, thestring, strlen(thestring)); + ret = write(fd, thestring, strlen(thestring)); + if (ret < 0) { + // debug(1,"metadata_process error %d exit 5",ret); + return; + } +} + +void metadata_pack_cleanup_function(void *arg) { + // debug(1, "metadata_pack_cleanup_function called"); + metadata_package *pack = (metadata_package *)arg; + if (pack->carrier) + msg_free(&pack->carrier); // release the message + else if (pack->data) + free(pack->data); + // debug(1, "metadata_pack_cleanup_function exit"); +} + +void metadata_thread_cleanup_function(__attribute__((unused)) void *arg) { + // debug(2, "metadata_thread_cleanup_function called"); + metadata_close(); + pc_queue_delete(&metadata_queue); +} + +void *metadata_thread_function(__attribute__((unused)) void *ignore) { + // create a pc_queue for passing information to a threaded metadata handler + pc_queue_init(&metadata_queue, (char *)&metadata_queue_items, sizeof(metadata_package), + metadata_queue_size, "pipe"); + metadata_create_multicast_socket(); + metadata_package pack; + pthread_cleanup_push(metadata_thread_cleanup_function, NULL); + while (1) { + pc_queue_get_item(&metadata_queue, &pack); + pthread_cleanup_push(metadata_pack_cleanup_function, (void *)&pack); + if (config.metadata_enabled) { + if (pack.carrier) { + debug(3, " pipe: type %x, code %x, length %u, message %d.", pack.type, pack.code, + pack.length, pack.carrier->index_number); + } else { + debug(3, " pipe: type %x, code %x, length %u.", pack.type, pack.code, pack.length); + } + metadata_process(pack.type, pack.code, pack.data, pack.length); + debug(3, " pipe: done."); + } + pthread_cleanup_pop(1); + } + pthread_cleanup_pop(1); // will never happen + pthread_exit(NULL); +} + +void metadata_multicast_thread_cleanup_function(__attribute__((unused)) void *arg) { + // debug(2, "metadata_multicast_thread_cleanup_function called"); + metadata_delete_multicast_socket(); + pc_queue_delete(&metadata_multicast_queue); +} + +void *metadata_multicast_thread_function(__attribute__((unused)) void *ignore) { + // create a pc_queue for passing information to a threaded metadata handler + pc_queue_init(&metadata_multicast_queue, (char *)&metadata_multicast_queue_items, + sizeof(metadata_package), metadata_multicast_queue_size, "multicast"); + metadata_create_multicast_socket(); + metadata_package pack; + pthread_cleanup_push(metadata_multicast_thread_cleanup_function, NULL); + while (1) { + pc_queue_get_item(&metadata_multicast_queue, &pack); + pthread_cleanup_push(metadata_pack_cleanup_function, (void *)&pack); + if (config.metadata_enabled) { + if (pack.carrier) { + debug(3, + " multicast: type " + "%x, code %x, length %u, message %d.", + pack.type, pack.code, pack.length, pack.carrier->index_number); + } else { + debug(3, + " multicast: type " + "%x, code %x, length %u.", + pack.type, pack.code, pack.length); + } + metadata_multicast_process(pack.type, pack.code, pack.data, pack.length); + debug(3, + " multicast: done."); + } + pthread_cleanup_pop(1); + } + pthread_cleanup_pop(1); // will never happen + pthread_exit(NULL); +} + +#ifdef CONFIG_METADATA_HUB +void metadata_hub_close(void) {} + +void metadata_hub_thread_cleanup_function(__attribute__((unused)) void *arg) { + // debug(2, "metadata_hub_thread_cleanup_function called"); + metadata_hub_close(); + pc_queue_delete(&metadata_hub_queue); +} + +void *metadata_hub_thread_function(__attribute__((unused)) void *ignore) { + // create a pc_queue for passing information to a threaded metadata handler + pc_queue_init(&metadata_hub_queue, (char *)&metadata_hub_queue_items, sizeof(metadata_package), + metadata_hub_queue_size, "hub"); + metadata_package pack; + pthread_cleanup_push(metadata_hub_thread_cleanup_function, NULL); + while (1) { + pc_queue_get_item(&metadata_hub_queue, &pack); + pthread_cleanup_push(metadata_pack_cleanup_function, (void *)&pack); + if (pack.carrier) { + debug(3, " hub: type %x, code %x, length %u, message %d.", pack.type, + pack.code, pack.length, pack.carrier->index_number); + } else { + debug(3, " hub: type %x, code %x, length %u.", pack.type, pack.code, + pack.length); + } + metadata_hub_process_metadata(pack.type, pack.code, pack.data, pack.length); + debug(3, " hub: done."); + pthread_cleanup_pop(1); + } + pthread_cleanup_pop(1); // will never happen + pthread_exit(NULL); +} +#endif + +#ifdef CONFIG_MQTT +void metadata_mqtt_close(void) {} + +void metadata_mqtt_thread_cleanup_function(__attribute__((unused)) void *arg) { + // debug(2, "metadata_mqtt_thread_cleanup_function called"); + metadata_mqtt_close(); + pc_queue_delete(&metadata_mqtt_queue); + // debug(2, "metadata_mqtt_thread_cleanup_function done"); +} + +void *metadata_mqtt_thread_function(__attribute__((unused)) void *ignore) { + // create a pc_queue for passing information to a threaded metadata handler + pc_queue_init(&metadata_mqtt_queue, (char *)&metadata_mqtt_queue_items, sizeof(metadata_package), + metadata_mqtt_queue_size, "mqtt"); + metadata_package pack; + pthread_cleanup_push(metadata_mqtt_thread_cleanup_function, NULL); + while (1) { + pc_queue_get_item(&metadata_mqtt_queue, &pack); + pthread_cleanup_push(metadata_pack_cleanup_function, (void *)&pack); + if (config.mqtt_enabled) { + if (pack.carrier) { + debug(3, + " mqtt: type %x, code %x, length %u, message " + "%d.", + pack.type, pack.code, pack.length, pack.carrier->index_number); + } else { + debug(3, " mqtt: type %x, code %x, length %u.", + pack.type, pack.code, pack.length); + } + mqtt_process_metadata(pack.type, pack.code, pack.data, pack.length); + debug(3, " mqtt: done."); + } + + pthread_cleanup_pop(1); + } + pthread_cleanup_pop(1); // will never happen + pthread_exit(NULL); +} +#endif + +void metadata_init(void) { + if (config.metadata_enabled) { + // create the metadata pipe, if necessary + size_t pl = strlen(config.metadata_pipename) + 1; + char *path = malloc(pl + 1); + snprintf(path, pl + 1, "%s", config.metadata_pipename); + mode_t oldumask = umask(000); + if (mkfifo(path, 0666) && errno != EEXIST) + die("Could not create metadata pipe \"%s\".", path); + umask(oldumask); + debug(1, "metadata pipe name is \"%s\".", path); + + // try to open it + fd = try_to_open_pipe_for_writing(path); + // we check that it's not a "real" error. From the "man 2 open" page: + // "ENXIO O_NONBLOCK | O_WRONLY is set, the named file is a FIFO, and no process has the FIFO + // open for reading." Which is okay. + if ((fd == -1) && (errno != ENXIO)) { + char errorstring[1024]; + strerror_r(errno, (char *)errorstring, sizeof(errorstring)); + debug(1, "metadata_hub_thread_function -- error %d (\"%s\") opening pipe: \"%s\".", errno, + (char *)errorstring, path); + warn("can not open metadata pipe -- error %d (\"%s\") opening pipe: \"%s\".", errno, + (char *)errorstring, path); + } + free(path); + if (pthread_create(&metadata_thread, NULL, metadata_thread_function, NULL) != 0) + debug(1, "Failed to create metadata thread!"); + + if (pthread_create(&metadata_multicast_thread, NULL, metadata_multicast_thread_function, + NULL) != 0) + debug(1, "Failed to create metadata multicast thread!"); + } +#ifdef CONFIG_METADATA_HUB + if (pthread_create(&metadata_hub_thread, NULL, metadata_hub_thread_function, NULL) != 0) + debug(1, "Failed to create metadata hub thread!"); +#endif +#ifdef CONFIG_MQTT + if (pthread_create(&metadata_mqtt_thread, NULL, metadata_mqtt_thread_function, NULL) != 0) + debug(1, "Failed to create metadata mqtt thread!"); +#endif + metadata_running = 1; +} + +void metadata_stop(void) { + if (metadata_running) { + debug(2, "metadata_stop called."); +#ifdef CONFIG_MQTT + // debug(2, "metadata stop mqtt thread."); + pthread_cancel(metadata_mqtt_thread); + pthread_join(metadata_mqtt_thread, NULL); + // debug(2, "metadata stop mqtt done."); +#endif +#ifdef CONFIG_METADATA_HUB + // debug(2, "metadata stop hub thread."); + pthread_cancel(metadata_hub_thread); + pthread_join(metadata_hub_thread, NULL); + // debug(2, "metadata stop hub done."); +#endif + if (config.metadata_enabled) { + // debug(2, "metadata stop multicast thread."); + if (metadata_multicast_thread) { + pthread_cancel(metadata_multicast_thread); + pthread_join(metadata_multicast_thread, NULL); + // debug(2, "metadata stop multicast done."); + } + if (metadata_thread) { + // debug(2, "metadata stop metadata_thread thread."); + pthread_cancel(metadata_thread); + pthread_join(metadata_thread, NULL); + // debug(2, "metadata_stop finished successfully."); + } + } + } +} + +int send_metadata_to_queue(pc_queue *queue, uint32_t type, uint32_t code, char *data, + uint32_t length, rtsp_message *carrier, int block) { + + // clang-format off + // parameters: + // type, + // code, + // pointer to data or NULL, + // length of data or NULL, + // the rtsp_message or NULL, + + // the rtsp_message is sent for 'core' messages, because it contains the data + // and must not be freed until the data has been read. + // So, it is passed to send_metadata to be retained, sent to the thread where metadata + // is processed and released (and probably freed). + + // The rtsp_message is also sent for certain non-'core' messages. + + // The reading of the parameters is a bit complex: + // If the rtsp_message field is non-null, then it represents an rtsp_message + // and the data pointer is assumed to point to something within it. + // The reference counter of the rtsp_message is incremented here and + // should be decremented by the metadata handler when finished. + // If the reference count reduces to zero, the message will be freed. + + // If the rtsp_message is NULL, then if the pointer is non-null then the data it + // points to, of the length specified, is memcpy'd and passed to the metadata + // handler. The handler should free it when done. + + // If the rtsp_message is NULL and the pointer is also NULL, nothing further + // is done. + // clang-format on + + metadata_package pack; + pack.type = type; + pack.code = code; + pack.length = length; + pack.carrier = carrier; + pack.data = data; + if (pack.carrier) { + msg_retain(pack.carrier); + } else { + if (data) + pack.data = memdup(data, length); // only if it's not a null + } + int rc = pc_queue_add_item(queue, &pack, block); + if (rc != 0) { + if (pack.carrier) { + if (rc == EWOULDBLOCK) + debug(2, + "metadata queue \"%s\" full, dropping message item: type %x, code %x, data %x, " + "length %u, message %d.", + queue->name, pack.type, pack.code, pack.data, pack.length, + pack.carrier->index_number); + msg_free(&pack.carrier); + } else { + if (rc == EWOULDBLOCK) + debug( + 2, + "metadata queue \"%s\" full, dropping data item: type %x, code %x, data %x, length %u.", + queue->name, pack.type, pack.code, pack.data, pack.length); + if (pack.data) + free(pack.data); + } + } + return rc; +} + +int send_metadata(uint32_t type, uint32_t code, char *data, uint32_t length, rtsp_message *carrier, + int block) { + int rc = 0; + if (config.metadata_enabled) { + rc = send_metadata_to_queue(&metadata_queue, type, code, data, length, carrier, block); + rc = + send_metadata_to_queue(&metadata_multicast_queue, type, code, data, length, carrier, block); + } + +#ifdef CONFIG_METADATA_HUB + rc = send_metadata_to_queue(&metadata_hub_queue, type, code, data, length, carrier, block); +#endif + +#ifdef CONFIG_MQTT + rc = send_metadata_to_queue(&metadata_mqtt_queue, type, code, data, length, carrier, block); +#endif + + return rc; +} + +static void handle_set_parameter_metadata(__attribute__((unused)) rtsp_conn_info *conn, + rtsp_message *req, + __attribute__((unused)) rtsp_message *resp) { + char *cp = req->content; + unsigned int cl = req->contentlength; + + unsigned int off = 8; + + uint32_t itag, vl; + while (off < cl) { + // pick up the metadata tag as an unsigned longint + memcpy(&itag, (uint32_t *)(cp + off), sizeof(uint32_t)); /* can be misaligned, thus memcpy */ + itag = ntohl(itag); + off += sizeof(uint32_t); + + // pick up the length of the data + memcpy(&vl, (uint32_t *)(cp + off), sizeof(uint32_t)); /* can be misaligned, thus memcpy */ + vl = ntohl(vl); + off += sizeof(uint32_t); + + // pass the data over + if (vl == 0) + send_metadata('core', itag, NULL, 0, NULL, 1); + else + send_metadata('core', itag, (char *)(cp + off), vl, req, 1); + + // move on to the next item + off += vl; + } +} + +#endif + +static void handle_get_parameter(__attribute__((unused)) rtsp_conn_info *conn, rtsp_message *req, + rtsp_message *resp) { + // debug(1, "Connection %d: GET_PARAMETER", conn->connection_number); + // debug_print_msg_headers(1,req); + // debug_print_msg_content(1,req); + + if ((req->content) && (req->contentlength == strlen("volume\r\n")) && + strstr(req->content, "volume") == req->content) { + debug(1, "Connection %d: Current volume (%.6f) requested", conn->connection_number, + config.airplay_volume); + char *p = malloc(128); // will be automatically deallocated with the response is deleted + if (p) { + resp->content = p; + resp->contentlength = snprintf(p, 128, "\r\nvolume: %.6f\r\n", config.airplay_volume); + } else { + debug(1, "Couldn't allocate space for a response."); + } + } + resp->respcode = 200; +} + +static void handle_set_parameter(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { + debug(3, "Connection %d: SET_PARAMETER", conn->connection_number); + // if (!req->contentlength) + // debug(1, "received empty SET_PARAMETER request."); + + // debug_print_msg_headers(1,req); + + char *ct = msg_get_header(req, "Content-Type"); + + if (ct) { + // debug(2, "SET_PARAMETER Content-Type:\"%s\".", ct); + +#ifdef CONFIG_METADATA + // It seems that the rtptime of the message is used as a kind of an ID that + // can be used + // to link items of metadata, including pictures, that refer to the same + // entity. + // If they refer to the same item, they have the same rtptime. + // So we send the rtptime before and after both the metadata items and the + // picture item + // get the rtptime + char *p = NULL; + char *hdr = msg_get_header(req, "RTP-Info"); + + if (hdr) { + p = strstr(hdr, "rtptime="); + if (p) { + p = strchr(p, '='); + } + } + + // not all items have RTP-time stuff in them, which is okay + + if (!strncmp(ct, "application/x-dmap-tagged", 25)) { + debug(3, "received metadata tags in SET_PARAMETER request."); + if (p == NULL) + debug(1, "Missing RTP-Time info for metadata"); + if (p) + send_metadata('ssnc', 'mdst', p + 1, strlen(p + 1), req, 1); // metadata starting + else + send_metadata('ssnc', 'mdst', NULL, 0, NULL, + 0); // metadata starting, if rtptime is not available + + handle_set_parameter_metadata(conn, req, resp); + + if (p) + send_metadata('ssnc', 'mden', p + 1, strlen(p + 1), req, 1); // metadata ending + else + send_metadata('ssnc', 'mden', NULL, 0, NULL, + 0); // metadata starting, if rtptime is not available + + } else if (!strncmp(ct, "image", 5)) { + // Some server simply ignore the md field from the TXT record. If The + // config says 'please, do not include any cover art', we are polite and + // do not write them to the pipe. + if (config.get_coverart) { + // debug(1, "received image in SET_PARAMETER request."); + // note: the image/type tag isn't reliable, so it's not being sent + // -- best look at the first few bytes of the image + if (p == NULL) + debug(1, "Missing RTP-Time info for picture item"); + if (p) + send_metadata('ssnc', 'pcst', p + 1, strlen(p + 1), req, 1); // picture starting + else + send_metadata('ssnc', 'pcst', NULL, 0, NULL, + 0); // picture starting, if rtptime is not available + + send_metadata('ssnc', 'PICT', req->content, req->contentlength, req, 1); + + if (p) + send_metadata('ssnc', 'pcen', p + 1, strlen(p + 1), req, 1); // picture ending + else + send_metadata('ssnc', 'pcen', NULL, 0, NULL, + 0); // picture ending, if rtptime is not available + } else { + debug(1, "Ignore received picture item (include_cover_art = no)."); + } + } else +#endif + if (!strncmp(ct, "text/parameters", 15)) { + // debug(2, "received parameters in SET_PARAMETER request."); + handle_set_parameter_parameter(conn, req, resp); // this could be volume or progress + } else { + debug(1, "Connection %d: received unknown Content-Type \"%s\" in SET_PARAMETER request.", + conn->connection_number, ct); + debug_print_msg_headers(1, req); + } + } else { + debug(1, "Connection %d: missing Content-Type header in SET_PARAMETER request.", + conn->connection_number); + } + resp->respcode = 200; +} + +static void handle_announce(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { + debug(3, "Connection %d: ANNOUNCE", conn->connection_number); + int get_play_status = get_play_lock(conn, config.allow_session_interruption); + if (get_play_status != -1) { + debug(3, "Connection %d: ANNOUNCE has acquired play lock.", conn->connection_number); + + // now, if this new session did not break in, then it's okay to reset the next UDP ports + // to the start of the range + + if (get_play_status == 1) { // will be zero if it wasn't waiting to break in + resetFreeUDPPort(); + } + + /* + { + char *cp = req->content; + int cp_left = req->contentlength; + while (cp_left > 1) { + if (strlen(cp) != 0) + debug(1,">>>>>> %s", cp); + cp += strlen(cp) + 1; + cp_left -= strlen(cp) + 1; + } + } + */ + // In AirPlay 2, an ANNOUNCE signifies the start of an AirPlay 1 session. +#ifdef CONFIG_AIRPLAY_2 + conn->airplay_type = ap_1; + conn->timing_type = ts_ntp; + debug(1, "Connection %d: Classic AirPlay connection from %s:%u to self at %s:%u.", + conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, + conn->self_ip_string, conn->self_rtsp_port); +#endif + + conn->stream.type = ast_unknown; + resp->respcode = 456; // 456 - Header Field Not Valid for Resource + char *pssid = NULL; + char *paesiv = NULL; + char *prsaaeskey = NULL; + char *pfmtp = NULL; + char *pminlatency = NULL; + char *pmaxlatency = NULL; + // char *pAudioMediaInfo = NULL; + char *pUncompressedCDAudio = NULL; + char *cp = req->content; + int cp_left = req->contentlength; + char *next; + while (cp_left && cp) { + next = nextline(cp, cp_left); + cp_left -= next - cp; + + if (!strncmp(cp, "a=rtpmap:96 L16/44100/2", strlen("a=rtpmap:96 L16/44100/2"))) + pUncompressedCDAudio = cp + strlen("a=rtpmap:96 L16/44100/2"); + + // if (!strncmp(cp, "m=audio", strlen("m=audio"))) + // pAudioMediaInfo = cp + strlen("m=audio"); + + if (!strncmp(cp, "o=iTunes", strlen("o=iTunes"))) + pssid = cp + strlen("o=iTunes"); + + if (!strncmp(cp, "a=fmtp:", strlen("a=fmtp:"))) + pfmtp = cp + strlen("a=fmtp:"); + + if (!strncmp(cp, "a=aesiv:", strlen("a=aesiv:"))) + paesiv = cp + strlen("a=aesiv:"); + + if (!strncmp(cp, "a=rsaaeskey:", strlen("a=rsaaeskey:"))) + prsaaeskey = cp + strlen("a=rsaaeskey:"); + + if (!strncmp(cp, "a=min-latency:", strlen("a=min-latency:"))) + pminlatency = cp + strlen("a=min-latency:"); + + if (!strncmp(cp, "a=max-latency:", strlen("a=max-latency:"))) + pmaxlatency = cp + strlen("a=max-latency:"); + + cp = next; + } + + if (pUncompressedCDAudio) { + debug(2, "An uncompressed PCM stream has been detected."); + conn->stream.type = ast_uncompressed; + conn->max_frames_per_packet = 352; // number of audio frames per packet. + conn->input_rate = 44100; + conn->input_num_channels = 2; + conn->input_bit_depth = 16; + conn->input_bytes_per_frame = conn->input_num_channels * ((conn->input_bit_depth + 7) / 8); + + /* + int y = strlen(pAudioMediaInfo); + if (y > 0) { + char obf[4096]; + if (y > 4096) + y = 4096; + char *p = pAudioMediaInfo; + char *obfp = obf; + int obfc; + for (obfc = 0; obfc < y; obfc++) { + snprintf(obfp, 3, "%02X", (unsigned int)*p); + p++; + obfp += 2; + }; + *obfp = 0; + debug(1, "AudioMediaInfo: \"%s\".", obf); + } + */ + } + + if (pssid) { + uint32_t ssid = uatoi(pssid); + debug(3, "Synchronisation Source Identifier: %08X,%u", ssid, ssid); + } + + if (pminlatency) { + conn->minimum_latency = atoi(pminlatency); + debug(3, "Minimum latency %d specified", conn->minimum_latency); + } + + if (pmaxlatency) { + conn->maximum_latency = atoi(pmaxlatency); + debug(3, "Maximum latency %d specified", conn->maximum_latency); + } + + if ((paesiv == NULL) && (prsaaeskey == NULL)) { + // debug(1,"Unencrypted session requested?"); + conn->stream.encrypted = 0; + } else if ((paesiv != NULL) && (prsaaeskey != NULL)){ + conn->stream.encrypted = 1; + // debug(1,"Encrypted session requested"); + } else { + warn("Invalid Announce message -- missing paesiv or prsaaeskey."); + goto out; + } + if (conn->stream.encrypted) { + int len, keylen; + uint8_t *aesiv = base64_dec(paesiv, &len); + if (len != 16) { + warn("client announced aeskey of %d bytes, wanted 16", len); + free(aesiv); + goto out; + } + memcpy(conn->stream.aesiv, aesiv, 16); + free(aesiv); + + uint8_t *rsaaeskey = base64_dec(prsaaeskey, &len); + uint8_t *aeskey = rsa_apply(rsaaeskey, len, &keylen, RSA_MODE_KEY); + free(rsaaeskey); + if (keylen != 16) { + warn("client announced rsaaeskey of %d bytes, wanted 16", keylen); + free(aeskey); + goto out; + } + memcpy(conn->stream.aeskey, aeskey, 16); + free(aeskey); + } + + if (pfmtp) { + conn->stream.type = ast_apple_lossless; + debug(3, "An ALAC stream has been detected."); + + // Set reasonable connection defaults + conn->stream.fmtp[0] = 96; + conn->stream.fmtp[1] = 352; + conn->stream.fmtp[2] = 0; + conn->stream.fmtp[3] = 16; + conn->stream.fmtp[4] = 40; + conn->stream.fmtp[5] = 10; + conn->stream.fmtp[6] = 14; + conn->stream.fmtp[7] = 2; + conn->stream.fmtp[8] = 255; + conn->stream.fmtp[9] = 0; + conn->stream.fmtp[10] = 0; + conn->stream.fmtp[11] = 44100; + + unsigned int i = 0; + unsigned int max_param = sizeof(conn->stream.fmtp) / sizeof(conn->stream.fmtp[0]); + char *found; + while ((found = strsep(&pfmtp, " \t")) != NULL && i < max_param) { + conn->stream.fmtp[i++] = atoi(found); + } + // here we should check the sanity of the fmtp values + // for (i = 0; i < sizeof(conn->stream.fmtp) / sizeof(conn->stream.fmtp[0]); i++) + // debug(1," fmtp[%2d] is: %10d",i,conn->stream.fmtp[i]); + + // set the parameters of the player (as distinct from the parameters of the decoder -- that's + // done later). + conn->max_frames_per_packet = conn->stream.fmtp[1]; // number of audio frames per packet. + conn->input_rate = conn->stream.fmtp[11]; + conn->input_num_channels = conn->stream.fmtp[7]; + conn->input_bit_depth = conn->stream.fmtp[3]; + conn->input_bytes_per_frame = conn->input_num_channels * ((conn->input_bit_depth + 7) / 8); + } + + if (conn->stream.type == ast_unknown) { + warn("Can not process the following ANNOUNCE message:"); + // print each line of the request content + // the problem is that nextline has replace all returns, newlines, etc. by + // NULLs + char *cp = req->content; + int cp_left = req->contentlength; + while (cp_left > 1) { + if (strlen(cp) != 0) + warn(" %s", cp); + cp += strlen(cp) + 1; + cp_left -= strlen(cp) + 1; + } + goto out; + } + + char *hdr = msg_get_header(req, "X-Apple-Client-Name"); + if (hdr) { + debug(1, "Play connection from device named \"%s\" on RTSP conversation thread %d.", hdr, + conn->connection_number); +#ifdef CONFIG_METADATA + send_metadata('ssnc', 'snam', hdr, strlen(hdr), req, 1); +#endif + } + hdr = msg_get_header(req, "User-Agent"); + if (hdr) { + conn->UserAgent = strdup(hdr); + debug(2, "Play connection from user agent \"%s\" on RTSP conversation thread %d.", hdr, + conn->connection_number); + // if the user agent is AirPlay and has a version number of 353 or less (from iOS 11.1,2) + // use the older way of calculating the latency + + char *p = strstr(hdr, "AirPlay"); + if (p) { + p = strchr(p, '/'); + if (p) { + conn->AirPlayVersion = atoi(p + 1); + debug(2, "AirPlay version %d detected.", conn->AirPlayVersion); + } + } + +#ifdef CONFIG_METADATA + send_metadata('ssnc', 'snua', hdr, strlen(hdr), req, 1); +#endif + } + resp->respcode = 200; + } else { + resp->respcode = 453; + debug(1, "Connection %d: ANNOUNCE failed because another connection is already playing.", + conn->connection_number); + } + +out: + if (resp->respcode != 200 && resp->respcode != 453) { + debug(1, "Connection %d: Error in handling ANNOUNCE. Unlocking the play lock.", + conn->connection_number); + release_play_lock(conn); + } +} + +#ifdef CONFIG_AIRPLAY_2 +static struct method_handler { + char *method; + void (*ap1_handler)(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp); // for AirPlay 1 + void (*ap2_handler)(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp); // for AirPlay 2 +} method_handlers[] = {{"OPTIONS", handle_options, handle_options}, + {"ANNOUNCE", handle_announce, handle_announce}, + {"FLUSH", handle_flush, handle_flush}, + {"TEARDOWN", handle_teardown, handle_teardown_2}, + {"SETUP", handle_setup, handle_setup_2}, + {"GET_PARAMETER", handle_get_parameter, handle_get_parameter}, + {"SET_PARAMETER", handle_set_parameter, handle_set_parameter}, + {"RECORD", handle_record, handle_record_2}, + {"GET", handle_get, handle_get}, + {"POST", handle_post, handle_post}, + {"SETPEERS", handle_unimplemented_ap1, handle_setpeers}, + {"SETRATEANCHORTI", handle_unimplemented_ap1, handle_setrateanchori}, + {"FLUSHBUFFERED", handle_unimplemented_ap1, handle_flushbuffered}, + {"SETRATE", handle_unimplemented_ap1, handle_setrate}, + {NULL, NULL, NULL}}; +#else +static struct method_handler { + char *method; + void (*handler)(rtsp_conn_info *conn, rtsp_message *req, + rtsp_message *resp); // for AirPlay 1 only +} method_handlers[] = {{"OPTIONS", handle_options}, + {"GET", handle_get}, + {"POST", handle_post}, + {"ANNOUNCE", handle_announce}, + {"FLUSH", handle_flush}, + {"TEARDOWN", handle_teardown}, + {"SETUP", handle_setup}, + {"GET_PARAMETER", handle_get_parameter}, + {"SET_PARAMETER", handle_set_parameter}, + {"RECORD", handle_record}, + {NULL, NULL}}; +#endif + +static void apple_challenge(int fd, rtsp_message *req, rtsp_message *resp) { + char *hdr = msg_get_header(req, "Apple-Challenge"); + if (!hdr) + return; + SOCKADDR fdsa; + socklen_t sa_len = sizeof(fdsa); + getsockname(fd, (struct sockaddr *)&fdsa, &sa_len); + + int chall_len; + uint8_t *chall = base64_dec(hdr, &chall_len); + if (chall == NULL) + die("null chall in apple_challenge"); + uint8_t buf[48], *bp = buf; + int i; + memset(buf, 0, sizeof(buf)); + + if (chall_len > 16) { + warn("oversized Apple-Challenge!"); + free(chall); + return; + } + memcpy(bp, chall, chall_len); + free(chall); + bp += chall_len; + +#ifdef AF_INET6 + if (fdsa.SAFAMILY == AF_INET6) { + struct sockaddr_in6 *sa6 = (struct sockaddr_in6 *)(&fdsa); + memcpy(bp, sa6->sin6_addr.s6_addr, 16); + bp += 16; + } else +#endif + { + struct sockaddr_in *sa = (struct sockaddr_in *)(&fdsa); + memcpy(bp, &sa->sin_addr.s_addr, 4); + bp += 4; + } + + for (i = 0; i < 6; i++) + *bp++ = config.hw_addr[i]; + + int buflen, resplen; + buflen = bp - buf; + if (buflen < 0x20) + buflen = 0x20; + + uint8_t *challresp = rsa_apply(buf, buflen, &resplen, RSA_MODE_AUTH); + char *encoded = base64_enc(challresp, resplen); + if (encoded == NULL) + die("could not allocate memory for \"encoded\""); + // strip the padding. + char *padding = strchr(encoded, '='); + if (padding) + *padding = 0; + + msg_add_header(resp, "Apple-Response", encoded); // will be freed when the response is freed. + free(challresp); + free(encoded); +} + +static char *make_nonce(void) { + uint8_t random[8]; + int fd = open("/dev/urandom", O_RDONLY); + if (fd < 0) + die("could not open /dev/urandom!"); + // int ignore = + if (read(fd, random, sizeof(random)) != sizeof(random)) + debug(1, "Error reading /dev/urandom"); + close(fd); + return base64_enc(random, 8); // returns a pointer to malloc'ed memory +} + +static int rtsp_auth(char **nonce, rtsp_message *req, rtsp_message *resp) { + + if (!config.password) + return 0; + if (!*nonce) { + *nonce = make_nonce(); + goto authenticate; + } + + char *hdr = msg_get_header(req, "Authorization"); + if (!hdr || strncmp(hdr, "Digest ", 7)) + goto authenticate; + + char *realm = strstr(hdr, "realm=\""); + char *username = strstr(hdr, "username=\""); + char *response = strstr(hdr, "response=\""); + char *uri = strstr(hdr, "uri=\""); + + if (!realm || !username || !response || !uri) + goto authenticate; + + char *quote; + realm = strchr(realm, '"') + 1; + if (!(quote = strchr(realm, '"'))) + goto authenticate; + *quote = 0; + username = strchr(username, '"') + 1; + if (!(quote = strchr(username, '"'))) + goto authenticate; + *quote = 0; + response = strchr(response, '"') + 1; + if (!(quote = strchr(response, '"'))) + goto authenticate; + *quote = 0; + uri = strchr(uri, '"') + 1; + if (!(quote = strchr(uri, '"'))) + goto authenticate; + *quote = 0; + + uint8_t digest_urp[16], digest_mu[16], digest_total[16]; + +#ifdef CONFIG_OPENSSL + MD5_CTX ctx; + + int oldState; + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); + MD5_Init(&ctx); + MD5_Update(&ctx, username, strlen(username)); + MD5_Update(&ctx, ":", 1); + MD5_Update(&ctx, realm, strlen(realm)); + MD5_Update(&ctx, ":", 1); + MD5_Update(&ctx, config.password, strlen(config.password)); + MD5_Final(digest_urp, &ctx); + MD5_Init(&ctx); + MD5_Update(&ctx, req->method, strlen(req->method)); + MD5_Update(&ctx, ":", 1); + MD5_Update(&ctx, uri, strlen(uri)); + MD5_Final(digest_mu, &ctx); + pthread_setcancelstate(oldState, NULL); +#endif + +#ifdef CONFIG_MBEDTLS +#if MBEDTLS_VERSION_MINOR >= 7 + mbedtls_md5_context tctx; + mbedtls_md5_starts_ret(&tctx); + mbedtls_md5_update_ret(&tctx, (const unsigned char *)username, strlen(username)); + mbedtls_md5_update_ret(&tctx, (unsigned char *)":", 1); + mbedtls_md5_update_ret(&tctx, (const unsigned char *)realm, strlen(realm)); + mbedtls_md5_update_ret(&tctx, (unsigned char *)":", 1); + mbedtls_md5_update_ret(&tctx, (const unsigned char *)config.password, strlen(config.password)); + mbedtls_md5_finish_ret(&tctx, digest_urp); + mbedtls_md5_starts_ret(&tctx); + mbedtls_md5_update_ret(&tctx, (const unsigned char *)req->method, strlen(req->method)); + mbedtls_md5_update_ret(&tctx, (unsigned char *)":", 1); + mbedtls_md5_update_ret(&tctx, (const unsigned char *)uri, strlen(uri)); + mbedtls_md5_finish_ret(&tctx, digest_mu); +#else + mbedtls_md5_context tctx; + mbedtls_md5_starts(&tctx); + mbedtls_md5_update(&tctx, (const unsigned char *)username, strlen(username)); + mbedtls_md5_update(&tctx, (unsigned char *)":", 1); + mbedtls_md5_update(&tctx, (const unsigned char *)realm, strlen(realm)); + mbedtls_md5_update(&tctx, (unsigned char *)":", 1); + mbedtls_md5_update(&tctx, (const unsigned char *)config.password, strlen(config.password)); + mbedtls_md5_finish(&tctx, digest_urp); + mbedtls_md5_starts(&tctx); + mbedtls_md5_update(&tctx, (const unsigned char *)req->method, strlen(req->method)); + mbedtls_md5_update(&tctx, (unsigned char *)":", 1); + mbedtls_md5_update(&tctx, (const unsigned char *)uri, strlen(uri)); + mbedtls_md5_finish(&tctx, digest_mu); +#endif +#endif + +#ifdef CONFIG_POLARSSL + md5_context tctx; + md5_starts(&tctx); + md5_update(&tctx, (const unsigned char *)username, strlen(username)); + md5_update(&tctx, (unsigned char *)":", 1); + md5_update(&tctx, (const unsigned char *)realm, strlen(realm)); + md5_update(&tctx, (unsigned char *)":", 1); + md5_update(&tctx, (const unsigned char *)config.password, strlen(config.password)); + md5_finish(&tctx, digest_urp); + md5_starts(&tctx); + md5_update(&tctx, (const unsigned char *)req->method, strlen(req->method)); + md5_update(&tctx, (unsigned char *)":", 1); + md5_update(&tctx, (const unsigned char *)uri, strlen(uri)); + md5_finish(&tctx, digest_mu); +#endif + + int i; + unsigned char buf[33]; + for (i = 0; i < 16; i++) + snprintf((char *)buf + 2 * i, 3, "%02x", digest_urp[i]); + +#ifdef CONFIG_OPENSSL + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); + MD5_Init(&ctx); + MD5_Update(&ctx, buf, 32); + MD5_Update(&ctx, ":", 1); + MD5_Update(&ctx, *nonce, strlen(*nonce)); + MD5_Update(&ctx, ":", 1); + for (i = 0; i < 16; i++) + snprintf((char *)buf + 2 * i, 3, "%02x", digest_mu[i]); + MD5_Update(&ctx, buf, 32); + MD5_Final(digest_total, &ctx); + pthread_setcancelstate(oldState, NULL); +#endif + +#ifdef CONFIG_MBEDTLS +#if MBEDTLS_VERSION_MINOR >= 7 + mbedtls_md5_starts_ret(&tctx); + mbedtls_md5_update_ret(&tctx, buf, 32); + mbedtls_md5_update_ret(&tctx, (unsigned char *)":", 1); + mbedtls_md5_update_ret(&tctx, (const unsigned char *)*nonce, strlen(*nonce)); + mbedtls_md5_update_ret(&tctx, (unsigned char *)":", 1); + for (i = 0; i < 16; i++) + snprintf((char *)buf + 2 * i, 3, "%02x", digest_mu[i]); + mbedtls_md5_update_ret(&tctx, buf, 32); + mbedtls_md5_finish_ret(&tctx, digest_total); +#else + mbedtls_md5_starts(&tctx); + mbedtls_md5_update(&tctx, buf, 32); + mbedtls_md5_update(&tctx, (unsigned char *)":", 1); + mbedtls_md5_update(&tctx, (const unsigned char *)*nonce, strlen(*nonce)); + mbedtls_md5_update(&tctx, (unsigned char *)":", 1); + for (i = 0; i < 16; i++) + snprintf((char *)buf + 2 * i, 3, "%02x", digest_mu[i]); + mbedtls_md5_update(&tctx, buf, 32); + mbedtls_md5_finish(&tctx, digest_total); +#endif +#endif + +#ifdef CONFIG_POLARSSL + md5_starts(&tctx); + md5_update(&tctx, buf, 32); + md5_update(&tctx, (unsigned char *)":", 1); + md5_update(&tctx, (const unsigned char *)*nonce, strlen(*nonce)); + md5_update(&tctx, (unsigned char *)":", 1); + for (i = 0; i < 16; i++) + snprintf((char *)buf + 2 * i, 3, "%02x", digest_mu[i]); + md5_update(&tctx, buf, 32); + md5_finish(&tctx, digest_total); +#endif + + for (i = 0; i < 16; i++) + snprintf((char *)buf + 2 * i, 3, "%02x", digest_total[i]); + + if (!strcmp(response, (const char *)buf)) + return 0; + warn("Password authorization failed."); + +authenticate: + resp->respcode = 401; + int hdrlen = strlen(*nonce) + 40; + char *authhdr = malloc(hdrlen); + snprintf(authhdr, hdrlen, "Digest realm=\"raop\", nonce=\"%s\"", *nonce); + msg_add_header(resp, "WWW-Authenticate", authhdr); + free(authhdr); + return 1; +} + +void rtsp_conversation_thread_cleanup_function(void *arg) { + rtsp_conn_info *conn = (rtsp_conn_info *)arg; + int oldState; + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); + + debug(2, "Connection %d: rtsp_conversation_thread_func_cleanup_function called.", + conn->connection_number); +#ifdef CONFIG_AIRPLAY_2 + // AP2 + teardown_phase_one(conn); + teardown_phase_two(conn); +#else + // AP1 + if (have_play_lock(conn)) { + teardown(conn); + release_play_lock(conn); + } +#endif + + debug(3, "Connection %d: terminating -- closing timing, control and audio sockets...", + conn->connection_number); + if (conn->control_socket) { + debug(3, "Connection %d: terminating -- closing control_socket %d.", + conn->connection_number, conn->control_socket); + close(conn->control_socket); + conn->control_socket = 0; + } + if (conn->timing_socket) { + debug(3, "Connection %d: terminating -- closing timing_socket %d.", + conn->connection_number, conn->timing_socket); + close(conn->timing_socket); + conn->timing_socket = 0; + } + if (conn->audio_socket) { + debug(3, "Connection %d: terminating -- closing audio_socket %d.", + conn->connection_number, conn->audio_socket); + close(conn->audio_socket); + conn->audio_socket = 0; + } + if (conn->fd > 0) { + debug(1, "Connection %d: terminating -- closing RTSP connection socket %d: from %s:%u to self at %s:%u.", + conn->connection_number, conn->fd, conn->client_ip_string, conn->client_rtsp_port, + conn->self_ip_string, conn->self_rtsp_port); + close(conn->fd); + conn->fd = 0; + } + if (conn->auth_nonce) { + free(conn->auth_nonce); + conn->auth_nonce = NULL; + } + +#ifdef CONFIG_AIRPLAY_2 + buf_drain(&conn->ap2_pairing_context.control_cipher_bundle.plaintext_read_buffer, -1); + buf_drain(&conn->ap2_pairing_context.control_cipher_bundle.encrypted_read_buffer, -1); + pair_cipher_free(conn->ap2_pairing_context.control_cipher_bundle.cipher_ctx); + pair_setup_free(conn->ap2_pairing_context.setup_ctx); + pair_verify_free(conn->ap2_pairing_context.verify_ctx); + if (conn->airplay_gid) { + free(conn->airplay_gid); + conn->airplay_gid = NULL; + } +#endif + + rtp_terminate(conn); + + if (conn->dacp_id) { + free(conn->dacp_id); + conn->dacp_id = NULL; + } + + if (conn->UserAgent) { + free(conn->UserAgent); + conn->UserAgent = NULL; + } + + // remove flow control and mutexes + int rc = pthread_mutex_destroy(&conn->volume_control_mutex); + if (rc) + debug(1, "Connection %d: error %d destroying volume_control_mutex.", conn->connection_number, + rc); + rc = pthread_cond_destroy(&conn->flowcontrol); + if (rc) + debug(1, "Connection %d: error %d destroying flow control condition variable.", + conn->connection_number, rc); + rc = pthread_mutex_destroy(&conn->ab_mutex); + if (rc) + debug(1, "Connection %d: error %d destroying ab_mutex.", conn->connection_number, rc); + rc = pthread_mutex_destroy(&conn->flush_mutex); + if (rc) + debug(1, "Connection %d: error %d destroying flush_mutex.", conn->connection_number, rc); + + debug(3, "Cancel watchdog thread."); + pthread_cancel(conn->player_watchdog_thread); + debug(3, "Join watchdog thread."); + pthread_join(conn->player_watchdog_thread, NULL); + debug(3, "Delete watchdog mutex."); + pthread_mutex_destroy(&conn->watchdog_mutex); + + // debug(3, "Connection %d: Checking play lock.", conn->connection_number); + // release_play_lock(conn); + + debug(2, "Connection %d: Closed.", conn->connection_number); + conn->running = 0; + pthread_setcancelstate(oldState, NULL); +} + +void msg_cleanup_function(void *arg) { + // debug(3, "msg_cleanup_function called."); + msg_free((rtsp_message **)arg); +} + +static void *rtsp_conversation_thread_func(void *pconn) { + rtsp_conn_info *conn = pconn; + + // create the watchdog mutex, initialise the watchdog time and start the watchdog thread; + conn->watchdog_bark_time = get_absolute_time_in_ns(); + pthread_mutex_init(&conn->watchdog_mutex, NULL); + pthread_create(&conn->player_watchdog_thread, NULL, &player_watchdog_thread_code, (void *)conn); + + int rc = pthread_mutex_init(&conn->flush_mutex, NULL); + if (rc) + die("Connection %d: error %d initialising flush_mutex.", conn->connection_number, rc); + rc = pthread_mutex_init(&conn->ab_mutex, NULL); + if (rc) + die("Connection %d: error %d initialising ab_mutex.", conn->connection_number, rc); + rc = pthread_cond_init(&conn->flowcontrol, NULL); + if (rc) + die("Connection %d: error %d initialising flow control condition variable.", + conn->connection_number, rc); + rc = pthread_mutex_init(&conn->volume_control_mutex, NULL); + if (rc) + die("Connection %d: error %d initialising volume_control_mutex.", conn->connection_number, rc); + + // nothing before this is cancellable + pthread_cleanup_push(rtsp_conversation_thread_cleanup_function, (void *)conn); + + rtp_initialise(conn); + char *hdr = NULL; + + enum rtsp_read_request_response reply; + + int rtsp_read_request_attempt_count = 1; // 1 means exit immediately + rtsp_message *req, *resp; + +#ifdef CONFIG_AIRPLAY_2 + conn->ap2_audio_buffer_size = 1024 * 1024 * 8; +#endif + + while (conn->stop == 0) { + int debug_level = 3; // for printing the request and response + reply = rtsp_read_request(conn, &req); + if (reply == rtsp_read_request_response_ok) { + pthread_cleanup_push(msg_cleanup_function, (void *)&req); + resp = msg_init(); + pthread_cleanup_push(msg_cleanup_function, (void *)&resp); + resp->respcode = 501; // Not Implemented + int dl = debug_level; + if ((strcmp(req->method, "OPTIONS") == 0) || + (strcmp(req->method, "POST") == + 0)) // the options message is very common, so don't log it until level 3 + dl = 3; + + if (conn->airplay_stream_category == remote_control_stream) { + debug(dl, "Connection %d (RC): Received an RTSP Packet of type \"%s\":", + conn->connection_number, req->method), + debug_log_rtsp_message(dl, NULL, req); + } else { + debug(dl, "Connection %d: Received an RTSP Packet of type \"%s\":", conn->connection_number, + req->method), + debug_log_rtsp_message(dl, NULL, req); + } + apple_challenge(conn->fd, req, resp); + hdr = msg_get_header(req, "CSeq"); + if (hdr) + msg_add_header(resp, "CSeq", hdr); + // msg_add_header(resp, "Audio-Jack-Status", "connected; type=analog"); +#ifdef CONFIG_AIRPLAY_2 + msg_add_header(resp, "Server", "AirTunes/366.0"); +#else + msg_add_header(resp, "Server", "AirTunes/105.1"); +#endif + + if ((conn->authorized == 1) || (rtsp_auth(&conn->auth_nonce, req, resp)) == 0) { + conn->authorized = 1; // it must have been authorized or didn't need a password + struct method_handler *mh; + int method_selected = 0; + for (mh = method_handlers; mh->method; mh++) { + if (!strcmp(mh->method, req->method)) { + method_selected = 1; +#ifdef CONFIG_AIRPLAY_2 + if (conn->airplay_type == ap_1) + mh->ap1_handler(conn, req, resp); + else + mh->ap2_handler(conn, req, resp); +#else + mh->handler(conn, req, resp); +#endif + break; + } + } + if (method_selected == 0) { + debug(1, + "Connection %d: Unrecognised and unhandled rtsp request \"%s\". HTTP Response Code " + "501 (\"Not Implemented\") returned.", + conn->connection_number, req->method); + + int y = req->contentlength; + if (y > 0) { + char obf[4096]; + if (y > 4096) + y = 4096; + char *p = req->content; + char *obfp = obf; + int obfc; + for (obfc = 0; obfc < y; obfc++) { + snprintf(obfp, 3, "%02X", (unsigned int)*p); + p++; + obfp += 2; + }; + *obfp = 0; + debug(1, "Content: \"%s\".", obf); + } + } + } + if (conn->airplay_stream_category == remote_control_stream) { + debug(dl, "Connection %d (RC): RTSP Response:", conn->connection_number); + debug_log_rtsp_message(dl, NULL, resp); + } else { + debug(dl, "Connection %d: RTSP Response:", conn->connection_number); + debug_log_rtsp_message(dl, NULL, resp); + } + if (conn->stop == 0) { + int err = msg_write_response(conn, resp); + if (err) { + debug(1, + "Connection %d: Unable to write an RTSP message response. Terminating the " + "connection.", + conn->connection_number); + struct linger so_linger; + so_linger.l_onoff = 1; // "true" + so_linger.l_linger = 0; + err = setsockopt(conn->fd, SOL_SOCKET, SO_LINGER, &so_linger, sizeof so_linger); + if (err) + debug(1, "Could not set the RTSP socket to abort due to a write error on closing."); + conn->stop = 1; + // if (debuglev >= 1) + // debuglev = 3; // see what happens next + } + } + pthread_cleanup_pop(1); + pthread_cleanup_pop(1); + } else { + int tstop = 0; + if (reply == rtsp_read_request_response_immediate_shutdown_requested) + tstop = 1; + else if ((reply == rtsp_read_request_response_channel_closed) || + (reply == rtsp_read_request_response_read_error)) { + if (conn->player_thread) { + rtsp_read_request_attempt_count--; + if (rtsp_read_request_attempt_count == 0) { + tstop = 1; + if (reply == rtsp_read_request_response_read_error) { + struct linger so_linger; + so_linger.l_onoff = 1; // "true" + so_linger.l_linger = 0; + int err = setsockopt(conn->fd, SOL_SOCKET, SO_LINGER, &so_linger, sizeof so_linger); + if (err) + debug(1, "Could not set the RTSP socket to abort due to a read error on closing."); + } + // debuglev = 3; // see what happens next + } else { + if (reply == rtsp_read_request_response_channel_closed) + debug(2, + "Connection %d: RTSP channel unexpectedly closed -- will try again %d time(s).", + conn->connection_number, rtsp_read_request_attempt_count); + if (reply == rtsp_read_request_response_read_error) + debug(2, "Connection %d: RTSP channel read error -- will try again %d time(s).", + conn->connection_number, rtsp_read_request_attempt_count); + usleep(20000); + } + } else { + tstop = 1; + } + } else if (reply == rtsp_read_request_response_bad_packet) { + char *response_text = "RTSP/1.0 400 Bad Request\r\nServer: AirTunes/105.1\r\n\r\n"; + ssize_t reply = write(conn->fd, response_text, strlen(response_text)); + if (reply == -1) { + char errorstring[1024]; + strerror_r(errno, (char *)errorstring, sizeof(errorstring)); + debug(1, "rtsp_read_request_response_bad_packet write response error %d: \"%s\".", errno, + (char *)errorstring); + } else if (reply != (ssize_t)strlen(response_text)) { + debug(1, "rtsp_read_request_response_bad_packet write %d bytes requested but %d written.", + strlen(response_text), reply); + } + } else { + debug(1, "Connection %d: rtsp_read_request error %d, packet ignored.", + conn->connection_number, (int)reply); + } + if (tstop) { + debug(3, "Connection %d: Terminate RTSP connection.", conn->connection_number); + conn->stop = 1; + } + } + } + pthread_cleanup_pop(1); + pthread_exit(NULL); + debug(1, "Connection %d: RTSP thread exit.", conn->connection_number); +} + +/* +// this function is not thread safe. +static const char *format_address(struct sockaddr *fsa) { + static char string[INETx_ADDRSTRLEN]; + void *addr; +#ifdef AF_INET6 + if (fsa->sa_family == AF_INET6) { + struct sockaddr_in6 *sa6 = (struct sockaddr_in6 *)(fsa); + addr = &(sa6->sin6_addr); + } else +#endif + { + struct sockaddr_in *sa = (struct sockaddr_in *)(fsa); + addr = &(sa->sin_addr); + } + return inet_ntop(fsa->sa_family, addr, string, sizeof(string)); +} +*/ + +void rtsp_listen_loop_cleanup_handler(__attribute__((unused)) void *arg) { + int oldState; + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); + debug(2, "rtsp_listen_loop_cleanup_handler called."); + cancel_all_RTSP_threads(unspecified_stream_category, 0); // kill all RTSP listeners + int *sockfd = (int *)arg; + if (sockfd) { + int i; + for (i = 1; i <= sockfd[0]; i++) { + debug(2, "closing socket %d.", sockfd[i]); + close(sockfd[i]); + } + free(sockfd); + } + pthread_setcancelstate(oldState, NULL); +} + +void *rtsp_listen_loop(__attribute((unused)) void *arg) { + int oldState; + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); + struct addrinfo hints, *info, *p; + char portstr[6]; + int *sockfd = NULL; + int nsock = 0; + int i, ret; + + playing_conn = NULL; // the data structure representing the connection that has the player. + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_PASSIVE; + + snprintf(portstr, 6, "%d", config.port); + + // debug(1,"listen socket port request is \"%s\".",portstr); + + ret = getaddrinfo(NULL, portstr, &hints, &info); + if (ret) { + die("getaddrinfo failed: %s", gai_strerror(ret)); + } + + for (p = info; p; p = p->ai_next) { + ret = 0; + int fd = socket(p->ai_family, p->ai_socktype, IPPROTO_TCP); + int yes = 1; + + // Handle socket open failures if protocol unavailable (or IPV6 not handled) + if (fd != -1) { + // Set the RTSP socket to close on exec() of child processes + // otherwise background run_this_before_play_begins or run_this_after_play_ends commands + // that are sleeping prevent the daemon from being restarted because + // the listening RTSP port is still in use. + // See: https://github.com/mikebrady/shairport-sync/issues/329 + fcntl(fd, F_SETFD, FD_CLOEXEC); + ret = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); + + struct timeval tv; + tv.tv_sec = 3; // three seconds write timeout + tv.tv_usec = 0; + if (setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, (const char *)&tv, sizeof tv) == -1) + debug(1, "Error %d setting send timeout for rtsp writeback.", errno); + + if ((config.dont_check_timeout == 0) && (config.timeout != 0)) { + tv.tv_sec = config.timeout; // 120 seconds read timeout by default. + tv.tv_usec = 0; + if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (const char *)&tv, sizeof tv) == -1) + debug(1, "Error %d setting read timeout for rtsp connection.", 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 + // always. + if (p->ai_family == AF_INET6) { + ret |= setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &yes, sizeof(yes)); + } +#endif + + if (!ret) + ret = bind(fd, p->ai_addr, p->ai_addrlen); + + // one of the address families will fail on some systems that + // report its availability. do not complain. + + if (ret) { + char *family; +#ifdef AF_INET6 + if (p->ai_family == AF_INET6) { + family = "IPv6"; + } else +#endif + family = "IPv4"; + debug(1, "unable to listen on %s port %d. The error is: \"%s\".", family, config.port, + strerror(errno)); + } else { + listen(fd, 255); + nsock++; + sockfd = realloc(sockfd, (nsock + 1) * sizeof(int)); + sockfd[nsock] = fd; + sockfd[0] = nsock; // the first entry is the number of sockets in the array + } + } + + /* + listen(fd, 5); + nsock++; + sockfd = realloc(sockfd, nsock * sizeof(int)); + sockfd[nsock - 1] = fd; + */ + } + + freeaddrinfo(info); + + if (nsock) { + int maxfd = -1; + fd_set fds; + FD_ZERO(&fds); + // skip the first element in sockfd -- it's the count + for (i = 1; i <= nsock; i++) { + if (sockfd[i] > maxfd) + maxfd = sockfd[i]; + } + + char **t1 = txt_records; // ap1 test records + char **t2 = NULL; // possibly two text records +#ifdef CONFIG_AIRPLAY_2 + // make up a secondary set of text records + t2 = secondary_txt_records; // second set of text records in AirPlay 2 only +#endif + build_bonjour_strings(NULL); // no conn yet + mdns_register(t1, t2); // note that the dacp thread could still be using the mdns stuff after + // all player threads have been terminated, so mdns_unregister can't be + // in the rtsp_listen_loop cleanup. + + pthread_setcancelstate(oldState, NULL); + int acceptfd; + struct timeval tv; + pthread_cleanup_push(rtsp_listen_loop_cleanup_handler, (void *)sockfd); + do { + pthread_testcancel(); + tv.tv_sec = 60; + tv.tv_usec = 0; + + // skip the first element in sockfd -- it's the count + for (i = 1; i <= nsock; i++) + FD_SET(sockfd[i], &fds); + + ret = select(maxfd + 1, &fds, 0, 0, &tv); + if (ret < 0) { + if (errno == EINTR) + continue; + break; + } + + cleanup_threads(); + + acceptfd = -1; + // skip the first element in sockfd -- it's the count + for (i = 1; i <= nsock; i++) { + if (FD_ISSET(sockfd[i], &fds)) { + acceptfd = sockfd[i]; + break; + } + } + if (acceptfd < 0) // timeout + continue; + + rtsp_conn_info *conn = malloc(sizeof(rtsp_conn_info)); + if (conn == 0) + die("Couldn't allocate memory for an rtsp_conn_info record."); + memset(conn, 0, sizeof(rtsp_conn_info)); + conn->connection_number = RTSP_connection_index++; +#ifdef CONFIG_AIRPLAY_2 + conn->airplay_type = ap_2; // changed if an ANNOUNCE is received + conn->timing_type = ts_ptp; // changed if an ANNOUNCE is received +#endif + + socklen_t size_of_reply = sizeof(SOCKADDR); + conn->fd = accept(acceptfd, (struct sockaddr *)&conn->remote, &size_of_reply); + if (conn->fd < 0) { + debug(1, "Connection %d: New connection on port %d not accepted:", conn->connection_number, + config.port); + perror("failed to accept connection"); + free(conn); + } else { + size_of_reply = sizeof(SOCKADDR); + if (getsockname(conn->fd, (struct sockaddr *)&conn->local, &size_of_reply) == 0) { + // initialise the connection info + void *client_addr = NULL, *self_addr = NULL; + conn->connection_ip_family = conn->local.SAFAMILY; + +#ifdef AF_INET6 + if (conn->connection_ip_family == AF_INET6) { + struct sockaddr_in6 *sa6 = (struct sockaddr_in6 *)&conn->remote; + client_addr = &(sa6->sin6_addr); + conn->client_rtsp_port = ntohs(sa6->sin6_port); + + sa6 = (struct sockaddr_in6 *)&conn->local; + self_addr = &(sa6->sin6_addr); + conn->self_rtsp_port = ntohs(sa6->sin6_port); + conn->self_scope_id = sa6->sin6_scope_id; + } +#endif + if (conn->connection_ip_family == AF_INET) { + struct sockaddr_in *sa4 = (struct sockaddr_in *)&conn->remote; + client_addr = &(sa4->sin_addr); + conn->client_rtsp_port = ntohs(sa4->sin_port); + + sa4 = (struct sockaddr_in *)&conn->local; + self_addr = &(sa4->sin_addr); + conn->self_rtsp_port = ntohs(sa4->sin_port); + } + + inet_ntop(conn->connection_ip_family, client_addr, conn->client_ip_string, + sizeof(conn->client_ip_string)); + inet_ntop(conn->connection_ip_family, self_addr, conn->self_ip_string, + sizeof(conn->self_ip_string)); + + debug(2, "Connection %d: New connection from %s:%u to self at %s:%u.", + conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, + conn->self_ip_string, conn->self_rtsp_port); + conn->connection_start_time = get_absolute_time_in_ns(); + } else { + debug(1, "Error figuring out Shairport Sync's own IP number."); + } + + ret = pthread_create(&conn->thread, NULL, rtsp_conversation_thread_func, + conn); // also acts as a memory barrier + if (ret) { + char errorstring[1024]; + strerror_r(ret, (char *)errorstring, sizeof(errorstring)); + die("Connection %d: cannot create an RTSP conversation thread. Error %d: \"%s\".", + conn->connection_number, ret, (char *)errorstring); + } + debug(3, "Successfully created RTSP receiver thread %d.", conn->connection_number); + conn->running = 1; // this must happen before the thread is tracked + track_thread(conn); + } + } while (1); + pthread_cleanup_pop(1); // should never happen + } else { + warn("could not establish a service on port %d -- program terminating. Is another instance of " + "Shairport Sync running on this device?", + config.port); + } + debug(1, "Oops -- fell out of the RTSP select loop"); + pthread_exit(NULL); +} diff --git a/player.c b/player.c index c6432eec..ec282c1c 100644 --- a/player.c +++ b/player.c @@ -2741,24 +2741,32 @@ void *player_thread_func(void *arg) { #ifdef CONFIG_AIRPLAY_2 if (conn->airplay_stream_type == realtime_stream) { if (conn->airplay_type == ap_1) { +#ifdef CONFIG_METADATA_HUB send_ssnc_metadata('styp', "Classic", strlen("Classic"), 1); +#endif if (config.statistics_requested) inform("Connection %d: Playback Started at frame %" PRId64 " -- AirPlay 1 Compatible.", conn->connection_number, inframe->given_timestamp); } else { +#ifdef CONFIG_METADATA_HUB send_ssnc_metadata('styp', "Realtime", strlen("Realtime"), 1); +#endif if (config.statistics_requested) inform("Connection %d: Playback Started at frame %" PRId64 " -- AirPlay 2 Realtime.", conn->connection_number, inframe->given_timestamp); } } else { +#ifdef CONFIG_METADATA_HUB send_ssnc_metadata('styp', "Buffered", strlen("Buffered"), 1); +#endif if (config.statistics_requested) inform("Connection %d: Playback Started at frame %" PRId64 " -- AirPlay 2 Buffered.", conn->connection_number, inframe->given_timestamp); } #else +#ifdef CONFIG_METADATA_HUB send_ssnc_metadata('styp', "Classic", strlen("Classic"), 1); +#endif if (config.statistics_requested) inform("Connection %d: Playback Started at frame %" PRId64 " -- AirPlay 1.", conn->connection_number, inframe->given_timestamp); #endif From 9af924dd43bc9664b1f45e5ecd99b81d345d27cf Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 12 Nov 2022 17:45:36 +0000 Subject: [PATCH 15/51] Add OutputRate and OutputFormat properties to the D-Bus interface. --- dbus-service.c | 2 + new-rtsp.c | 5564 ----------------------------------- org.gnome.ShairportSync.xml | 2 + 3 files changed, 4 insertions(+), 5564 deletions(-) delete mode 100644 new-rtsp.c diff --git a/dbus-service.c b/dbus-service.c index 51c1b128..9c0bd718 100644 --- a/dbus-service.c +++ b/dbus-service.c @@ -1041,6 +1041,8 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name #endif shairport_sync_set_service_name(SHAIRPORT_SYNC(shairportSyncSkeleton), config.service_name); + shairport_sync_set_output_rate(SHAIRPORT_SYNC(shairportSyncSkeleton), config.output_rate); + shairport_sync_set_output_format(SHAIRPORT_SYNC(shairportSyncSkeleton), sps_format_description_string(config.output_format)); #ifdef CONFIG_AIRPLAY_2 shairport_sync_set_protocol(SHAIRPORT_SYNC(shairportSyncSkeleton), "AirPlay 2"); diff --git a/new-rtsp.c b/new-rtsp.c deleted file mode 100644 index 8d11fbc7..00000000 --- a/new-rtsp.c +++ /dev/null @@ -1,5564 +0,0 @@ -/* - * RTSP protocol handler. This file is part of Shairport Sync - * Copyright (c) James Laird 2013 - - * Modifications associated with audio synchronization, multithreading and - * metadata handling copyright (c) Mike Brady 2014-2022 - * All rights reserved. - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "activity_monitor.h" -#include "config.h" - -#ifdef CONFIG_OPENSSL -#include -#endif - -#ifdef CONFIG_MBEDTLS -#include -#include -#endif - -#ifdef CONFIG_POLARSSL -#include -#endif - -#include "common.h" -#include "player.h" -#include "rtp.h" -#include "rtsp.h" - -#ifdef CONFIG_METADATA_HUB -#include "metadata_hub.h" -#endif - -#ifdef CONFIG_MQTT -#include "mqtt.h" -#endif - -#ifdef AF_INET6 -#define INETx_ADDRSTRLEN INET6_ADDRSTRLEN -#else -#define INETx_ADDRSTRLEN INET_ADDRSTRLEN -#endif - -#ifdef CONFIG_AIRPLAY_2 -#include "pair_ap/pair.h" -#include "plist/plist.h" -#include "plist_xml_strings.h" -#include "ptp-utilities.h" -#endif - -#ifdef CONFIG_DBUS_INTERFACE -#include "dbus-service.h" -#endif - -#include "mdns.h" - -// mDNS advertisement strings - -// Create these strings and then keep them updated. -// When necessary, update the mDNS service records, using e.g. Avahi -// from these sources. - -char *txt_records[64]; -char *secondary_txt_records[64]; - -char firmware_version[64]; -char ap1_featuresString[64]; -char pkString[128]; -#ifdef CONFIG_AIRPLAY_2 -char deviceIdString[64]; -char featuresString[64]; -char statusflagsString[32]; -char piString[128]; -char gidString[128]; -#endif - -#define METADATA_SNDBUF (4 * 1024 * 1024) - -enum rtsp_read_request_response { - rtsp_read_request_response_ok, - rtsp_read_request_response_immediate_shutdown_requested, - rtsp_read_request_response_bad_packet, - rtsp_read_request_response_channel_closed, - rtsp_read_request_response_read_error, - rtsp_read_request_response_error -}; - -rtsp_conn_info *playing_conn; -rtsp_conn_info **conns; - -int metadata_running = 0; - -// always lock this when accessing the playing conn value -pthread_mutex_t playing_conn_lock = PTHREAD_MUTEX_INITIALIZER; - -// always lock this when accessing the list of connection threads -pthread_mutex_t conns_lock = PTHREAD_MUTEX_INITIALIZER; - -// every time we want to retain or release a reference count, lock it with this -// if a reference count is read as zero, it means the it's being deallocated. -static pthread_mutex_t reference_counter_lock = PTHREAD_MUTEX_INITIALIZER; - -// only one thread is allowed to use the player at once. -// it monitors the request variable (at least when interrupted) -// static pthread_mutex_t playing_mutex = PTHREAD_MUTEX_INITIALIZER; -// static int please_shutdown = 0; -// static pthread_t playing_thread = 0; - -int RTSP_connection_index = 1; - -#ifdef CONFIG_METADATA -typedef struct { - pthread_mutex_t pc_queue_lock; - pthread_cond_t pc_queue_item_added_signal; - pthread_cond_t pc_queue_item_removed_signal; - char *name; - size_t item_size; // number of bytes in each item - uint32_t count; // number of items in the queue - uint32_t capacity; // maximum number of items - uint32_t toq; // first item to take - uint32_t eoq; // free space at end of queue - void *items; // a pointer to where the items are actually stored -} pc_queue; // producer-consumer queue -#endif - -static int msg_indexes = 1; - -typedef struct { - int index_number; - uint32_t referenceCount; // we might start using this... - unsigned int nheaders; - char *name[16]; - char *value[16]; - - uint32_t contentlength; - char *content; - - // for requests - char method[16]; - char path[256]; - - // for responses - int respcode; -} rtsp_message; - -#ifdef CONFIG_AIRPLAY_2 - -int add_pstring_to_malloc(const char *s, void **allocation, size_t *size) { - int response = 0; - void *p = *allocation; - if (p == NULL) { - p = malloc(strlen(s) + 1); - if (p == NULL) { - debug(1, "error allocating memory"); - } else { - *allocation = p; - *size = *size + strlen(s) + 1; - uint8_t *b = (uint8_t *)p; - *b = strlen(s); - p = p + 1; - memcpy(p, s, strlen(s)); - response = 1; - } - } else { - p = realloc(p, *size + strlen(s) + 1); - if (p == NULL) { // assuming we never allocate a zero byte space - debug(1, "error reallocating memory"); - } else { - *allocation = p; - uint8_t *b = (uint8_t *)p + *size; - *b = strlen(s); - p = p + *size + 1; - memcpy(p, s, strlen(s)); - *size = *size + strlen(s) + 1; - response = 1; - } - } - return response; -} - -static void pkString_make(char *str, size_t str_size, const char *device_id) { - uint8_t public_key[32]; - if (str_size < 2 * sizeof(public_key) + 1) { - warn("Insufficient string size"); - str[0] = '\0'; - return; - } - pair_public_key_get(PAIR_SERVER_HOMEKIT, public_key, device_id); - char *ptr = str; - for (size_t i = 0; i < sizeof(public_key); i++) - ptr += sprintf(ptr, "%02x", public_key[i]); -} -#endif - -#ifdef CONFIG_AIRPLAY_2 -void build_bonjour_strings(rtsp_conn_info *conn) { -#else -void build_bonjour_strings(__attribute((unused)) rtsp_conn_info *conn) { -#endif - - int entry_number = 0; - - // make up a firmware version -#ifdef CONFIG_USE_GIT_VERSION_STRING - if (git_version_string[0] != '\0') - snprintf(firmware_version, sizeof(firmware_version), "fv=%s", git_version_string); - else -#endif - snprintf(firmware_version, sizeof(firmware_version), "fv=%s", PACKAGE_VERSION); - -#ifdef CONFIG_AIRPLAY_2 - uint64_t features_hi = config.airplay_features; - features_hi = (features_hi >> 32) & 0xffffffff; - uint64_t features_lo = config.airplay_features; - features_lo = features_lo & 0xffffffff; - snprintf(ap1_featuresString, sizeof(ap1_featuresString), "ft=0x%" PRIX64 ",0x%" PRIX64 "", - features_lo, features_hi); - snprintf(pkString, sizeof(pkString), "pk="); - pkString_make(pkString + strlen("pk="), sizeof(pkString) - strlen("pk="), - config.airplay_device_id); - - txt_records[entry_number++] = "cn=0,1"; - txt_records[entry_number++] = "da=true"; - txt_records[entry_number++] = "et=0,4"; - txt_records[entry_number++] = ap1_featuresString; - txt_records[entry_number++] = firmware_version; - txt_records[entry_number++] = "md=2"; - txt_records[entry_number++] = "am=Shairport Sync"; - txt_records[entry_number++] = "sf=0x4"; - txt_records[entry_number++] = "tp=UDP"; - txt_records[entry_number++] = "vn=65537"; - txt_records[entry_number++] = "vs=366.0"; - txt_records[entry_number++] = pkString; - txt_records[entry_number++] = NULL; - -#else - // here, just replicate what happens in mdns.h when using those #defines - txt_records[entry_number++] = "sf=0x4"; - txt_records[entry_number++] = firmware_version; - txt_records[entry_number++] = "am=ShairportSync"; - txt_records[entry_number++] = "vs=105.1"; - txt_records[entry_number++] = "tp=TCP,UDP"; - txt_records[entry_number++] = "vn=65537"; -#ifdef CONFIG_METADATA - if (config.get_coverart == 0) - txt_records[entry_number++] = "md=0,2"; - else - txt_records[entry_number++] = "md=0,1,2"; -#endif - txt_records[entry_number++] = "ss=16"; - txt_records[entry_number++] = "sr=44100"; - txt_records[entry_number++] = "da=true"; - txt_records[entry_number++] = "sv=false"; - txt_records[entry_number++] = "et=0,1"; - txt_records[entry_number++] = "ek=1"; - txt_records[entry_number++] = "cn=0,1"; - txt_records[entry_number++] = "ch=2"; - txt_records[entry_number++] = "txtvers=1"; - if (config.password == 0) - txt_records[entry_number++] = "pw=false"; - else - txt_records[entry_number++] = "pw=true"; - txt_records[entry_number++] = NULL; -#endif - -#ifdef CONFIG_AIRPLAY_2 - // make up a secondary set of text records - entry_number = 0; - - secondary_txt_records[entry_number++] = "srcvers=366.0"; - snprintf(deviceIdString, sizeof(deviceIdString), "deviceid=%s", config.airplay_device_id); - secondary_txt_records[entry_number++] = deviceIdString; - snprintf(featuresString, sizeof(featuresString), "features=0x%" PRIX64 ",0x%" PRIX64 "", - features_lo, features_hi); - secondary_txt_records[entry_number++] = featuresString; - snprintf(statusflagsString, sizeof(statusflagsString), "flags=0x%" PRIX32, - config.airplay_statusflags); - - secondary_txt_records[entry_number++] = statusflagsString; - secondary_txt_records[entry_number++] = "protovers=1.1"; - secondary_txt_records[entry_number++] = "acl=0"; - secondary_txt_records[entry_number++] = "rsf=0x0"; - secondary_txt_records[entry_number++] = firmware_version; - secondary_txt_records[entry_number++] = "model=Shairport Sync"; - snprintf(piString, sizeof(piString), "pi=%s", config.airplay_pi); - secondary_txt_records[entry_number++] = piString; - if ((conn != NULL) && (conn->airplay_gid != 0)) { - snprintf(gidString, sizeof(gidString), "gid=%s", conn->airplay_gid); - } else { - snprintf(gidString, sizeof(gidString), "gid=%s", config.airplay_pi); - } - secondary_txt_records[entry_number++] = gidString; - if ((conn != NULL) && (conn->groupContainsGroupLeader != 0)) - secondary_txt_records[entry_number++] = "gcgl=1"; - else - secondary_txt_records[entry_number++] = "gcgl=0"; - if ((conn != NULL) && (conn->airplay_gid != 0)) // if it's in a group - secondary_txt_records[entry_number++] = "isGroupLeader=0"; - secondary_txt_records[entry_number++] = pkString; - secondary_txt_records[entry_number++] = NULL; -#endif -} - -#ifdef CONFIG_METADATA -typedef struct { - uint32_t type; - uint32_t code; - char *data; - uint32_t length; - rtsp_message *carrier; -} metadata_package; - -void pc_queue_init(pc_queue *the_queue, char *items, size_t item_size, uint32_t number_of_items, - const char *name) { - if (name) - debug(2, "Creating metadata queue \"%s\".", name); - else - debug(1, "Creating an unnamed metadata queue."); - pthread_mutex_init(&the_queue->pc_queue_lock, NULL); - pthread_cond_init(&the_queue->pc_queue_item_added_signal, NULL); - pthread_cond_init(&the_queue->pc_queue_item_removed_signal, NULL); - the_queue->item_size = item_size; - the_queue->items = items; - the_queue->count = 0; - the_queue->capacity = number_of_items; - the_queue->toq = 0; - the_queue->eoq = 0; - if (name == NULL) - the_queue->name = NULL; - else - the_queue->name = strdup(name); -} - -void pc_queue_delete(pc_queue *the_queue) { - if (the_queue->name) - debug(2, "Deleting metadata queue \"%s\".", the_queue->name); - else - debug(1, "Deleting an unnamed metadata queue."); - if (the_queue->name != NULL) - free(the_queue->name); - // debug(2, "destroying pc_queue_item_removed_signal"); - pthread_cond_destroy(&the_queue->pc_queue_item_removed_signal); - // debug(2, "destroying pc_queue_item_added_signal"); - pthread_cond_destroy(&the_queue->pc_queue_item_added_signal); - // debug(2, "destroying pc_queue_lock"); - pthread_mutex_destroy(&the_queue->pc_queue_lock); - // debug(2, "destroying signals and locks done"); -} - -int send_metadata(uint32_t type, uint32_t code, char *data, uint32_t length, rtsp_message *carrier, - int block); - -int send_ssnc_metadata(uint32_t code, char *data, uint32_t length, int block) { - return send_metadata('ssnc', code, data, length, NULL, block); -} - -void pc_queue_cleanup_handler(void *arg) { - // debug(1, "pc_queue_cleanup_handler called."); - pc_queue *the_queue = (pc_queue *)arg; - int rc = pthread_mutex_unlock(&the_queue->pc_queue_lock); - if (rc) - debug(1, "Error unlocking for pc_queue_add_item or pc_queue_get_item."); -} - -int pc_queue_add_item(pc_queue *the_queue, const void *the_stuff, int block) { - int response = 0; - int rc; - if (the_queue) { - if (block == 0) { - rc = debug_mutex_lock(&the_queue->pc_queue_lock, 10000, 2); - if (rc == EBUSY) - return EBUSY; - } else - rc = pthread_mutex_lock(&the_queue->pc_queue_lock); - if (rc) - debug(1, "Error locking for pc_queue_add_item"); - pthread_cleanup_push(pc_queue_cleanup_handler, (void *)the_queue); - // leave this out if you want this to return if the queue is already full - // irrespective of the block flag. - /* - while (the_queue->count == the_queue->capacity) { - rc = pthread_cond_wait(&the_queue->pc_queue_item_removed_signal, - &the_queue->pc_queue_lock); if (rc) debug(1, "Error waiting for item to be removed"); - } - */ - if (the_queue->count < the_queue->capacity) { - uint32_t i = the_queue->eoq; - void *p = the_queue->items + the_queue->item_size * i; - // void * p = &the_queue->qbase + the_queue->item_size*the_queue->eoq; - memcpy(p, the_stuff, the_queue->item_size); - - // update the pointer - i++; - if (i == the_queue->capacity) - // fold pointer if necessary - i = 0; - the_queue->eoq = i; - the_queue->count++; - // debug(2,"metadata queue+ \"%s\" %d/%d.", the_queue->name, the_queue->count, - // the_queue->capacity); - if (the_queue->count == the_queue->capacity) - debug(3, "metadata queue \"%s\": is now full with %d items in it!", the_queue->name, - the_queue->count); - rc = pthread_cond_signal(&the_queue->pc_queue_item_added_signal); - if (rc) - debug(1, "metadata queue \"%s\": error signalling after pc_queue_add_item", - the_queue->name); - } else { - response = EWOULDBLOCK; // a bit arbitrary, this. - debug(3, - "metadata queue \"%s\": is already full with %d items in it. Not adding this item to " - "the queue.", - the_queue->name, the_queue->count); - } - pthread_cleanup_pop(1); // unlock the queue lock. - } else { - debug(1, "Adding an item to a NULL queue"); - } - return response; -} - -int pc_queue_get_item(pc_queue *the_queue, void *the_stuff) { - int rc; - if (the_queue) { - rc = pthread_mutex_lock(&the_queue->pc_queue_lock); - if (rc) - debug(1, "metadata queue \"%s\": error locking for pc_queue_get_item", the_queue->name); - pthread_cleanup_push(pc_queue_cleanup_handler, (void *)the_queue); - while (the_queue->count == 0) { - rc = pthread_cond_wait(&the_queue->pc_queue_item_added_signal, &the_queue->pc_queue_lock); - if (rc) - debug(1, "metadata queue \"%s\": error waiting for item to be added", the_queue->name); - } - uint32_t i = the_queue->toq; - // void * p = &the_queue->qbase + the_queue->item_size*the_queue->toq; - void *p = the_queue->items + the_queue->item_size * i; - memcpy(the_stuff, p, the_queue->item_size); - - // update the pointer - i++; - if (i == the_queue->capacity) - // fold pointer if necessary - i = 0; - the_queue->toq = i; - the_queue->count--; - debug(3, "metadata queue- \"%s\" %d/%d.", the_queue->name, the_queue->count, - the_queue->capacity); - rc = pthread_cond_signal(&the_queue->pc_queue_item_removed_signal); - if (rc) - debug(1, "metadata queue \"%s\": error signalling after pc_queue_get_item", the_queue->name); - pthread_cleanup_pop(1); // unlock the queue lock. - } else { - debug(1, "Removing an item from a NULL queue"); - } - return 0; -} - -#endif - -void lock_player() { debug_mutex_lock(&playing_conn_lock, 1000000, 3); } - -void unlock_player() { debug_mutex_unlock(&playing_conn_lock, 3); } - -int have_play_lock(rtsp_conn_info *conn) { - int response = 0; - lock_player(); - if (playing_conn == conn) // this connection definitely has the play lock - response = 1; - unlock_player(); - return response; -} - -/* -// return 0 if the playing_conn isn't already locked by someone else and if -// it belongs to the conn passed in. -// remember to release it! -int try_to_get_and_lock_playing_conn(rtsp_conn_info **conn) { - int response = -1; - if (pthread_mutex_trylock(&playing_conn_lock) == 0) { - response = 0; - *conn = playing_conn; - } - return response; -} - - -void release_hold_on_play_lock(__attribute__((unused)) rtsp_conn_info *conn) { - pthread_mutex_unlock(&playing_conn_lock); -} - -*/ - -// make conn no longer the playing_conn -void release_play_lock(rtsp_conn_info *conn) { - if (conn != NULL) - debug(2, "Connection %d: release play lock.", conn->connection_number); - else - debug(2, "Release play lock."); - lock_player(); - if (playing_conn == conn) { // if we have the player - if (conn != NULL) - debug(2, "Connection %d: play lock released.", conn->connection_number); - else - debug(2, "Play lock released."); - playing_conn = NULL; // let it go - } - unlock_player(); -} - -// make conn the playing_conn, and kill the current session if permitted -int get_play_lock(rtsp_conn_info *conn, int allow_session_interruption) { - if (conn != NULL) - debug(2, "Connection %d: request play lock.", conn->connection_number); - else if (playing_conn != NULL) - debug(2, "Connection %d: request release.", playing_conn->connection_number); - else - debug(2, "Request release of non-existent player."); - // returns -1 if it failed, 0 if it succeeded and 1 if it succeeded but - // interrupted an existing session - int response = 0; - - int have_the_player = 0; - int should_wait = 0; // this will be true if you're trying to break in to the current session - int interrupting_current_session = 0; - - // try to become the current playing_conn - - lock_player(); // don't let it change while we are looking at it... - - if (playing_conn == NULL) { - playing_conn = conn; - have_the_player = 1; - } else if (playing_conn == conn) { - have_the_player = 1; - if (conn != NULL) - warn("Duplicate attempt to acquire the player by the same connection, by the look of it!"); - } else if (playing_conn->stop) { - debug(1, "Connection %d: Waiting for Connection %d to stop playing.", conn->connection_number, - playing_conn->connection_number); - should_wait = 1; - } else if (allow_session_interruption != 0) { - debug(2, "Asking Connection %d to stop playing.", playing_conn->connection_number); - playing_conn->stop = 1; - interrupting_current_session = 1; - should_wait = 1; - pthread_cancel(playing_conn->thread); // asking the RTSP thread to exit - } - unlock_player(); - - if (should_wait) { - int time_remaining = 3000000; // must be signed, as it could go negative... - - while ((time_remaining > 0) && (have_the_player == 0)) { - lock_player(); // get it - if (playing_conn == NULL) { - playing_conn = conn; - have_the_player = 1; - } - unlock_player(); - - if (have_the_player == 0) { - usleep(100000); - time_remaining -= 100000; - } - } - if ((have_the_player == 1) && (interrupting_current_session == 1)) { - if (conn != NULL) - debug(2, "Connection %d: Got player lock", conn->connection_number); - else - debug(2, "Player released."); - response = 1; - } else { - debug(2, "Connection %d: failed to get player lock after waiting.", conn->connection_number); - response = -1; - } - } - - if ((have_the_player == 1) && (interrupting_current_session == 0)) { - if (conn != NULL) - debug(2, "Connection %d: Got player lock.", conn->connection_number); - else - debug(2, "Player released."); - response = 0; - } - return response; -} - -void player_watchdog_thread_cleanup_handler(void *arg) { - rtsp_conn_info *conn = (rtsp_conn_info *)arg; - debug(3, "Connection %d: Watchdog Exit.", conn->connection_number); -} - -void *player_watchdog_thread_code(void *arg) { - pthread_cleanup_push(player_watchdog_thread_cleanup_handler, arg); - rtsp_conn_info *conn = (rtsp_conn_info *)arg; - do { - usleep(2000000); // check every two seconds - // debug(3, "Connection %d: Check the thread is doing something...", conn->connection_number); -#ifdef CONFIG_AIRPLAY_2 - if ((config.dont_check_timeout == 0) && (config.timeout != 0) && (conn->airplay_type == ap_1)) { -#else - if ((config.dont_check_timeout == 0) && (config.timeout != 0)) { -#endif - debug_mutex_lock(&conn->watchdog_mutex, 1000, 0); - uint64_t last_watchdog_bark_time = conn->watchdog_bark_time; - debug_mutex_unlock(&conn->watchdog_mutex, 0); - if (last_watchdog_bark_time != 0) { - uint64_t time_since_last_bark = - (get_absolute_time_in_ns() - last_watchdog_bark_time) / 1000000000; - uint64_t ct = config.timeout; // go from int to 64-bit int - - if (time_since_last_bark >= ct) { - conn->watchdog_barks++; - if (conn->watchdog_barks == 1) { - // debuglev = 3; // tell us everything. - 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) && (conn->unfixable_error_reported == 0)) { - conn->unfixable_error_reported = 1; - command_execute(config.cmd_unfixable, "unable_to_cancel_play_session", 1); - } else { - die("an unrecoverable error, \"unable_to_cancel_play_session\", has been detected.", - conn->connection_number); - } - } - } - } - } - } while (1); - pthread_cleanup_pop(0); // should never happen - pthread_exit(NULL); -} - -// keep track of the threads we have spawned so we can join() them -static int nconns = 0; -static void track_thread(rtsp_conn_info *conn) { - debug_mutex_lock(&conns_lock, 1000000, 3); - // look for an empty slot first - int i = 0; - int found = 0; - while ((i < nconns) && (found == 0)) { - if (conns[i] == NULL) - found = 1; - else - i++; - } - if (found != 0) { - conns[i] = conn; - } else { - // make space for a new element - conns = realloc(conns, sizeof(rtsp_conn_info *) * (nconns + 1)); - if (conns) { - conns[nconns] = conn; - nconns++; - } else { - die("could not reallocate memory for conns"); - } - } - debug_mutex_unlock(&conns_lock, 3); -} - -// note: connection numbers start at 1, so an except_this_one value of zero means "all threads" -void cancel_all_RTSP_threads(airplay_stream_c stream_category, int except_this_one) { - // if the stream category is unspecified_stream_category - // all categories are elegible for cancellation - // otherwise just the category itself - debug_mutex_lock(&conns_lock, 1000000, 3); - int i; - for (i = 0; i < nconns; i++) { - if ((conns[i] != NULL) && (conns[i]->running != 0) && - (conns[i]->connection_number != except_this_one) && - ((stream_category == unspecified_stream_category) || - (stream_category == conns[i]->airplay_stream_category))) { - pthread_cancel(conns[i]->thread); - debug(2, "Connection %d: cancelled.", conns[i]->connection_number); - } - } - for (i = 0; i < nconns; i++) { - if ((conns[i] != NULL) && (conns[i]->running != 0) && - (conns[i]->connection_number != except_this_one) && - ((stream_category == unspecified_stream_category) || - (stream_category == conns[i]->airplay_stream_category))) { - pthread_join(conns[i]->thread, NULL); - debug(2, "Connection %d: joined.", conns[i]->connection_number); - free(conns[i]); - conns[i] = NULL; - } - } - debug_mutex_unlock(&conns_lock, 3); -} - -void cleanup_threads(void) { - - void *retval; - int i; - // debug(2, "culling threads."); - debug_mutex_lock(&conns_lock, 1000000, 3); - for (i = 0; i < nconns; i++) { - if ((conns[i] != NULL) && (conns[i]->running == 0)) { - debug(3, "found RTSP connection thread %d in a non-running state.", - conns[i]->connection_number); - pthread_join(conns[i]->thread, &retval); - debug(2, "Connection %d: deleted in cleanup.", conns[i]->connection_number); - free(conns[i]); - conns[i] = NULL; - } - } - debug_mutex_unlock(&conns_lock, 3); -} - -// park a null at the line ending, and return the next line pointer -// accept \r, \n, or \r\n -static char *nextline(char *in, int inbuf) { - char *out = NULL; - while (inbuf) { - if (*in == '\r') { - *in++ = 0; - out = in; - inbuf--; - } - if ((*in == '\n') && (inbuf)) { - *in++ = 0; - out = in; - } - - if (out) - break; - - in++; - inbuf--; - } - return out; -} - -void msg_retain(rtsp_message *msg) { - int rc = pthread_mutex_lock(&reference_counter_lock); - if (rc) - debug(1, "Error %d locking reference counter lock"); - if (msg > (rtsp_message *)0x00010000) { - msg->referenceCount++; - debug(3, "msg_free increment reference counter message %d to %d.", msg->index_number, - msg->referenceCount); - // debug(1,"msg_retain -- item %d reference count %d.", msg->index_number, msg->referenceCount); - rc = pthread_mutex_unlock(&reference_counter_lock); - if (rc) - debug(1, "Error %d unlocking reference counter lock"); - } else { - debug(1, "invalid rtsp_message pointer 0x%x passed to retain", (uintptr_t)msg); - } -} - -rtsp_message *msg_init(void) { - rtsp_message *msg = malloc(sizeof(rtsp_message)); - if (msg) { - memset(msg, 0, sizeof(rtsp_message)); - msg->referenceCount = 1; // from now on, any access to this must be protected with the lock - msg->index_number = msg_indexes++; - debug(3, "msg_init message %d", msg->index_number); - } else { - die("msg_init -- can not allocate memory for rtsp_message %d.", msg_indexes); - } - // debug(1,"msg_init -- create item %d.", msg->index_number); - return msg; -} - -int msg_add_header(rtsp_message *msg, char *name, char *value) { - if (msg->nheaders >= sizeof(msg->name) / sizeof(char *)) { - warn("too many headers?!"); - return 1; - } - - msg->name[msg->nheaders] = strdup(name); - msg->value[msg->nheaders] = strdup(value); - msg->nheaders++; - - return 0; -} - -char *msg_get_header(rtsp_message *msg, char *name) { - unsigned int i; - for (i = 0; i < msg->nheaders; i++) - if (!strcasecmp(msg->name[i], name)) - return msg->value[i]; - return NULL; -} - -void _debug_print_msg_headers(const char *filename, const int linenumber, int level, - rtsp_message *msg) { - unsigned int i; - if (msg->respcode != 0) - _debug(filename, linenumber, level, " Response Code: %d.", msg->respcode); - for (i = 0; i < msg->nheaders; i++) { - _debug(filename, linenumber, level, " Type: \"%s\", content: \"%s\"", msg->name[i], - msg->value[i]); - } -} - -/* -static void debug_print_msg_content(int level, rtsp_message *msg) { - if (msg->contentlength) { - char *obf = malloc(msg->contentlength * 2 + 1); - if (obf) { - char *obfp = obf; - int obfc; - for (obfc = 0; obfc < msg->contentlength; obfc++) { - snprintf(obfp, 3, "%02X", msg->content[obfc]); - obfp += 2; - }; - *obfp = 0; - debug(level, "Content (hex): \"%s\"", obf); - free(obf); - } else { - debug(level, "Can't allocate space for debug buffer"); - } - } else { - debug(level, "No content"); - } -} -*/ - -void msg_free(rtsp_message **msgh) { - debug_mutex_lock(&reference_counter_lock, 1000, 0); - if (*msgh > (rtsp_message *)0x00010000) { - rtsp_message *msg = *msgh; - msg->referenceCount--; - if (msg->referenceCount) - debug(3, "msg_free decrement reference counter message %d to %d", msg->index_number, - msg->referenceCount); - if (msg->referenceCount == 0) { - unsigned int i; - for (i = 0; i < msg->nheaders; i++) { - free(msg->name[i]); - free(msg->value[i]); - } - if (msg->content) - free(msg->content); - // debug(1,"msg_free item %d -- free.",msg->index_number); - uintptr_t index = (msg->index_number) & 0xFFFF; - if (index == 0) - index = 0x10000; // ensure it doesn't fold to zero. - *msgh = - (rtsp_message *)(index); // put a version of the index number of the freed message in here - debug(3, "msg_free freed message %d", msg->index_number); - free(msg); - } else { - // debug(1,"msg_free item %d -- decrement reference to - // %d.",msg->index_number,msg->referenceCount); - } - } else if (*msgh != NULL) { - debug(1, - "msg_free: error attempting to free an allocated but already-freed rtsp_message, number " - "%d.", - (uintptr_t)*msgh); - } - debug_mutex_unlock(&reference_counter_lock, 0); -} - -int msg_handle_line(rtsp_message **pmsg, char *line) { - rtsp_message *msg = *pmsg; - - if (!msg) { - msg = msg_init(); - *pmsg = msg; - char *sp, *p; - sp = NULL; // this is to quieten a compiler warning - - debug(3, "RTSP Message Received: \"%s\".", line); - - p = strtok_r(line, " ", &sp); - if (!p) - goto fail; - strncpy(msg->method, p, sizeof(msg->method) - 1); - - p = strtok_r(NULL, " ", &sp); - if (!p) - goto fail; - strncpy(msg->path, p, sizeof(msg->path) - 1); - - p = strtok_r(NULL, " ", &sp); - if (!p) - goto fail; - if ((strcmp(p, "RTSP/1.0")) && (strcmp(p, "HTTP/1.1"))) { - debug(1,"can't find a RTSP or HTTP header in the following."); - - char obf[4096]; - char *obfp = obf; - int obfc; - for (obfc=0;obfc<20;obfc++) { - snprintf(obfp, 3, "%02X", line[obfc]); - obfp+=2; - }; - *obfp=0; - debug(1,"Funny Packet Received: \"%s\"",obf); - goto fail; - } - - return -1; - } - - if (strlen(line)) { - char *p; - p = strstr(line, ": "); - if (!p) { - warn("bad header: >>%s<<", line); - goto fail; - } - *p = 0; - p += 2; - msg_add_header(msg, line, p); - debug(3, " %s: %s.", line, p); - return -1; - } else { - char *cl = msg_get_header(msg, "Content-Length"); - if (cl) - return atoi(cl); - else - return 0; - } - -fail: - debug(3, "msg_handle_line fail"); - msg_free(pmsg); - *pmsg = NULL; - return 0; -} - -#ifdef CONFIG_AIRPLAY_2 - -void add_flush_request(int flushNow, uint32_t flushFromSeq, uint32_t flushFromTS, - uint32_t flushUntilSeq, uint32_t flushUntilTS, rtsp_conn_info *conn) { - // immediate flush requests are added sequentially. Don't know how more than one could arise, TBH - flush_request_t **t = &conn->flush_requests; - int done = 0; - do { - flush_request_t *u = *t; - if ((u == NULL) || ((u->flushNow == 0) && (flushNow != 0)) || - (flushFromSeq < u->flushFromSeq) || - ((flushFromSeq == u->flushFromSeq) && (flushFromTS < u->flushFromTS))) { - flush_request_t *n = (flush_request_t *)calloc(sizeof(flush_request_t), 1); - n->flushNow = flushNow; - n->flushFromSeq = flushFromSeq; - n->flushFromTS = flushFromTS; - n->flushUntilSeq = flushUntilSeq; - n->flushUntilTS = flushUntilTS; - n->next = u; - *t = n; - done = 1; - } else { - t = &u->next; - } - } while (done == 0); -} - -void display_all_flush_requests(rtsp_conn_info *conn) { - if (conn->flush_requests == NULL) { - debug(1, "No flush requests."); - } else { - flush_request_t *t = conn->flush_requests; - do { - if (t->flushNow) { - debug(1, "immediate flush to untilSeq: %u, untilTS: %u.", t->flushUntilSeq, - t->flushUntilTS); - } else { - debug(1, "fromSeq: %u, fromTS: %u, to untilSeq: %u, untilTS: %u.", t->flushFromSeq, - t->flushFromTS, t->flushUntilSeq, t->flushUntilTS); - } - t = t->next; - } while (t != NULL); - } -} - -int rtsp_message_contains_plist(rtsp_message *message) { - int reply = 0; // assume there is no plist in the message - if ((message->contentlength >= strlen("bplist00")) && - (strstr(message->content, "bplist00") == message->content)) - reply = 1; - return reply; -} - -plist_t plist_from_rtsp_content(rtsp_message *message) { - plist_t the_plist = NULL; - if (rtsp_message_contains_plist(message)) { - plist_from_memory(message->content, message->contentlength, &the_plist); - } - return the_plist; -} - -char *plist_content(plist_t the_plist) { - // caller must free the returned character buffer - // convert it to xml format - uint32_t size; - char *plist_out = NULL; - plist_to_xml(the_plist, &plist_out, &size); - - // put it into a NUL-terminated string - char *reply = malloc(size + 1); - if (reply) { - memcpy(reply, plist_out, size); - reply[size] = '\0'; - } - if (the_plist) - plist_free(the_plist); - if (plist_out) - free(plist_out); - return reply; -} - -// caller must free the returned character buffer -char *rtsp_plist_content(rtsp_message *message) { - char *reply = NULL; - // first, check if it has binary plist content - if (rtsp_message_contains_plist(message)) { - // get the plist from the content - - plist_t the_plist = plist_from_rtsp_content(message); - - // convert it to xml format - uint32_t size; - char *plist_out = NULL; - plist_to_xml(the_plist, &plist_out, &size); - - // put it into a NUL-terminated string - reply = malloc(size + 1); - if (reply) { - memcpy(reply, plist_out, size); - reply[size] = '\0'; - } - if (the_plist) - plist_free(the_plist); - if (plist_out) - free(plist_out); - } - return reply; -} - -#endif - -void _debug_log_rtsp_message(const char *filename, const int linenumber, int level, char *prompt, - rtsp_message *message) { - if (level > debuglev) - return; - if ((prompt) && (*prompt != '\0')) // okay to pass NULL or an empty list... - _debug(filename, linenumber, level, prompt); - _debug_print_msg_headers(filename, linenumber, level, message); -#ifdef CONFIG_AIRPLAY_2 - char *plist_content = rtsp_plist_content(message); - if (plist_content) { - _debug(filename, linenumber, level, " Content Plist (as XML):\n--\n%s--", plist_content); - free(plist_content); - } else -#endif - { - char *content = message->content; - char obf[4096]; - char *obfp = obf; - uint32_t obfc; - for (obfc=0; obfccontentlength; obfc++) { - snprintf(obfp, 3, "%02X", content[obfc]); - obfp+=2; - }; - *obfp=0; - _debug(filename, linenumber, level, " Cannot find a plist. Content length: %d, content \"%s\".", - message->contentlength, obf); - - } -} - -#define debug_log_rtsp_message(level, prompt, message) \ - _debug_log_rtsp_message(__FILE__, __LINE__, level, prompt, message) -#define debug_print_msg_headers(level, message) \ - _debug_print_msg_headers(__FILE__, __LINE__, level, message) - -#ifdef CONFIG_AIRPLAY_2 -static void buf_add(sized_buffer *buf, uint8_t *in, size_t in_len) { - if (buf->length + in_len > buf->size) { - buf->size = buf->length + in_len + 2048; // Extra legroom to avoid future memcpy's - uint8_t *new = malloc(buf->size); - memcpy(new, buf->data, buf->length); - free(buf->data); - buf->data = new; - } - memcpy(buf->data + buf->length, in, in_len); - buf->length += in_len; -} - -static void buf_drain(sized_buffer *buf, ssize_t len) { - if (len < 0 || (size_t)len >= buf->length) { - free(buf->data); - memset(buf, 0, sizeof(sized_buffer)); - return; - } - memmove(buf->data, buf->data + len, buf->length - len); - buf->length -= len; -} - -static size_t buf_remove(sized_buffer *buf, uint8_t *out, size_t out_len) { - size_t bytes = (buf->length > out_len) ? out_len : buf->length; - memcpy(out, buf->data, bytes); - buf_drain(buf, bytes); - return bytes; -} - -static ssize_t read_encrypted(int fd, pair_cipher_bundle *ctx, void *buf, size_t count) { - uint8_t in[4096]; - uint8_t *plain; - size_t plain_len; - - // If there is leftover decoded content from the last pass just return that - if (ctx->plaintext_read_buffer.length > 0) { - return buf_remove(&ctx->plaintext_read_buffer, buf, count); - } - - do { - ssize_t got = read(fd, in, sizeof(in)); - if (got <= 0) - return got; - buf_add(&ctx->encrypted_read_buffer, in, got); - - ssize_t consumed = pair_decrypt(&plain, &plain_len, ctx->encrypted_read_buffer.data, - ctx->encrypted_read_buffer.length, ctx->cipher_ctx); - if (consumed < 0) - return -1; - buf_drain(&ctx->encrypted_read_buffer, consumed); - } while (plain_len == 0); - - // Fast path, avoids some memcpy + allocs in case of the normal, small message - /* if (ctx->plaintext_read_buffer.len == 0 && plain_len < count) { - memcpy(buf, plain, plain_len); - free(plain); - return plain_len; - } - */ - buf_add(&ctx->plaintext_read_buffer, plain, plain_len); - free(plain); - - return buf_remove(&ctx->plaintext_read_buffer, buf, count); -} - -static ssize_t write_encrypted(int fd, pair_cipher_bundle *ctx, const void *buf, size_t count) { - uint8_t *encrypted; - size_t encrypted_len; - - ssize_t ret = pair_encrypt(&encrypted, &encrypted_len, buf, count, ctx->cipher_ctx); - if (ret < 0) { - debug(1, pair_cipher_errmsg(ctx->cipher_ctx)); - return -1; - } - - size_t remain = encrypted_len; - while (remain > 0) { - ssize_t wrote = write(fd, encrypted + (encrypted_len - remain), remain); - if (wrote <= 0) { - free(encrypted); - return wrote; - } - remain -= wrote; - } - free(encrypted); - return count; -} - -/* -static ssize_t write_encrypted(rtsp_conn_info *conn, const void *buf, size_t count) { - uint8_t *encrypted; - size_t encrypted_len; - - ssize_t ret = - pair_encrypt(&encrypted, &encrypted_len, buf, count, conn->ap2_pairing_context.cipher_ctx); - if (ret < 0) { - debug(1, pair_cipher_errmsg(conn->ap2_pairing_context.cipher_ctx)); - return -1; - } - - size_t remain = encrypted_len; - while (remain > 0) { - ssize_t wrote = write(conn->fd, encrypted + (encrypted_len - remain), remain); - if (wrote <= 0) { - free(encrypted); - return wrote; - } - remain -= wrote; - } - free(encrypted); - return count; -} -*/ -#endif - -ssize_t timed_read_from_rtsp_connection(rtsp_conn_info *conn, uint64_t wait_time, void *buf, - size_t count) { - // note: a wait time of zero means wait forever - ssize_t result = 0; // closed - if (conn->fd > 0) { - int64_t remaining_time = 0; - uint64_t time_to_wait_to = get_absolute_time_in_ns();; - time_to_wait_to = time_to_wait_to + wait_time; - - // remaining_time will be zero if wait_time is zero - if (wait_time != 0) { - remaining_time = time_to_wait_to - get_absolute_time_in_ns(); - } - do { - struct timeval tv; - tv.tv_sec = remaining_time / 1000000000; // seconds - tv.tv_usec = (remaining_time % 1000000000) / 1000; // microseconds - if (setsockopt(conn->fd, SOL_SOCKET, SO_RCVTIMEO, (const char *)&tv, sizeof tv) != 0) { - char errorstring[1024]; - strerror_r(errno, (char *)errorstring, sizeof(errorstring)); - debug(1, "could not set time limit on timed_read_from_rtsp_connection -- error %d \"%s\".", - errno, errorstring); - } - -#ifdef CONFIG_AIRPLAY_2 - if (conn->ap2_pairing_context.control_cipher_bundle.cipher_ctx) { - conn->ap2_pairing_context.control_cipher_bundle.is_encrypted = 1; - result = read_encrypted(conn->fd, &conn->ap2_pairing_context.control_cipher_bundle, buf, count); - } else { - result = read(conn->fd, buf, count); - } -#else - result = read(conn->fd, buf, count); -#endif - if (wait_time != 0) - remaining_time = time_to_wait_to - get_absolute_time_in_ns(); - if (((result == -1) && ((errno == EAGAIN) || (errno == EWOULDBLOCK))) && (remaining_time > 0)) - debug(1,"remaining time on a timed read is %" PRId64 " ns.", remaining_time); - } while (((result == -1) && ((errno == EAGAIN) || (errno == EWOULDBLOCK))) && (remaining_time > 0)); - - } else { - debug(1,"Connection %d: attempt to read from a closed RTSP connection.",conn->connection_number); - } - return result; -} - -#ifdef CONFIG_AIRPLAY_2 -void set_client_as_ptp_clock(rtsp_conn_info *conn) { - char timing_list_message[4096] = ""; - strncat(timing_list_message, "T ", sizeof(timing_list_message) - 1 - strlen(timing_list_message)); - strncat(timing_list_message, (const char *)&conn->client_ip_string, - sizeof(timing_list_message) - 1 - strlen(timing_list_message)); - ptp_send_control_message_string(timing_list_message); -} - -void clear_ptp_clock() { - ptp_send_control_message_string("T"); -} -#endif - -ssize_t read_from_rtsp_connection(rtsp_conn_info *conn, void *buf, size_t count) { - // first try to read with a timeout, to see if there is any traffic... - // ssize_t response = timed_read_from_rtsp_connection(conn, 20000000000L, buf, count); - // actually don't use a timeout -- OwnTone doesn't supply regular traffic. - ssize_t response = timed_read_from_rtsp_connection(conn, 0, buf, count); - if ((response == -1) && ((errno == EAGAIN) || (errno == EWOULDBLOCK))) { - if (conn->rtsp_link_is_idle == 0) { - debug(1, "Connection %d: RTSP connection is idle.", conn->connection_number); - conn->rtsp_link_is_idle = 1; - conn->udp_clock_sender_is_initialised = 0; - conn->udp_clock_is_initialised = 0; - } - response = timed_read_from_rtsp_connection(conn, 0, buf, count); - } - if (conn->rtsp_link_is_idle == 1) { - conn->rtsp_link_is_idle = 0; - debug(1, "Connection %d: RTSP connection traffic has resumed.", conn->connection_number); -#ifdef CONFIG_AIRPLAY_2 - if (conn->airplay_stream_type == realtime_stream) { - conn->last_anchor_info_is_valid = 0; - conn->anchor_remote_info_is_valid = 0; - conn->first_packet_timestamp = 0; - conn->input_frame_rate_starting_point_is_valid = 0; - ab_resync(conn); - } -#else - conn->anchor_remote_info_is_valid = 0; - conn->local_to_remote_time_difference_measurement_time = 0; - conn->local_to_remote_time_difference = 0; - conn->first_packet_timestamp = 0; - conn->input_frame_rate_starting_point_is_valid = 0; - ab_resync(conn); -#endif - } - return response; -} - -enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn, rtsp_message **the_packet) { - - *the_packet = NULL; // need this for error handling - - enum rtsp_read_request_response reply = rtsp_read_request_response_ok; - ssize_t buflen = 4096; -#ifdef CONFIG_METADATA - if ((config.metadata_enabled != 0) && (config.get_coverart != 0)) - buflen = 1024 * 256; // big enough for typical picture data, which will be base64 encoded -#endif - int release_buffer = 0; // on exit, don't deallocate the buffer if everything was okay - char *buf = malloc(buflen + 1); // add a NUL at the end - if (!buf) { - warn("Connection %d: rtsp_read_request: can't get a buffer.", conn->connection_number); - return (rtsp_read_request_response_error); - } - pthread_cleanup_push(malloc_cleanup, buf); - ssize_t nread; - ssize_t inbuf = 0; - int msg_size = -1; - - while (msg_size < 0) { - if (conn->stop != 0) { - debug(3, "Connection %d: shutdown requested.", conn->connection_number); - reply = rtsp_read_request_response_immediate_shutdown_requested; - goto shutdown; - } - - nread = read_from_rtsp_connection(conn, buf + inbuf, buflen - inbuf); - - if (nread == 0) { - // a blocking read that returns zero means eof -- implies connection closed by client - debug(1, "Connection %d: RTSP connection closed by client.", - conn->connection_number); - reply = rtsp_read_request_response_channel_closed; - // Note: the socket will be closed when the thread exits - goto shutdown; - } - - if (nread < 0) { - if (errno == EINTR) - continue; - if (errno == EAGAIN) { - debug(1, "Connection %d: getting Error 11 -- EAGAIN from a blocking read!", - conn->connection_number); - continue; - } - if (errno != ECONNRESET) { - char errorstring[1024]; - strerror_r(errno, (char *)errorstring, sizeof(errorstring)); - if (errno != 0) - debug(2, "Connection %d: rtsp_read_request_response_read_error %d: \"%s\".", - conn->connection_number, errno, (char *)errorstring); - } - reply = rtsp_read_request_response_read_error; - goto shutdown; - } -/* - // this outputs the message received - { - void *pt = malloc(nread+1); - memset(pt, 0, nread+1); - memcpy(pt, buf + inbuf, nread); - debug(1, "Incoming string on RTSP port: \"%s\"",pt); - free(pt); - } - */ - - inbuf += nread; - - char *next; - while (msg_size < 0 && (next = nextline(buf, inbuf))) { - msg_size = msg_handle_line(the_packet, buf); - - if (!(*the_packet)) { - debug(1, "Connection %d: rtsp_read_request can't find an RTSP header.", - conn->connection_number); - reply = rtsp_read_request_response_bad_packet; - goto shutdown; - } - - inbuf -= next - buf; - if (inbuf) - memmove(buf, next, inbuf); - } - } - - if (msg_size > buflen) { - buf = realloc(buf, msg_size + 1); - if (!buf) { - warn("Connection %d: too much content.", conn->connection_number); - reply = rtsp_read_request_response_error; - goto shutdown; - } - buflen = msg_size; - } - - uint64_t threshold_time = - get_absolute_time_in_ns() + ((uint64_t)15000000000); // i.e. fifteen seconds from now - int warning_message_sent = 0; - - // const size_t max_read_chunk = 1024 * 1024 / 16; - while (inbuf < msg_size) { - - // we are going to read the stream in chunks and time how long it takes to - // do so. - // If it's taking too long, (and we find out about it), we will send an - // error message as - // metadata - - if (warning_message_sent == 0) { - uint64_t time_now = get_absolute_time_in_ns(); - if (time_now > threshold_time) { // it's taking too long - debug(1, "Error receiving metadata from source -- transmission seems " - "to be stalled."); -#ifdef CONFIG_METADATA - send_ssnc_metadata('stal', NULL, 0, 1); -#endif - warning_message_sent = 1; - } - } - - if (conn->stop != 0) { - debug(1, "RTSP shutdown requested."); - reply = rtsp_read_request_response_immediate_shutdown_requested; - goto shutdown; - } - size_t read_chunk = msg_size - inbuf; - // if (read_chunk > max_read_chunk) - // read_chunk = max_read_chunk; - // usleep(80000); // wait about 80 milliseconds between reads of up to max_read_chunk - nread = read_from_rtsp_connection(conn, buf + inbuf, read_chunk); - if (!nread) { - reply = rtsp_read_request_response_error; - goto shutdown; - } - if (nread < 0) { - if (errno == EINTR) - continue; - if (errno == EAGAIN) { - debug(1, "Getting Error 11 -- EAGAIN from a blocking read!"); - continue; - } - if (errno != ECONNRESET) { - char errorstring[1024]; - strerror_r(errno, (char *)errorstring, sizeof(errorstring)); - debug(1, "Connection %d: rtsp_read_request_response_read_error %d: \"%s\".", - conn->connection_number, errno, (char *)errorstring); - } - reply = rtsp_read_request_response_read_error; - goto shutdown; - } - inbuf += nread; - } - - rtsp_message *msg = *the_packet; - msg->contentlength = inbuf; - msg->content = buf; - char *jp = inbuf + buf; - *jp = '\0'; - *the_packet = msg; -shutdown: - if (reply != rtsp_read_request_response_ok) { - msg_free(the_packet); - release_buffer = 1; // allow the buffer to be released - } - pthread_cleanup_pop(release_buffer); - return reply; -} - -int msg_write_response(rtsp_conn_info *conn, rtsp_message *resp) { - char pkt[4096]; - int pktfree = sizeof(pkt); - char *p = pkt; - int n; - unsigned int i; - - struct response_t { - int code; - char *string; - }; - - struct response_t responses[] = {{200, "OK"}, - {400, "Bad Request"}, - {403, "Unauthorized"}, - {404, "Not Found"}, - {451, "Unavailable"}, - {456, "Header Field Not Valid for Resource"}, - {470, "Connection Authorization Required"}, - {500, "Internal Server Error"}, - {501, "Not Implemented"}}; - // 451 is really "Unavailable For Legal Reasons"! - int found = 0; - char *respcode_text = "Unauthorized"; - for (i = 0; i < sizeof(responses) / sizeof(struct response_t); i++) { - if (resp->respcode == responses[i].code) { - found = 1; - respcode_text = responses[i].string; - } - } - - if (found == 0) - debug(1, "can't find text for response code %d. Using \"%s\" instead.", resp->respcode, - respcode_text); - - n = snprintf(p, pktfree, "RTSP/1.0 %d %s\r\n", resp->respcode, respcode_text); - pktfree -= n; - p += n; - - for (i = 0; i < resp->nheaders; i++) { - // debug(3, " %s: %s.", resp->name[i], resp->value[i]); - n = snprintf(p, pktfree, "%s: %s\r\n", resp->name[i], resp->value[i]); - pktfree -= n; - p += n; - if (pktfree <= 1024) { - debug(1, "Attempted to write overlong RTSP packet 1"); - return -1; - } - } - - // Here, if there's content, write the Content-Length header ... - - if (resp->contentlength) { - debug(2, "Responding with content of length %d", resp->contentlength); - n = snprintf(p, pktfree, "Content-Length: %d\r\n", resp->contentlength); - pktfree -= n; - p += n; - if (pktfree <= 1024) { - debug(1, "Attempted to write overlong RTSP packet 2"); - return -2; - } - } - - n = snprintf(p, pktfree, "\r\n"); - pktfree -= n; - p += n; - - if (resp->contentlength) { - memcpy(p, resp->content, resp->contentlength); - pktfree -= resp->contentlength; - p += resp->contentlength; - } - - if (pktfree <= 1024) { - debug(1, "Attempted to write overlong RTSP packet 3"); - return -3; - } - - // here, if the link is encrypted, better do it - -#ifdef CONFIG_AIRPLAY_2 - ssize_t reply; - if (conn->ap2_pairing_context.control_cipher_bundle.is_encrypted) { - reply = - write_encrypted(conn->fd, &conn->ap2_pairing_context.control_cipher_bundle, pkt, p - pkt); - } else { - reply = write(conn->fd, pkt, p - pkt); - } -#else - ssize_t reply = write(conn->fd, pkt, p - pkt); -#endif - - if (reply == -1) { - char errorstring[1024]; - strerror_r(errno, (char *)errorstring, sizeof(errorstring)); - debug(1, "msg_write_response error %d: \"%s\".", errno, (char *)errorstring); - return -4; - } - if (reply != p - pkt) { - debug(1, "msg_write_response error -- requested bytes: %d not fully written: %d.", p - pkt, - reply); - return -5; - } - return 0; -} - -char *get_category_string(airplay_stream_c cat) { - char *category; - switch (cat) { - case unspecified_stream_category: - category = "unspecified stream"; - break; - case ptp_stream: - category = "PTP stream"; - break; - case ntp_stream: - category = "NTP stream"; - break; - case remote_control_stream: - category = "Remote Control stream"; - break; - default: - category = "Unexpected stream code"; - break; - } - return category; -} - -void handle_record_2(rtsp_conn_info *conn, __attribute((unused)) rtsp_message *req, - rtsp_message *resp) { - debug(2, "Connection %d: RECORD on %s", conn->connection_number, - get_category_string(conn->airplay_stream_category)); - // debug_log_rtsp_message(1, "RECORD incoming message", req); - resp->respcode = 200; -} - -void handle_record(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { - debug(2, "Connection %d: RECORD", conn->connection_number); - if (have_play_lock(conn)) { - if (conn->player_thread) - warn("Connection %d: RECORD: Duplicate RECORD message -- ignored", conn->connection_number); - else { - debug(1, "Connection %d: Classic AirPlay connection from %s:%u to self at %s:%u.", - conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, - conn->self_ip_string, conn->self_rtsp_port); - activity_monitor_signify_activity(1); - player_prepare_to_play(conn); - player_play(conn); // the thread better be 0 - } - - resp->respcode = 200; - // I think this is for telling the client what the absolute minimum latency - // actually is, - // and when the client specifies a latency, it should be added to this figure. - - // Thus, [the old version of] AirPlay's latency figure of 77175, when added to 11025 gives you - // exactly 88200 - // and iTunes' latency figure of 88553, when added to 11025 gives you 99578, - // pretty close to the 99400 we guessed. - - msg_add_header(resp, "Audio-Latency", "11025"); - - char *p; - uint32_t rtptime = 0; - char *hdr = msg_get_header(req, "RTP-Info"); - - if (hdr) { - // debug(1,"FLUSH message received: \"%s\".",hdr); - // get the rtp timestamp - p = strstr(hdr, "rtptime="); - if (p) { - p = strchr(p, '='); - if (p) { - rtptime = uatoi(p + 1); // unsigned integer -- up to 2^32-1 - // rtptime--; - // debug(1,"RTSP Flush Requested by handle_record: %u.",rtptime); - player_flush(rtptime, conn); - } - } - } - } else { - warn("Connection %d RECORD received without having the player (no ANNOUNCE?)", - conn->connection_number); - resp->respcode = 451; - } -} - -#ifdef CONFIG_AIRPLAY_2 - - -void handle_get_info(__attribute((unused)) rtsp_conn_info *conn, rtsp_message *req, - rtsp_message *resp) { - debug_log_rtsp_message(2, "GET /info:", req); - if (rtsp_message_contains_plist(req)) { // it's stage one - // get version of AirPlay -- it might be too old. Not using it yet. - char *hdr = msg_get_header(req, "User-Agent"); - if (hdr) { - if (strstr(hdr, "AirPlay/") == hdr) { - hdr = hdr + strlen("AirPlay/"); - // double airplay_version = 0.0; - // airplay_version = atof(hdr); - debug(2, "Connection %d: GET_INFO: Source AirPlay Version is: %s.", conn->connection_number, - hdr); - } - } - plist_t info_plist = NULL; - plist_from_memory(req->content, req->contentlength, &info_plist); - - plist_t qualifier = plist_dict_get_item(info_plist, "qualifier"); - if (qualifier == NULL) { - debug(1, "GET /info Stage 1: plist->qualifier was NULL"); - goto user_fail; - } - if (plist_array_get_size(qualifier) < 1) { - debug(1, "GET /info Stage 1: plist->qualifier array length < 1"); - goto user_fail; - } - plist_t qualifier_array_value = plist_array_get_item(qualifier, 0); - char *qualifier_array_val_cstr; - plist_get_string_val(qualifier_array_value, &qualifier_array_val_cstr); - if (qualifier_array_val_cstr == NULL) { - debug(1, "GET /info Stage 1: first item in qualifier array not a string"); - goto user_fail; - } - debug(2, "GET /info Stage 1: qualifier: %s", qualifier_array_val_cstr); - plist_free(info_plist); - free(qualifier_array_val_cstr); - - // uint8_t bt_addr[6] = {0xB8, 0x27, 0xEB, 0xB7, 0xD4, 0x0E}; - plist_t response_plist = NULL; - plist_from_xml((const char *)plists_get_info_response_xml, plists_get_info_response_xml_len, - &response_plist); - if (response_plist == NULL) { - debug(1, "GET /info Stage 1: response plist not created from XML!"); - } else { - void *qualifier_response_data = NULL; - size_t qualifier_response_data_length = 0; - if (add_pstring_to_malloc("acl=0", &qualifier_response_data, - &qualifier_response_data_length) == 0) - debug(1, "Problem"); - if (add_pstring_to_malloc(deviceIdString, &qualifier_response_data, - &qualifier_response_data_length) == 0) - debug(1, "Problem"); - if (add_pstring_to_malloc(featuresString, &qualifier_response_data, - &qualifier_response_data_length) == 0) - debug(1, "Problem"); - if (add_pstring_to_malloc("rsf=0x0", &qualifier_response_data, - &qualifier_response_data_length) == 0) - debug(1, "Problem"); - if (add_pstring_to_malloc("flags=0x4", &qualifier_response_data, - &qualifier_response_data_length) == 0) - debug(1, "Problem"); - if (add_pstring_to_malloc("model=Shairport Sync", &qualifier_response_data, - &qualifier_response_data_length) == 0) - debug(1, "Problem"); - if (add_pstring_to_malloc("manufacturer=", &qualifier_response_data, - &qualifier_response_data_length) == 0) - debug(1, "Problem"); - if (add_pstring_to_malloc("serialNumber=", &qualifier_response_data, - &qualifier_response_data_length) == 0) - debug(1, "Problem"); - if (add_pstring_to_malloc("protovers=1.1", &qualifier_response_data, - &qualifier_response_data_length) == 0) - debug(1, "Problem"); - if (add_pstring_to_malloc("srcvers=366.0", &qualifier_response_data, - &qualifier_response_data_length) == 0) - debug(1, "Problem"); - if (add_pstring_to_malloc(piString, &qualifier_response_data, - &qualifier_response_data_length) == 0) - debug(1, "Problem"); - if (add_pstring_to_malloc(gidString, &qualifier_response_data, - &qualifier_response_data_length) == 0) - debug(1, "Problem"); - if (add_pstring_to_malloc("gcgl=0", &qualifier_response_data, - &qualifier_response_data_length) == 0) - debug(1, "Problem"); - snprintf(pkString, sizeof(pkString), "pk="); - pkString_make(pkString + strlen("pk="), sizeof(pkString) - strlen("pk="), - config.airplay_device_id); - if (add_pstring_to_malloc(pkString, &qualifier_response_data, - &qualifier_response_data_length) == 0) - debug(1, "Problem"); - // debug(1,"qualifier_response_data_length: %u.", qualifier_response_data_length); - - plist_dict_set_item(response_plist, "txtAirPlay", - plist_new_data(qualifier_response_data, qualifier_response_data_length)); - - plist_dict_set_item(response_plist, "features", plist_new_uint(config.airplay_features)); - plist_dict_set_item(response_plist, "statusFlags", - plist_new_uint(config.airplay_statusflags)); - plist_dict_set_item(response_plist, "deviceID", plist_new_string(config.airplay_device_id)); - plist_dict_set_item(response_plist, "pi", plist_new_string(config.airplay_pi)); - plist_dict_set_item(response_plist, "name", plist_new_string(config.service_name)); - char *vs = get_version_string(); - // plist_dict_set_item(response_plist, "model", plist_new_string(vs)); - plist_dict_set_item(response_plist, "model", plist_new_string("Shairport Sync")); - free(vs); - // pkString_make(pkString, sizeof(pkString), config.airplay_device_id); - // plist_dict_set_item(response_plist, "pk", plist_new_string(pkString)); - plist_to_bin(response_plist, &resp->content, &resp->contentlength); - if (resp->contentlength == 0) - debug(1, "GET /info Stage 1: response bplist not created!"); - plist_free(response_plist); - free(qualifier_response_data); - } - msg_add_header(resp, "Content-Type", "application/x-apple-binary-plist"); - debug_log_rtsp_message(2, "GET /info Stage 1 Response:", resp); - resp->respcode = 200; - return; - - user_fail: - resp->respcode = 400; - return; - } else { // stage two - plist_t response_plist = NULL; - plist_from_xml((const char *)plists_get_info_response_xml, plists_get_info_response_xml_len, - &response_plist); - plist_dict_set_item(response_plist, "features", plist_new_uint(config.airplay_features)); - plist_dict_set_item(response_plist, "statusFlags", plist_new_uint(config.airplay_statusflags)); - plist_dict_set_item(response_plist, "deviceID", plist_new_string(config.airplay_device_id)); - plist_dict_set_item(response_plist, "pi", plist_new_string(config.airplay_pi)); - plist_dict_set_item(response_plist, "name", plist_new_string(config.service_name)); - char *vs = get_version_string(); - // plist_dict_set_item(response_plist, "model", plist_new_string(vs)); - plist_dict_set_item(response_plist, "model", plist_new_string("Shairport Sync")); - free(vs); - // pkString_make(pkString, sizeof(pkString), config.airplay_device_id); - // plist_dict_set_item(response_plist, "pk", plist_new_string(pkString)); - plist_to_bin(response_plist, &resp->content, &resp->contentlength); - plist_free(response_plist); - msg_add_header(resp, "Content-Type", "application/x-apple-binary-plist"); - debug_log_rtsp_message(2, "GET /info Stage 2 Response", resp); - resp->respcode = 200; - return; - } -} - -void handle_flushbuffered(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { - debug(3, "Connection %d: FLUSHBUFFERED %s : Content-Length %d", conn->connection_number, - req->path, req->contentlength); - debug_log_rtsp_message(2, "FLUSHBUFFERED request", req); - - uint64_t flushFromSeq = 0; - uint64_t flushFromTS = 0; - uint64_t flushUntilSeq = 0; - uint64_t flushUntilTS = 0; - int flushFromValid = 0; - plist_t messagePlist = plist_from_rtsp_content(req); - if (messagePlist != NULL) { - plist_t item = plist_dict_get_item(messagePlist, "flushFromSeq"); - if (item == NULL) { - debug(2, "Can't find a flushFromSeq"); - } else { - flushFromValid = 1; - plist_get_uint_val(item, &flushFromSeq); - debug(2, "flushFromSeq is %" PRId64 ".", flushFromSeq); - } - - item = plist_dict_get_item(messagePlist, "flushFromTS"); - if (item == NULL) { - if (flushFromValid != 0) - debug(1, "flushFromSeq without flushFromTS!"); - else - debug(2, "Can't find a flushFromTS"); - } else { - plist_get_uint_val(item, &flushFromTS); - if (flushFromValid == 0) - debug(1, "flushFromTS without flushFromSeq!"); - debug(2, "flushFromTS is %" PRId64 ".", flushFromTS); - } - - item = plist_dict_get_item(messagePlist, "flushUntilSeq"); - if (item == NULL) { - debug(1, "Can't find the flushUntilSeq"); - } else { - plist_get_uint_val(item, &flushUntilSeq); - debug(2, "flushUntilSeq is %" PRId64 ".", flushUntilSeq); - } - - item = plist_dict_get_item(messagePlist, "flushUntilTS"); - if (item == NULL) { - debug(1, "Can't find the flushUntilTS"); - } else { - plist_get_uint_val(item, &flushUntilTS); - debug(2, "flushUntilTS is %" PRId64 ".", flushUntilTS); - } - - debug_mutex_lock(&conn->flush_mutex, 1000, 1); - // a flush with from... components will not be followed by a setanchor (i.e. a play) - // if it's a flush that will be followed by a setanchor (i.e. a play) then stop play now. - if (flushFromValid == 0) - conn->ap2_play_enabled = 0; - - // add the exact request as made to the linked list (not used for anything but diagnostics now) - // int flushNow = 0; - // if (flushFromValid == 0) - // flushNow = 1; - // add_flush_request(flushNow, flushFromSeq, flushFromTS, flushUntilSeq, flushUntilTS, conn); - - // now, if it's an immediate flush, replace the existing request, if any - // but it if's a deferred flush and there is an existing deferred request, - // only update the flushUntil stuff -- that seems to preserve - // the intended semantics - - // so, always replace these - conn->ap2_flush_until_sequence_number = flushUntilSeq; - conn->ap2_flush_until_rtp_timestamp = flushUntilTS; - - if ((conn->ap2_flush_requested != 0) && (conn->ap2_flush_from_valid != 0) && - (flushFromValid != 0)) { - // if there is a request already, and it's a deferred request, and the current request is also - // deferred... do nothing! -- leave the starting point in place. Yeah, yeah, we know de Morgan's - // Law, but this seems clearer - } else { - conn->ap2_flush_from_sequence_number = flushFromSeq; - conn->ap2_flush_from_rtp_timestamp = flushFromTS; - } - - conn->ap2_flush_from_valid = flushFromValid; - conn->ap2_flush_requested = 1; - - // reflect the possibly updated flush request - // add_flush_request(flushNow, conn->ap2_flush_from_sequence_number, - // conn->ap2_flush_from_rtp_timestamp, conn->ap2_flush_until_sequence_number, - // conn->ap2_flush_until_rtp_timestamp, conn); - - debug_mutex_unlock(&conn->flush_mutex, 3); - - if (flushFromValid) - debug(2, "Deferred Flush Requested"); - else - debug(2, "Immediate Flush Requested"); - - plist_free(messagePlist); - // display_all_flush_requests(conn); - } - - resp->respcode = 200; -} - -void handle_setrate(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { - debug(3, "Connection %d: SETRATE %s : Content-Length %d", conn->connection_number, req->path, - req->contentlength); - debug_log_rtsp_message(2, "SETRATE request -- unimplemented", req); - resp->respcode = 501; // Not Implemented -} - -void handle_unimplemented_ap1(__attribute((unused)) rtsp_conn_info *conn, rtsp_message *req, - rtsp_message *resp) { - debug_log_rtsp_message(1, "request not recognised for AirPlay 1 operation", req); - resp->respcode = 501; -} - -void handle_setrateanchori(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { - debug(3, "Connection %d: SETRATEANCHORI %s :: Content-Length %d", conn->connection_number, - req->path, req->contentlength); - - plist_t messagePlist = plist_from_rtsp_content(req); - - if (messagePlist != NULL) { - pthread_cleanup_push(plist_cleanup, (void *)messagePlist); - plist_t item = plist_dict_get_item(messagePlist, "networkTimeSecs"); - if (item != NULL) { - plist_t item_2 = plist_dict_get_item(messagePlist, "networkTimeTimelineID"); - if (item_2 == NULL) { - debug(1, "Can't identify the Clock ID of the player."); - } else { - uint64_t nid; - plist_get_uint_val(item_2, &nid); - debug(2, "networkTimeTimelineID \"%" PRIx64 "\".", nid); - conn->networkTimeTimelineID = nid; - } - uint64_t networkTimeSecs; - plist_get_uint_val(item, &networkTimeSecs); - debug(2, "anchor networkTimeSecs is %" PRIu64 ".", networkTimeSecs); - - item = plist_dict_get_item(messagePlist, "networkTimeFrac"); - uint64_t networkTimeFrac; - plist_get_uint_val(item, &networkTimeFrac); - debug(2, "anchor networkTimeFrac is 0%" PRIu64 ".", networkTimeFrac); - // it looks like the networkTimeFrac is a fraction where the msb is work 1/2, the - // next 1/4 and so on - // now, convert the network time and fraction into nanoseconds - networkTimeFrac = networkTimeFrac >> 32; // reduce precision to about 1/4 nanosecond - networkTimeFrac = networkTimeFrac * 1000000000; - networkTimeFrac = networkTimeFrac >> 32; // we should now be left with the ns - - networkTimeSecs = networkTimeSecs * 1000000000; // turn the whole seconds into ns - uint64_t anchorTimeNanoseconds = networkTimeSecs + networkTimeFrac; - - debug(2, "anchorTimeNanoseconds looks like %" PRIu64 ".", anchorTimeNanoseconds); - - item = plist_dict_get_item(messagePlist, "rtpTime"); - uint64_t rtpTime; - - plist_get_uint_val(item, &rtpTime); - // debug(1, "anchor rtpTime is %" PRId64 ".", rtpTime); - uint32_t anchorRTPTime = rtpTime; - - int32_t added_latency = (int32_t)(config.audio_backend_latency_offset * conn->input_rate); - // debug(1,"anchorRTPTime: %" PRIu32 ", added latency: %" PRId32 ".", anchorRTPTime, - // added_latency); - set_ptp_anchor_info(conn, conn->networkTimeTimelineID, anchorRTPTime - added_latency, - anchorTimeNanoseconds); - } - - item = plist_dict_get_item(messagePlist, "rate"); - if (item != NULL) { - uint64_t rate; - plist_get_uint_val(item, &rate); - debug(3, "anchor rate 0x%016" PRIx64 ".", rate); - debug_mutex_lock(&conn->flush_mutex, 1000, 1); - pthread_cleanup_push(mutex_unlock, &conn->flush_mutex); - conn->ap2_rate = rate; - if ((rate & 1) != 0) { - debug(2, "Connection %d: Start playing, with anchor clock %" PRIx64 ".", - conn->connection_number, conn->networkTimeTimelineID); - activity_monitor_signify_activity(1); - -#ifdef CONFIG_METADATA - send_ssnc_metadata('pres', NULL, 0, 1); // resume -- contains cancellation points -#endif - conn->ap2_play_enabled = 1; - } else { - debug(2, "Connection %d: Stop playing.", conn->connection_number); - conn->ap2_play_enabled = 0; - activity_monitor_signify_activity(0); - reset_anchor_info(conn); -#ifdef CONFIG_METADATA - send_ssnc_metadata('paus', NULL, 0, 1); // pause -- contains cancellation points -#endif - if (config.output->stop) { - debug(2, "Connection %d: Stop the output backend.", conn->connection_number); - config.output->stop(); - } - } - pthread_cleanup_pop(1); // unlock the conn->flush_mutex - } - pthread_cleanup_pop(1); // plist_free the messagePlist; - } else { - debug(1, "missing plist!"); - } - resp->respcode = 200; -} - -void handle_get(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { - debug(2, "Connection %d: GET %s :: Content-Length %d", conn->connection_number, req->path, - req->contentlength); - debug_log_rtsp_message(2, "GET request", req); - if (strcmp(req->path, "/info") == 0) { - handle_get_info(conn, req, resp); - } else { - debug(1, "Unhandled GET, path \"%s\".", req->path); - resp->respcode = 501; // Not Implemented - } -} - -#else -void handle_get(__attribute((unused)) rtsp_conn_info *conn, __attribute((unused)) rtsp_message *req, - __attribute((unused)) rtsp_message *resp) { - debug(1, "Connection %d: GET %s Content-Length %d", conn->connection_number, req->path, - req->contentlength); - resp->respcode = 500; -} - -void handle_post(rtsp_conn_info *conn, rtsp_message *req, - __attribute((unused)) rtsp_message *resp) { - debug(1, "Connection %d: POST %s Content-Length %d", conn->connection_number, req->path, - req->contentlength); - resp->respcode = 500; -} -#endif - -#ifdef CONFIG_AIRPLAY_2 -struct pairings { - char device_id[PAIR_AP_DEVICE_ID_LEN_MAX]; - uint8_t public_key[32]; - - struct pairings *next; -} * pairings; - -static struct pairings *pairing_find(const char *device_id) { - for (struct pairings *pairing = pairings; pairing; pairing = pairing->next) { - if (strcmp(device_id, pairing->device_id) == 0) - return pairing; - } - return NULL; -} - -static void pairing_add(uint8_t public_key[32], const char *device_id) { - struct pairings *pairing = calloc(1, sizeof(struct pairings)); - snprintf(pairing->device_id, sizeof(pairing->device_id), "%s", device_id); - memcpy(pairing->public_key, public_key, sizeof(pairing->public_key)); - - pairing->next = pairings; - pairings = pairing; -} - -static void pairing_remove(struct pairings *pairing) { - if (pairing == pairings) { - pairings = pairing->next; - } else { - struct pairings *iter; - for (iter = pairings; iter && (iter->next != pairing); iter = iter->next) - ; /* EMPTY */ - - if (iter) - iter->next = pairing->next; - } - - free(pairing); -} - -static int pairing_add_cb(uint8_t public_key[32], const char *device_id, - void *cb_arg __attribute__((unused))) { - debug(1, "pair-add cb for %s", device_id); - - struct pairings *pairing = pairing_find(device_id); - if (pairing) { - memcpy(pairing->public_key, public_key, sizeof(pairing->public_key)); - return 0; - } - - pairing_add(public_key, device_id); - return 0; -} - -static int pairing_remove_cb(uint8_t public_key[32] __attribute__((unused)), const char *device_id, - void *cb_arg __attribute__((unused))) { - debug(1, "pair-remove cb for %s", device_id); - - struct pairings *pairing = pairing_find(device_id); - if (!pairing) { - debug(1, "pair-remove callback for unknown device"); - return -1; - } - - pairing_remove(pairing); - return 0; -} - -static void pairing_list_cb(pair_cb enum_cb, void *enum_cb_arg, - void *cb_arg __attribute__((unused))) { - debug(2, "pair-list cb"); - - for (struct pairings *pairing = pairings; pairing; pairing = pairing->next) { - enum_cb(pairing->public_key, pairing->device_id, enum_cb_arg); - } -} - -void handle_pair_add(rtsp_conn_info *conn __attribute__((unused)), rtsp_message *req, - rtsp_message *resp) { - uint8_t *body = NULL; - size_t body_len = 0; - int ret = pair_add(PAIR_SERVER_HOMEKIT, &body, &body_len, pairing_add_cb, NULL, - (const uint8_t *)req->content, req->contentlength); - if (ret < 0) { - debug(1, "pair-add returned an error"); - resp->respcode = 451; - return; - } - resp->content = (char *)body; // these will be freed when the data is sent - resp->contentlength = body_len; - msg_add_header(resp, "Content-Type", "application/octet-stream"); - debug_log_rtsp_message(2, "pair-add response", resp); -} - -void handle_pair_list(rtsp_conn_info *conn __attribute__((unused)), rtsp_message *req, - rtsp_message *resp) { - uint8_t *body = NULL; - size_t body_len = 0; - int ret = pair_list(PAIR_SERVER_HOMEKIT, &body, &body_len, pairing_list_cb, NULL, - (const uint8_t *)req->content, req->contentlength); - if (ret < 0) { - debug(1, "pair-list returned an error"); - resp->respcode = 451; - return; - } - resp->content = (char *)body; // these will be freed when the data is sent - resp->contentlength = body_len; - msg_add_header(resp, "Content-Type", "application/octet-stream"); - debug_log_rtsp_message(2, "pair-list response", resp); -} - -void handle_pair_remove(rtsp_conn_info *conn __attribute__((unused)), rtsp_message *req, - rtsp_message *resp) { - uint8_t *body = NULL; - size_t body_len = 0; - int ret = pair_remove(PAIR_SERVER_HOMEKIT, &body, &body_len, pairing_remove_cb, NULL, - (const uint8_t *)req->content, req->contentlength); - if (ret < 0) { - debug(1, "pair-remove returned an error"); - resp->respcode = 451; - return; - } - resp->content = (char *)body; // these will be freed when the data is sent - resp->contentlength = body_len; - msg_add_header(resp, "Content-Type", "application/octet-stream"); - debug_log_rtsp_message(2, "pair-remove response", resp); -} - -void handle_pair_verify(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { - int ret; - uint8_t *body = NULL; - size_t body_len = 0; - struct pair_result *result; - debug(2, "Connection %d: pair-verify Content-Length %d", conn->connection_number, - req->contentlength); - - if (!conn->ap2_pairing_context.verify_ctx) { - conn->ap2_pairing_context.verify_ctx = - pair_verify_new(PAIR_SERVER_HOMEKIT, NULL, NULL, NULL, config.airplay_device_id); - if (!conn->ap2_pairing_context.verify_ctx) { - debug(1, "Error creating verify context"); - resp->respcode = 500; // Internal Server Error - goto out; - } - } - - ret = pair_verify(&body, &body_len, conn->ap2_pairing_context.verify_ctx, - (const uint8_t *)req->content, req->contentlength); - if (ret < 0) { - debug(1, pair_verify_errmsg(conn->ap2_pairing_context.verify_ctx)); - resp->respcode = 470; // Connection Authorization Required - goto out; - } - - ret = pair_verify_result(&result, conn->ap2_pairing_context.verify_ctx); - if (ret == 0 && result->shared_secret_len > 0) { - conn->ap2_pairing_context.control_cipher_bundle.cipher_ctx = - pair_cipher_new(PAIR_SERVER_HOMEKIT, 2, result->shared_secret, result->shared_secret_len); - if (!conn->ap2_pairing_context.control_cipher_bundle.cipher_ctx) { - debug(1, "Error setting up rtsp control channel ciphering\n"); - goto out; - } - } - -out: - resp->content = (char *)body; // these will be freed when the data is sent - resp->contentlength = body_len; - if (body) - msg_add_header(resp, "Content-Type", "application/octet-stream"); - debug_log_rtsp_message(2, "pair-verify response", resp); -} - -void handle_pair_setup(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { - debug_log_rtsp_message(1, "pair-setup", req); - int ret; - uint8_t *body = NULL; - size_t body_len = 0; - struct pair_result *result; - debug(2, "Connection %d: handle_pair-setup Content-Length %d", conn->connection_number, - req->contentlength); - - if (!conn->ap2_pairing_context.setup_ctx) { - conn->ap2_pairing_context.setup_ctx = pair_setup_new(PAIR_SERVER_HOMEKIT, config.airplay_pin, - NULL, NULL, config.airplay_device_id); - if (!conn->ap2_pairing_context.setup_ctx) { - debug(1, "Error creating setup context"); - resp->respcode = 500; // Internal Server Error - goto out; - } - } - - ret = pair_setup(&body, &body_len, conn->ap2_pairing_context.setup_ctx, - (const uint8_t *)req->content, req->contentlength); - if (ret < 0) { - debug(1, pair_setup_errmsg(conn->ap2_pairing_context.setup_ctx)); - resp->respcode = 470; // Connection Authorization Required - goto out; - } - - ret = pair_setup_result(NULL, &result, conn->ap2_pairing_context.setup_ctx); - if (ret == 0 && result->shared_secret_len > 0) { - // Transient pairing completed (pair-setup step 2), prepare encryption, but - // don't activate yet, the response to this request is still plaintext - conn->ap2_pairing_context.control_cipher_bundle.cipher_ctx = - pair_cipher_new(PAIR_SERVER_HOMEKIT, 2, result->shared_secret, result->shared_secret_len); - if (!conn->ap2_pairing_context.control_cipher_bundle.cipher_ctx) { - debug(1, "Error setting up rtsp control channel ciphering\n"); - goto out; - } - } - -out: - resp->content = (char *)body; // these will be freed when the data is sent - resp->contentlength = body_len; - if (body) - msg_add_header(resp, "Content-Type", "application/octet-stream"); - debug_log_rtsp_message(1, "pair-setup response", resp); -} - -void handle_fp_setup(__attribute__((unused)) rtsp_conn_info *conn, rtsp_message *req, - rtsp_message *resp) { - - /* Fairplay magic */ - static uint8_t server_fp_reply1[] = - "\x46\x50\x4c\x59\x03\x01\x02\x00\x00\x00\x00\x82\x02\x00\x0f\x9f\x3f\x9e\x0a" - "\x25\x21\xdb\xdf\x31\x2a\xb2\xbf\xb2\x9e\x8d\x23\x2b\x63\x76\xa8\xc8\x18\x70" - "\x1d\x22\xae\x93\xd8\x27\x37\xfe\xaf\x9d\xb4\xfd\xf4\x1c\x2d\xba\x9d\x1f\x49" - "\xca\xaa\xbf\x65\x91\xac\x1f\x7b\xc6\xf7\xe0\x66\x3d\x21\xaf\xe0\x15\x65\x95" - "\x3e\xab\x81\xf4\x18\xce\xed\x09\x5a\xdb\x7c\x3d\x0e\x25\x49\x09\xa7\x98\x31" - "\xd4\x9c\x39\x82\x97\x34\x34\xfa\xcb\x42\xc6\x3a\x1c\xd9\x11\xa6\xfe\x94\x1a" - "\x8a\x6d\x4a\x74\x3b\x46\xc3\xa7\x64\x9e\x44\xc7\x89\x55\xe4\x9d\x81\x55\x00" - "\x95\x49\xc4\xe2\xf7\xa3\xf6\xd5\xba"; - static uint8_t server_fp_reply2[] = - "\x46\x50\x4c\x59\x03\x01\x02\x00\x00\x00\x00\x82\x02\x01\xcf\x32\xa2\x57\x14" - "\xb2\x52\x4f\x8a\xa0\xad\x7a\xf1\x64\xe3\x7b\xcf\x44\x24\xe2\x00\x04\x7e\xfc" - "\x0a\xd6\x7a\xfc\xd9\x5d\xed\x1c\x27\x30\xbb\x59\x1b\x96\x2e\xd6\x3a\x9c\x4d" - "\xed\x88\xba\x8f\xc7\x8d\xe6\x4d\x91\xcc\xfd\x5c\x7b\x56\xda\x88\xe3\x1f\x5c" - "\xce\xaf\xc7\x43\x19\x95\xa0\x16\x65\xa5\x4e\x19\x39\xd2\x5b\x94\xdb\x64\xb9" - "\xe4\x5d\x8d\x06\x3e\x1e\x6a\xf0\x7e\x96\x56\x16\x2b\x0e\xfa\x40\x42\x75\xea" - "\x5a\x44\xd9\x59\x1c\x72\x56\xb9\xfb\xe6\x51\x38\x98\xb8\x02\x27\x72\x19\x88" - "\x57\x16\x50\x94\x2a\xd9\x46\x68\x8a"; - static uint8_t server_fp_reply3[] = - "\x46\x50\x4c\x59\x03\x01\x02\x00\x00\x00\x00\x82\x02\x02\xc1\x69\xa3\x52\xee" - "\xed\x35\xb1\x8c\xdd\x9c\x58\xd6\x4f\x16\xc1\x51\x9a\x89\xeb\x53\x17\xbd\x0d" - "\x43\x36\xcd\x68\xf6\x38\xff\x9d\x01\x6a\x5b\x52\xb7\xfa\x92\x16\xb2\xb6\x54" - "\x82\xc7\x84\x44\x11\x81\x21\xa2\xc7\xfe\xd8\x3d\xb7\x11\x9e\x91\x82\xaa\xd7" - "\xd1\x8c\x70\x63\xe2\xa4\x57\x55\x59\x10\xaf\x9e\x0e\xfc\x76\x34\x7d\x16\x40" - "\x43\x80\x7f\x58\x1e\xe4\xfb\xe4\x2c\xa9\xde\xdc\x1b\x5e\xb2\xa3\xaa\x3d\x2e" - "\xcd\x59\xe7\xee\xe7\x0b\x36\x29\xf2\x2a\xfd\x16\x1d\x87\x73\x53\xdd\xb9\x9a" - "\xdc\x8e\x07\x00\x6e\x56\xf8\x50\xce"; - static uint8_t server_fp_reply4[] = - "\x46\x50\x4c\x59\x03\x01\x02\x00\x00\x00\x00\x82\x02\x03\x90\x01\xe1\x72\x7e" - "\x0f\x57\xf9\xf5\x88\x0d\xb1\x04\xa6\x25\x7a\x23\xf5\xcf\xff\x1a\xbb\xe1\xe9" - "\x30\x45\x25\x1a\xfb\x97\xeb\x9f\xc0\x01\x1e\xbe\x0f\x3a\x81\xdf\x5b\x69\x1d" - "\x76\xac\xb2\xf7\xa5\xc7\x08\xe3\xd3\x28\xf5\x6b\xb3\x9d\xbd\xe5\xf2\x9c\x8a" - "\x17\xf4\x81\x48\x7e\x3a\xe8\x63\xc6\x78\x32\x54\x22\xe6\xf7\x8e\x16\x6d\x18" - "\xaa\x7f\xd6\x36\x25\x8b\xce\x28\x72\x6f\x66\x1f\x73\x88\x93\xce\x44\x31\x1e" - "\x4b\xe6\xc0\x53\x51\x93\xe5\xef\x72\xe8\x68\x62\x33\x72\x9c\x22\x7d\x82\x0c" - "\x99\x94\x45\xd8\x92\x46\xc8\xc3\x59"; - - static uint8_t server_fp_header[] = "\x46\x50\x4c\x59\x03\x01\x04\x00\x00\x00\x00\x14"; - - resp->respcode = 200; // assume it's handled - - // uint8_t *out; - // size_t out_len; - int version_pos = 4; - int mode_pos = 14; - int type_pos = 5; - int seq_pos = 6; - int setup_message_type = 1; - int setup1_message_seq = 1; - int setup2_message_seq = 3; - int setup2_suffix_len = 20; - // int ret; - - // response and len are dummy values and can be ignored - - // debug(1, "Version: %02x, mode: %02x, type: %02x, seq: %02x", req->content[version_pos], - // req->content[mode_pos], req->content[type_pos], req->content[seq_pos]); - - if (req->content[version_pos] != 3 || req->content[type_pos] != setup_message_type) { - debug(1, "Unsupported FP version."); - } - - char *response = NULL; - size_t len = 0; - - if (req->content[seq_pos] == setup1_message_seq) { - // All replies are the same length. -1 to account for the NUL byte at the end. - len = sizeof(server_fp_reply1) - 1; - - if (req->content[mode_pos] == 0) - response = memdup(server_fp_reply1, len); - if (req->content[mode_pos] == 1) - response = memdup(server_fp_reply2, len); - if (req->content[mode_pos] == 2) - response = memdup(server_fp_reply3, len); - if (req->content[mode_pos] == 3) - response = memdup(server_fp_reply4, len); - - } else if (req->content[seq_pos] == setup2_message_seq) { - // -1 to account for the NUL byte at the end. - len = sizeof(server_fp_header) - 1 + setup2_suffix_len; - response = malloc(len); - if (response) { - memcpy(response, server_fp_header, sizeof(server_fp_header) - 1); - memcpy(response + sizeof(server_fp_header) - 1, - req->content + req->contentlength - setup2_suffix_len, setup2_suffix_len); - } - } - - if (response == NULL) { - debug(1, "Cannot create a response."); - } - - resp->content = response; // these will be freed when the data is sent - resp->contentlength = len; - msg_add_header(resp, "Content-Type", "application/octet-stream"); -} - -/* - Identifier - 21cc689d-d5de-4814-872c-71d1426b57e0 - Enable_HK_Access_Control - - PublicKey - - qXJDhhL5F3OACL+HO7LVLQVdy0OJtavepjpF720PaOQ= - - Device_Name - MyDevice - Access_Control_Level - 0 -*/ -void handle_configure(rtsp_conn_info *conn __attribute__((unused)), - rtsp_message *req __attribute__((unused)), rtsp_message *resp) { - uint8_t public_key[32]; - - pair_public_key_get(PAIR_SERVER_HOMEKIT, public_key, config.airplay_device_id); - - plist_t response_plist = plist_new_dict(); - - plist_dict_set_item(response_plist, "Identifier", plist_new_string(config.airplay_pi)); - plist_dict_set_item(response_plist, "Enable_HK_Access_Control", plist_new_bool(1)); - plist_dict_set_item(response_plist, "PublicKey", - plist_new_data((const char *)public_key, sizeof(public_key))); - plist_dict_set_item(response_plist, "Device_Name", plist_new_string(config.service_name)); - plist_dict_set_item(response_plist, "Access_Control_Level", plist_new_uint(0)); - - plist_to_bin(response_plist, &resp->content, &resp->contentlength); - plist_free(response_plist); - - msg_add_header(resp, "Content-Type", "application/x-apple-binary-plist"); - debug_log_rtsp_message(2, "POST /configure response:", resp); -} - -void handle_feedback(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req, - __attribute__((unused)) rtsp_message *resp) { - debug(2, "Connection %d: POST %s Content-Length %d", conn->connection_number, req->path, - req->contentlength); - debug_log_rtsp_message(2, NULL, req); - if (conn->airplay_stream_category == remote_control_stream) { - plist_t array_plist = plist_new_array(); - - plist_t response_plist = plist_new_dict(); - plist_dict_set_item(response_plist, "streams", array_plist); - - plist_to_bin(response_plist, &resp->content, &resp->contentlength); - plist_free(response_plist); - - msg_add_header(resp, "Content-Type", "application/x-apple-binary-plist"); - debug_log_rtsp_message(2, "FEEDBACK response (remote_control_stream):", resp); - } - - /* not finished yet - plist_t payload_plist = plist_new_dict(); - plist_dict_set_item(payload_plist, "type", plist_new_uint(103)); - plist_dict_set_item(payload_plist, "sr", plist_new_real(44100.0)); - - plist_t array_plist = plist_new_array(); - plist_array_append_item(array_plist, payload_plist); - - plist_t response_plist = plist_new_dict(); - plist_dict_set_item(response_plist, "streams",array_plist); - - plist_to_bin(response_plist, &resp->content, &resp->contentlength); - plist_free(response_plist); - // plist_free(array_plist); - // plist_free(payload_plist); - - msg_add_header(resp, "Content-Type", "application/x-apple-binary-plist"); - debug_log_rtsp_message(2, "FEEDBACK response:", resp); - */ -} - -void handle_command(__attribute__((unused)) rtsp_conn_info *conn, rtsp_message *req, - __attribute__((unused)) rtsp_message *resp) { - debug(2, "Connection %d: POST %s Content-Length %d", conn->connection_number, req->path, - req->contentlength); - debug_log_rtsp_message(2, NULL, req); - if (rtsp_message_contains_plist(req)) { - plist_t command_dict = NULL; - plist_from_memory(req->content, req->contentlength, &command_dict); - if (command_dict != NULL) { - // we have a plist -- try to get the dict item keyed to "updateMRSupportedCommands" - plist_t item = plist_dict_get_item(command_dict, "type"); - if (item != NULL) { - char *typeValue = NULL; - plist_get_string_val(item, &typeValue); - if ((typeValue != NULL) && (strcmp(typeValue, "updateMRSupportedCommands") == 0)) { - item = plist_dict_get_item(command_dict, "params"); - if (item != NULL) { - // the item should be a dict - plist_t item_array = plist_dict_get_item(item, "mrSupportedCommandsFromSender"); - if (item_array != NULL) { - // here we have an array of data items - uint32_t items = plist_array_get_size(item_array); - if (items) { - uint32_t item_number; - for (item_number = 0; item_number < items; item_number++) { - plist_t the_item = plist_array_get_item(item_array, item_number); - char *buff = NULL; - uint64_t length = 0; - plist_get_data_val(the_item, &buff, &length); - // debug(1,"Item %d, length: %" PRId64 " bytes", item_number, length); - if ((buff != NULL) && (length >= strlen("bplist00")) && - (strstr(buff, "bplist00") == buff)) { - // debug(1,"Contains a plist."); - plist_t subsidiary_plist = NULL; - plist_from_memory(buff, length, &subsidiary_plist); - if (subsidiary_plist) { - char *printable_plist = plist_content(subsidiary_plist); - if (printable_plist) { - debug(3, "\n%s", printable_plist); - free(printable_plist); - } else { - debug(1, "Can't print the plist!"); - } - // plist_free(subsidiary_plist); - } else { - debug(1, "Can't access the plist!"); - } - } - if (buff != NULL) - free(buff); - } - } - } else { - debug(1, "POST /command no mrSupportedCommandsFromSender item."); - } - } else { - debug(1, "POST /command no params dict."); - } - resp->respcode = 400; // say it's a bad request - } else { - debug(1, - "POST /command plist type is \"%s\", but \"updateMRSupportedCommands\" expected.", - typeValue); - } - if (typeValue != NULL) - free(typeValue); - } else { - debug(2, "Could not find a \"type\" item."); - } - - plist_free(command_dict); - } else { - debug(1, "POST /command plist cannot be inputted."); - } - } else { - debug(1, "POST /command contains no plist"); - } -} - -void handle_audio_mode(rtsp_conn_info *conn, rtsp_message *req, - __attribute__((unused)) rtsp_message *resp) { - debug(2, "Connection %d: POST %s Content-Length %d", conn->connection_number, req->path, - req->contentlength); - debug_log_rtsp_message(2, NULL, req); -} - -void handle_post(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { - resp->respcode = 200; - if (strcmp(req->path, "/pair-setup") == 0) { - handle_pair_setup(conn, req, resp); - } else if (strcmp(req->path, "/pair-verify") == 0) { - handle_pair_verify(conn, req, resp); - } else if (strcmp(req->path, "/pair-add") == 0) { - handle_pair_add(conn, req, resp); - } else if (strcmp(req->path, "/pair-remove") == 0) { - handle_pair_remove(conn, req, resp); - } else if (strcmp(req->path, "/pair-list") == 0) { - handle_pair_list(conn, req, resp); - } else if (strcmp(req->path, "/fp-setup") == 0) { - handle_fp_setup(conn, req, resp); - } else if (strcmp(req->path, "/configure") == 0) { - handle_configure(conn, req, resp); - } else if (strcmp(req->path, "/feedback") == 0) { - handle_feedback(conn, req, resp); - } else if (strcmp(req->path, "/command") == 0) { - handle_command(conn, req, resp); - } else if (strcmp(req->path, "/audioMode") == 0) { - handle_audio_mode(conn, req, resp); - } else { - debug(1, "Connection %d: Unhandled POST %s Content-Length %d", conn->connection_number, - req->path, req->contentlength); - debug_log_rtsp_message(2, "POST request", req); - } -} - -void handle_setpeers(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { - debug(2, "Connection %d: SETPEERS %s Content-Length %d", conn->connection_number, req->path, - req->contentlength); - debug_log_rtsp_message(2, "SETPEERS request", req); -/* - char timing_list_message[4096]; - timing_list_message[0] = 'T'; - timing_list_message[1] = 0; - - // ensure the client itself is first -- it's okay if it's duplicated later - strncat(timing_list_message, " ", sizeof(timing_list_message) - 1 - strlen(timing_list_message)); - strncat(timing_list_message, (const char *)&conn->client_ip_string, - sizeof(timing_list_message) - 1 - strlen(timing_list_message)); - - plist_t addresses_array = NULL; - plist_from_memory(req->content, req->contentlength, &addresses_array); - uint32_t items = plist_array_get_size(addresses_array); - if (items) { - uint32_t item; - for (item = 0; item < items; item++) { - plist_t n = plist_array_get_item(addresses_array, item); - char *ip_address = NULL; - plist_get_string_val(n, &ip_address); - // debug(1,ip_address); - strncat(timing_list_message, " ", - sizeof(timing_list_message) - 1 - strlen(timing_list_message)); - strncat(timing_list_message, ip_address, - sizeof(timing_list_message) - 1 - strlen(timing_list_message)); - if (ip_address != NULL) - free(ip_address); - } - ptp_send_control_message_string(timing_list_message); - } - plist_free(addresses_array); -*/ - //set_client_as_ptp_clock(conn); - resp->respcode = 200; -} -#endif - -#ifndef CONFIG_AIRPLAY_2 -void handle_options(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req, - rtsp_message *resp) { - debug_log_rtsp_message(2, "OPTIONS request", req); - debug(3, "Connection %d: OPTIONS", conn->connection_number); - resp->respcode = 200; - msg_add_header(resp, "Public", - "ANNOUNCE, SETUP, RECORD, " - "PAUSE, FLUSH, TEARDOWN, " - "OPTIONS, GET_PARAMETER, SET_PARAMETER"); -} -#endif - -#ifdef CONFIG_AIRPLAY_2 - -void handle_options(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req, - rtsp_message *resp) { - debug_log_rtsp_message(2, "OPTIONS request", req); - debug(3, "Connection %d: OPTIONS", conn->connection_number); - resp->respcode = 200; - msg_add_header(resp, "Public", - "ANNOUNCE, SETUP, RECORD, " - "PAUSE, FLUSH, FLUSHBUFFERED, TEARDOWN, " - "OPTIONS, POST, GET, PUT"); -} - -void teardown_phase_one(rtsp_conn_info *conn) { - // this can be called more than once on the same connection -- - // by the player itself but also by the play session being killed - if (conn->player_thread) { - player_stop(conn); // this nulls the player_thread - activity_monitor_signify_activity(0); // inactive, and should be after command_stop() - } - if (conn->session_key) { - free(conn->session_key); - conn->session_key = NULL; - } -} - -void teardown_phase_two(rtsp_conn_info *conn) { - // we are being asked to disconnect - // this can be called more than once on the same connection -- - // by the player itself but also by the play seesion being killed - debug(2, "Connection %d: TEARDOWN %s connection.", conn->connection_number, - get_category_string(conn->airplay_stream_category)); - if (conn->airplay_stream_category == remote_control_stream) { - if (conn->rtp_data_thread) { - debug(2, "Connection %d: TEARDOWN %s Delete Data Thread.", conn->connection_number, - get_category_string(conn->airplay_stream_category)); - pthread_cancel(*conn->rtp_data_thread); - pthread_join(*conn->rtp_data_thread, NULL); - free(conn->rtp_data_thread); - conn->rtp_data_thread = NULL; - } - if (conn->data_socket) { - debug(2, "Connection %d: TEARDOWN %s Close Data Socket.", conn->connection_number, - get_category_string(conn->airplay_stream_category)); - close(conn->data_socket); - conn->data_socket = 0; - } - } - - if (conn->rtp_event_thread) { - debug(2, "Connection %d: TEARDOWN %s Delete Event Thread.", conn->connection_number, - get_category_string(conn->airplay_stream_category)); - pthread_cancel(*conn->rtp_event_thread); - pthread_join(*conn->rtp_event_thread, NULL); - free(conn->rtp_event_thread); - conn->rtp_event_thread = NULL; - } - if (conn->event_socket) { - debug(2, "Connection %d: TEARDOWN %s Close Event Socket.", conn->connection_number, - get_category_string(conn->airplay_stream_category)); - close(conn->event_socket); - conn->event_socket = 0; - } - - // if we are closing a PTP stream only, do this - if (conn->airplay_stream_category == ptp_stream) { - if (conn->airplay_gid != NULL) { - free(conn->airplay_gid); - conn->airplay_gid = NULL; - -#ifdef CONFIG_METADATA - // this is here to ensure it's only performed once during a teardown of a ptp stream - send_ssnc_metadata('disc', conn->client_ip_string, strlen(conn->client_ip_string), 1); -#endif - } - conn->groupContainsGroupLeader = 0; - config.airplay_statusflags &= (0xffffffff - (1 << 11)); // DeviceSupportsRelay - build_bonjour_strings(conn); - debug(2, "Connection %d: TEARDOWN mdns_update on %s.", conn->connection_number, - get_category_string(conn->airplay_stream_category)); - mdns_update(NULL, secondary_txt_records); - if (conn->dacp_active_remote != NULL) { - free(conn->dacp_active_remote); - conn->dacp_active_remote = NULL; - } - release_play_lock(conn); - clear_ptp_clock(); - } -} - -void handle_teardown_2(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req, - rtsp_message *resp) { - - debug_log_rtsp_message(2, "TEARDOWN: ", req); - // if (have_player(conn)) { - resp->respcode = 200; - msg_add_header(resp, "Connection", "close"); - - plist_t messagePlist = plist_from_rtsp_content(req); - if (messagePlist != NULL) { - // now see if the incoming plist contains a "streams" array - plist_t streams = plist_dict_get_item(messagePlist, "streams"); - - if (streams) { - debug(2, "Connection %d: TEARDOWN %s Close the stream.", conn->connection_number, - get_category_string(conn->airplay_stream_category)); - // we are being asked to close a stream - teardown_phase_one(conn); - plist_free(streams); - debug(2, "Connection %d: TEARDOWN %s Close the stream complete", conn->connection_number, - get_category_string(conn->airplay_stream_category)); - } else { - debug(2, "Connection %d: TEARDOWN %s Close the connection.", conn->connection_number, - get_category_string(conn->airplay_stream_category)); - teardown_phase_one(conn); // try to do phase one anyway - teardown_phase_two(conn); - debug(2, "Connection %d: TEARDOWN %s Close the connection complete", conn->connection_number, - get_category_string(conn->airplay_stream_category)); - } - //} else { - // warn("Connection %d TEARDOWN received without having the player (no ANNOUNCE?)", - // conn->connection_number); - // resp->respcode = 451; - - plist_free(messagePlist); - resp->respcode = 200; - } else { - debug(1, "Connection %d: missing plist!", conn->connection_number); - resp->respcode = 451; // don't know what to do here - } - // debug(1,"Bogus exit for valgrind -- remember to comment it out!."); - // exit(EXIT_SUCCESS); // -} -#endif - -void teardown(rtsp_conn_info *conn) { - player_stop(conn); - activity_monitor_signify_activity(0); // inactive, and should be after command_stop() - if (conn->dacp_active_remote != NULL) { - free(conn->dacp_active_remote); - conn->dacp_active_remote = NULL; - } -} - -void handle_teardown(rtsp_conn_info *conn, __attribute__((unused)) rtsp_message *req, - rtsp_message *resp) { - debug_log_rtsp_message(2, "TEARDOWN request", req); - debug(2, "Connection %d: TEARDOWN", conn->connection_number); - if (have_play_lock(conn)) { - debug( - 3, - "TEARDOWN: synchronously terminating the player thread of RTSP conversation thread %d (2).", - conn->connection_number); - teardown(conn); - release_play_lock(conn); - resp->respcode = 200; - msg_add_header(resp, "Connection", "close"); - debug(3, "TEARDOWN: successful termination of playing thread of RTSP conversation thread %d.", - conn->connection_number); - } else { - warn("Connection %d TEARDOWN received without having the player (no ANNOUNCE?)", - conn->connection_number); - resp->respcode = 451; - } - // debug(1,"Bogus exit for valgrind -- remember to comment it out!."); - // exit(EXIT_SUCCESS); -} - -void handle_flush(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { - debug_log_rtsp_message(2, "FLUSH request", req); - debug(3, "Connection %d: FLUSH", conn->connection_number); - char *p = NULL; - uint32_t rtptime = 0; - char *hdr = msg_get_header(req, "RTP-Info"); - - if (hdr) { - // debug(1,"FLUSH message received: \"%s\".",hdr); - // get the rtp timestamp - p = strstr(hdr, "rtptime="); - if (p) { - p = strchr(p, '='); - if (p) - rtptime = uatoi(p + 1); // unsigned integer -- up to 2^32-1 - } - } - debug(2, "RTSP Flush Requested: %u.", rtptime); - if (have_play_lock(conn)) { -#ifdef CONFIG_METADATA - if (p) - send_metadata('ssnc', 'flsr', p + 1, strlen(p + 1), req, 1); - else - send_metadata('ssnc', 'flsr', NULL, 0, NULL, 0); -#endif - - player_flush(rtptime, conn); // will not crash even it there is no player thread. - resp->respcode = 200; - - } else { - warn("Connection %d FLUSH %u received without having the player", conn->connection_number, - rtptime); - resp->respcode = 451; - } -} - -#ifdef CONFIG_AIRPLAY_2 - -#ifdef CONFIG_METADATA -static void check_and_send_plist_metadata(plist_t messagePlist, const char *plist_key, - uint32_t metadata_code) { - plist_t item = plist_dict_get_item(messagePlist, plist_key); - if (item) { - char *value; - plist_get_string_val(item, &value); - if (value != NULL) { - send_metadata('ssnc', metadata_code, value, strlen(value), NULL, 0); - free(value); - } - } -} -#endif - -void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { - int err; - debug(1, "Connection %d: SETUP (AirPlay 2)", conn->connection_number); - debug_log_rtsp_message(1, "SETUP (AirPlay 2) SETUP incoming message", req); - - plist_t messagePlist = plist_from_rtsp_content(req); - plist_t setupResponsePlist = plist_new_dict(); - resp->respcode = 400; - - // see if we can get a name for the client - char *clientNameString = NULL; - plist_t nameItem = plist_dict_get_item(messagePlist, "name"); - if (nameItem != NULL) { - plist_get_string_val(nameItem, &clientNameString); - } else { - clientNameString = strdup(""); - } - - // see if the incoming plist contains a "streams" array - plist_t streams = plist_dict_get_item(messagePlist, "streams"); - if (streams == NULL) { - // no "streams" plist, so it must (?) be an initial setup - debug(2, - "Connection %d SETUP: No \"streams\" array has been found -- create an event thread " - "and open a TCP port.", - conn->connection_number); - conn->airplay_stream_category = unspecified_stream_category; - // figure out what category of stream it is, by looking at the plist - plist_t timingProtocol = plist_dict_get_item(messagePlist, "timingProtocol"); - if (timingProtocol != NULL) { - char *timingProtocolString = NULL; - plist_get_string_val(timingProtocol, &timingProtocolString); - if (timingProtocolString) { - if (strcmp(timingProtocolString, "PTP") == 0) { - debug(1, "Connection %d: AP2 PTP connection from %s:%u (\"%s\") to self at %s:%u.", - conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, clientNameString, - conn->self_ip_string, conn->self_rtsp_port); - conn->airplay_stream_category = ptp_stream; - conn->timing_type = ts_ptp; -#ifdef CONFIG_METADATA - send_ssnc_metadata('conn', conn->client_ip_string, strlen(conn->client_ip_string), - 1); // before disconnecting an existing play -#endif - get_play_lock(conn, 1); // airplay 2 always allows interruption -#ifdef CONFIG_METADATA - send_ssnc_metadata('clip', conn->client_ip_string, strlen(conn->client_ip_string), 1); - send_ssnc_metadata('svip', conn->self_ip_string, strlen(conn->self_ip_string), 1); -#endif - } else if (strcmp(timingProtocolString, "NTP") == 0) { - debug(1, "Connection %d: SETUP: NTP setup from %s:%u (\"%s\") to self at %s:%u.", - conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, clientNameString, - conn->self_ip_string, conn->self_rtsp_port); - conn->airplay_stream_category = ntp_stream; - conn->timing_type = ts_ntp; - } else if (strcmp(timingProtocolString, "None") == 0) { - debug(3, "Connection %d: SETUP: a \"None\" setup detected from %s:%u (\"%s\") to self at %s:%u.", - conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, clientNameString, - conn->self_ip_string, conn->self_rtsp_port); - // now check to see if it's got the "isRemoteControlOnly" item and check it's true - plist_t isRemoteControlOnly = plist_dict_get_item(messagePlist, "isRemoteControlOnly"); - if (isRemoteControlOnly != NULL) { - uint8_t isRemoteControlOnlyBoolean = 0; - plist_get_bool_val(isRemoteControlOnly, &isRemoteControlOnlyBoolean); - if (isRemoteControlOnlyBoolean != 0) { - debug(1, "Connection %d: Remote Control connection from %s:%u (\"%s\") to self at %s:%u.", - conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, clientNameString, - conn->self_ip_string, conn->self_rtsp_port); - conn->airplay_stream_category = remote_control_stream; - } else { - debug(1, - "Connection %d: SETUP: a \"None\" setup detected, with " - "\"isRemoteControlOnly\" item set to \"false\".", - conn->connection_number); - } - } else { - debug(1, - "Connection %d: SETUP: a \"None\" setup detected, but no " - "\"isRemoteControlOnly\" item detected.", - conn->connection_number); - } - } - - // here, we know it's an initial setup and we know the kind of setup being requested - // if it's a full service PTP stream, we get groupUUID, groupContainsGroupLeader and - // timingPeerList - if (conn->airplay_stream_category == ptp_stream) { - if (ptp_shm_interface_open() != - 0) // it should be open already, but just in case it isn't... - die("Can not access the NQPTP service. Has it stopped running?"); - // clear_ptp_clock(); - debug_log_rtsp_message(2, "SETUP \"PTP\" message", req); - plist_t groupUUID = plist_dict_get_item(messagePlist, "groupUUID"); - if (groupUUID) { - char *gid = NULL; - plist_get_string_val(groupUUID, &gid); - if (gid) { - if (conn->airplay_gid) - free(conn->airplay_gid); - conn->airplay_gid = gid; // it'll be free'd later on... - } else { - debug(1, "Invalid groupUUID"); - } - } else { - debug(1, "No groupUUID in SETUP"); - } - - // now see if the group contains a group leader - plist_t groupContainsGroupLeader = - plist_dict_get_item(messagePlist, "groupContainsGroupLeader"); - if (groupContainsGroupLeader) { - uint8_t value = 0; - plist_get_bool_val(groupContainsGroupLeader, &value); - conn->groupContainsGroupLeader = value; - debug(2, "Updated groupContainsGroupLeader to %u", conn->groupContainsGroupLeader); - } else { - debug(1, "No groupContainsGroupLeader in SETUP"); - } - - char timing_list_message[4096]; - timing_list_message[0] = 'T'; - timing_list_message[1] = 0; - - // ensure the client itself is first -- it's okay if it's duplicated later - strncat(timing_list_message, " ", - sizeof(timing_list_message) - 1 - strlen(timing_list_message)); - strncat(timing_list_message, (const char *)&conn->client_ip_string, - sizeof(timing_list_message) - 1 - strlen(timing_list_message)); - - plist_t timing_peer_info = plist_dict_get_item(messagePlist, "timingPeerInfo"); - if (timing_peer_info) { - // first, get the incoming plist. - plist_t addresses_array = plist_dict_get_item(timing_peer_info, "Addresses"); - if (addresses_array) { - // iterate through the array of items - uint32_t items = plist_array_get_size(addresses_array); - if (items) { - uint32_t item; - for (item = 0; item < items; item++) { - plist_t n = plist_array_get_item(addresses_array, item); - char *ip_address = NULL; - plist_get_string_val(n, &ip_address); - // debug(1, "Timing peer: %s", ip_address); - strncat(timing_list_message, " ", - sizeof(timing_list_message) - 1 - strlen(timing_list_message)); - strncat(timing_list_message, ip_address, - sizeof(timing_list_message) - 1 - strlen(timing_list_message)); - free(ip_address); - } - } else { - debug(1, "SETUP on Connection %d: No timingPeerInfo addresses in the array.", - conn->connection_number); - } - } else { - debug(1, "SETUP on Connection %d: Can't find timingPeerInfo addresses", - conn->connection_number); - } - // make up the timing peer info list part of the response... - // debug(1,"Create timingPeerInfoPlist"); - plist_t timingPeerInfoPlist = plist_new_dict(); - plist_t addresses = plist_new_array(); // to hold the device's interfaces - plist_array_append_item(addresses, plist_new_string(conn->self_ip_string)); - // debug(1,"self ip: \"%s\"", conn->self_ip_string); - - struct ifaddrs *addrs, *iap; - getifaddrs(&addrs); - for (iap = addrs; iap != NULL; iap = iap->ifa_next) { - // debug(1, "Interface index %d, name: \"%s\"",if_nametoindex(iap->ifa_name), - // iap->ifa_name); - if ((iap->ifa_addr) && (iap->ifa_netmask) && (iap->ifa_flags & IFF_UP) && - ((iap->ifa_flags & IFF_LOOPBACK) == 0)) { - char buf[INET6_ADDRSTRLEN + 1]; // +1 for a NUL - memset(buf, 0, sizeof(buf)); - if (iap->ifa_addr->sa_family == AF_INET6) { - struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)(iap->ifa_addr); - inet_ntop(AF_INET6, (void *)&addr6->sin6_addr, buf, sizeof(buf)); - plist_array_append_item(addresses, plist_new_string(buf)); - // debug(1, "Own address IPv6: %s", buf); - - // strncat(timing_list_message, " ", - // sizeof(timing_list_message) - 1 - strlen(timing_list_message)); - // strncat(timing_list_message, buf, - // sizeof(timing_list_message) - 1 - strlen(timing_list_message)); - - } else { - struct sockaddr_in *addr = (struct sockaddr_in *)(iap->ifa_addr); - inet_ntop(AF_INET, (void *)&addr->sin_addr, buf, sizeof(buf)); - plist_array_append_item(addresses, plist_new_string(buf)); - // debug(1, "Own address IPv4: %s", buf); - - // strncat(timing_list_message, " ", - // sizeof(timing_list_message) - 1 - strlen(timing_list_message)); - // strncat(timing_list_message, buf, - // sizeof(timing_list_message) - 1 - strlen(timing_list_message)); - } - } - } - freeifaddrs(addrs); - - // debug(1,"initial timing peer command: \"%s\".", timing_list_message); - //ptp_send_control_message_string(timing_list_message); - set_client_as_ptp_clock(conn); - plist_dict_set_item(timingPeerInfoPlist, "Addresses", addresses); - plist_dict_set_item(timingPeerInfoPlist, "ID", plist_new_string(conn->self_ip_string)); - plist_dict_set_item(setupResponsePlist, "timingPeerInfo", timingPeerInfoPlist); - // get a port to use as an event port - // bind a new TCP port and get a socket - conn->local_event_port = 0; // any port - int err = bind_socket_and_port(SOCK_STREAM, conn->connection_ip_family, - conn->self_ip_string, conn->self_scope_id, - &conn->local_event_port, &conn->event_socket); - if (err) { - die("SETUP on Connection %d: Error %d: could not find a TCP port to use as an event " - "port", - conn->connection_number, err); - } - - debug(2, "Connection %d: TCP PTP event port opened: %u.", conn->connection_number, - conn->local_event_port); - - if (conn->rtp_event_thread != NULL) - debug(1, "previous rtp_event_thread allocation not freed, it seems."); - conn->rtp_event_thread = malloc(sizeof(pthread_t)); - if (conn->rtp_event_thread == NULL) - die("Couldn't allocate space for pthread_t"); - - pthread_create(conn->rtp_event_thread, NULL, &rtp_event_receiver, (void *)conn); - - plist_dict_set_item(setupResponsePlist, "eventPort", - plist_new_uint(conn->local_event_port)); - plist_dict_set_item(setupResponsePlist, "timingPort", plist_new_uint(0)); // dummy - cancel_all_RTSP_threads(unspecified_stream_category, - conn->connection_number); // kill all the other listeners - - config.airplay_statusflags |= 1 << 11; // DeviceSupportsRelay - build_bonjour_strings(conn); - debug(2, "Connection %d: SETUP mdns_update on %s.", conn->connection_number, - get_category_string(conn->airplay_stream_category)); - mdns_update(NULL, secondary_txt_records); - - resp->respcode = 200; - } else { - debug(1, "SETUP on Connection %d: PTP setup -- no timingPeerInfo plist.", - conn->connection_number); - } - -#ifdef CONFIG_METADATA - check_and_send_plist_metadata(messagePlist, "name", 'snam'); - check_and_send_plist_metadata(messagePlist, "deviceID", 'cdid'); - check_and_send_plist_metadata(messagePlist, "model", 'cmod'); - check_and_send_plist_metadata(messagePlist, "macAddress", 'cmac'); -#endif - } else if (conn->airplay_stream_category == ntp_stream) { - debug(1, "SETUP on Connection %d: ntp stream handling is not implemented!", - conn->connection_number, req); - warn("Shairport Sync can not handle NTP streams."); - } else if (conn->airplay_stream_category == remote_control_stream) { - debug_log_rtsp_message(2, "SETUP (no stream) \"isRemoteControlOnly\" message", req); - - // get a port to use as an event port - // bind a new TCP port and get a socket - conn->local_event_port = 0; // any port - int err = bind_socket_and_port(SOCK_STREAM, conn->connection_ip_family, - conn->self_ip_string, conn->self_scope_id, - &conn->local_event_port, &conn->event_socket); - if (err) { - die("SETUP on Connection %d: Error %d: could not find a TCP port to use as an event " - "port", - conn->connection_number, err); - } - - debug(2, "Connection %d SETUP (RC): TCP Remote Control event port opened: %u.", - conn->connection_number, conn->local_event_port); - if (conn->rtp_event_thread != NULL) - debug(1, - "Connection %d SETUP (RC): previous rtp_event_thread allocation not freed, it " - "seems.", - conn->connection_number); - - conn->rtp_event_thread = malloc(sizeof(pthread_t)); - if (conn->rtp_event_thread == NULL) - die("Couldn't allocate space for pthread_t"); - pthread_create(conn->rtp_event_thread, NULL, &rtp_event_receiver, (void *)conn); - plist_dict_set_item(setupResponsePlist, "eventPort", - plist_new_uint(conn->local_event_port)); - plist_dict_set_item(setupResponsePlist, "timingPort", plist_new_uint(0)); - cancel_all_RTSP_threads( - remote_control_stream, - conn->connection_number); // kill all the other remote control listeners - resp->respcode = 200; - } else { - debug(1, "SETUP on Connection %d: an unrecognised \"%s\" setup detected.", - conn->connection_number, timingProtocolString); - warn("Shairport Sync can not handle streams of this type: \"%s\".", timingProtocolString); - } - free(timingProtocolString); - } else { - debug(1, "SETUP on Connection %d: Can't retrieve timingProtocol string in initial SETUP.", - conn->connection_number); - } - } else { - debug(1, - "SETUP on Connection %d: Unrecognised SETUP incoming message from \"%s\": no " - "timingProtocol or streams plist found.", - conn->connection_number, (const char *)conn->client_ip_string); - debug_log_rtsp_message(2, "Unrecognised SETUP incoming message.", req); - warn("Unrecognised SETUP incoming message -- ignored."); - } - } else { - debug(2, "Connection %d: SETUP on %s. A \"streams\" array has been found", - conn->connection_number, get_category_string(conn->airplay_stream_category)); - if (conn->airplay_stream_category == ptp_stream) { - // get stream[0] - plist_t stream0 = plist_array_get_item(streams, 0); - - plist_t streams_array = plist_new_array(); // to hold the ports and stuff - plist_t stream0dict = plist_new_dict(); - - // get the session key -- it must have one - - plist_t item = plist_dict_get_item(stream0, "shk"); // session key - uint64_t item_value = 0; // the length - plist_get_data_val(item, (char **)&conn->session_key, &item_value); - - // more stuff - // set up a UDP control stream and thread and a UDP or TCP audio stream and thread - - // bind a new UDP port and get a socket - conn->local_ap2_control_port = 0; // any port - err = bind_socket_and_port(SOCK_DGRAM, conn->connection_ip_family, conn->self_ip_string, - conn->self_scope_id, &conn->local_ap2_control_port, - &conn->ap2_control_socket); - if (err) { - die("Error %d: could not find a UDP port to use as an ap2_control port", err); - } - debug(2, "Connection %d: UDP control port opened: %u.", conn->connection_number, - conn->local_ap2_control_port); - - pthread_create(&conn->rtp_ap2_control_thread, NULL, &rtp_ap2_control_receiver, (void *)conn); - - // get the DACP-ID and Active Remote for remote control stuff - - char *ar = msg_get_header(req, "Active-Remote"); - if (ar) { - debug(3, "Connection %d: SETUP AP2 -- Active-Remote string seen: \"%s\".", - conn->connection_number, ar); - // get the active remote - if (conn->dacp_active_remote) // this is in case SETUP was previously called - free(conn->dacp_active_remote); - conn->dacp_active_remote = strdup(ar); -#ifdef CONFIG_METADATA - send_metadata('ssnc', 'acre', ar, strlen(ar), req, 1); -#endif - } else { - debug(1, "Connection %d: SETUP AP2 no Active-Remote information the SETUP Record.", - conn->connection_number); - if (conn->dacp_active_remote) { // this is in case SETUP was previously called - free(conn->dacp_active_remote); - conn->dacp_active_remote = NULL; - } - } - - ar = msg_get_header(req, "DACP-ID"); - if (ar) { - debug(3, "Connection %d: SETUP AP2 -- DACP-ID string seen: \"%s\".", - conn->connection_number, ar); - if (conn->dacp_id) // this is in case SETUP was previously called - free(conn->dacp_id); - conn->dacp_id = strdup(ar); -#ifdef CONFIG_METADATA - send_metadata('ssnc', 'daid', ar, strlen(ar), req, 1); -#endif - } else { - debug(1, "Connection %d: SETUP AP2 doesn't include DACP-ID string information.", - conn->connection_number); - if (conn->dacp_id) { // this is in case SETUP was previously called - free(conn->dacp_id); - conn->dacp_id = NULL; - } - } - - // now, get the type of the stream. - item = plist_dict_get_item(stream0, "type"); - item_value = 0; - plist_get_uint_val(item, &item_value); - - switch (item_value) { - case 96: { - debug(1, "Connection %d. AP2 Realtime Audio Stream.", conn->connection_number); - debug_log_rtsp_message(2, "Realtime Audio Stream SETUP incoming message", req); - // get_play_lock(conn); - conn->airplay_stream_type = realtime_stream; - // bind a new UDP port and get a socket - conn->local_realtime_audio_port = 0; // any port - err = bind_socket_and_port(SOCK_DGRAM, conn->connection_ip_family, conn->self_ip_string, - conn->self_scope_id, &conn->local_realtime_audio_port, - &conn->realtime_audio_socket); - if (err) { - die("Error %d: could not find a UDP port to use as a realtime_audio port", err); - } - debug(2, "Connection %d: UDP realtime audio port opened: %u.", conn->connection_number, - conn->local_realtime_audio_port); - - pthread_create(&conn->rtp_realtime_audio_thread, NULL, &rtp_realtime_audio_receiver, - (void *)conn); - - plist_dict_set_item(stream0dict, "type", plist_new_uint(96)); - plist_dict_set_item(stream0dict, "dataPort", - plist_new_uint(conn->local_realtime_audio_port)); - - conn->stream.type = ast_apple_lossless; - debug(3, "An ALAC stream has been detected."); - - // Set reasonable connection defaults - conn->stream.fmtp[0] = 96; - conn->stream.fmtp[1] = 352; - conn->stream.fmtp[2] = 0; - conn->stream.fmtp[3] = 16; - conn->stream.fmtp[4] = 40; - conn->stream.fmtp[5] = 10; - conn->stream.fmtp[6] = 14; - conn->stream.fmtp[7] = 2; - conn->stream.fmtp[8] = 255; - conn->stream.fmtp[9] = 0; - conn->stream.fmtp[10] = 0; - conn->stream.fmtp[11] = 44100; - - // set the parameters of the player (as distinct from the parameters of the decoder -- - // that's done later). - conn->max_frames_per_packet = conn->stream.fmtp[1]; // number of audio frames per packet. - conn->input_rate = conn->stream.fmtp[11]; - conn->input_num_channels = conn->stream.fmtp[7]; - conn->input_bit_depth = conn->stream.fmtp[3]; - conn->input_bytes_per_frame = conn->input_num_channels * ((conn->input_bit_depth + 7) / 8); - debug(2, "Realtime Stream Play"); - activity_monitor_signify_activity(1); - player_prepare_to_play(conn); - player_play(conn); - - conn->rtp_running = 1; // hack! - } break; - case 103: { - debug(1, "Connection %d. AP2 Buffered Audio Stream.", conn->connection_number); - debug_log_rtsp_message(2, "Buffered Audio Stream SETUP incoming message", req); - // get_play_lock(conn); - conn->airplay_stream_type = buffered_stream; - // get needed stuff - - // bind a new TCP port and get a socket - conn->local_buffered_audio_port = 0; // any port - err = bind_socket_and_port(SOCK_STREAM, conn->connection_ip_family, conn->self_ip_string, - conn->self_scope_id, &conn->local_buffered_audio_port, - &conn->buffered_audio_socket); - if (err) { - die("SETUP on Connection %d: Error %d: could not find a TCP port to use as a " - "buffered_audio port", - conn->connection_number, err); - } - - debug(2, "Connection %d: TCP Buffered Audio port opened: %u.", conn->connection_number, - conn->local_buffered_audio_port); - - // hack. - conn->max_frames_per_packet = 352; // number of audio frames per packet. - conn->input_rate = 44100; // we are stuck with this for the moment. - conn->input_num_channels = 2; - conn->input_bit_depth = 16; - conn->input_bytes_per_frame = conn->input_num_channels * ((conn->input_bit_depth + 7) / 8); - activity_monitor_signify_activity(1); - player_prepare_to_play( - conn); // get capabilities of DAC before creating the buffered audio thread - - pthread_create(&conn->rtp_buffered_audio_thread, NULL, &rtp_buffered_audio_processor, - (void *)conn); - - plist_dict_set_item(stream0dict, "type", plist_new_uint(103)); - plist_dict_set_item(stream0dict, "dataPort", - plist_new_uint(conn->local_buffered_audio_port)); - plist_dict_set_item(stream0dict, "audioBufferSize", - plist_new_uint(conn->ap2_audio_buffer_size)); - - // this should be cancelled by an activity_monitor_signify_activity(1) - // call in the SETRATEANCHORI handler, which should come up right away - activity_monitor_signify_activity(0); - player_play(conn); - - conn->rtp_running = 1; // hack! - } break; - default: - debug(1, "SETUP on Connection %d: Unhandled stream type %" PRIu64 ".", - conn->connection_number, item_value); - debug_log_rtsp_message(1, "Unhandled stream type incoming message", req); - } - - plist_dict_set_item(stream0dict, "controlPort", plist_new_uint(conn->local_ap2_control_port)); - - plist_array_append_item(streams_array, stream0dict); - plist_dict_set_item(setupResponsePlist, "streams", streams_array); - resp->respcode = 200; - - } else if (conn->airplay_stream_category == remote_control_stream) { - debug(2, "Connection %d (RC): SETUP: Remote Control Stream received from %s.", - conn->connection_number, conn->client_ip_string); - debug_log_rtsp_message(2, "Remote Control Stream SETUP incoming message", req); - /* - // get a port to use as an data port - // bind a new TCP port and get a socket - conn->local_data_port = 0; // any port - int err = - bind_socket_and_port(SOCK_STREAM, conn->connection_ip_family, conn->self_ip_string, - conn->self_scope_id, &conn->local_data_port, &conn->data_socket); - if (err) { - die("SETUP on Connection %d (RC): Error %d: could not find a TCP port to use as a data " - "port", - conn->connection_number, err); - } - - debug(1, "Connection %d SETUP (RC): TCP Remote Control data port opened: %u.", - conn->connection_number, conn->local_data_port); - if (conn->rtp_data_thread != NULL) - debug(1, "Connection %d SETUP (RC): previous rtp_data_thread allocation not freed, it - seems.", conn->connection_number); conn->rtp_data_thread = malloc(sizeof(pthread_t)); if - (conn->rtp_data_thread == NULL) die("Couldn't allocate space for pthread_t"); - - pthread_create(conn->rtp_data_thread, NULL, &rtp_data_receiver, (void *)conn); - - plist_t coreResponseDict = plist_new_dict(); - plist_dict_set_item(coreResponseDict, "streamID", plist_new_uint(1)); - plist_dict_set_item(coreResponseDict, "type", plist_new_uint(130)); - plist_dict_set_item(coreResponseDict, "dataPort", plist_new_uint(conn->local_data_port)); - - plist_t coreResponseArray = plist_new_array(); - plist_array_append_item(coreResponseArray, coreResponseDict); - plist_dict_set_item(setupResponsePlist, "streams", coreResponseArray); - */ - resp->respcode = 200; - } else { - debug(1, "Connection %d: SETUP: Stream received but no airplay category set. Nothing done.", - conn->connection_number); - } - } - - if (resp->respcode == 200) { - plist_to_bin(setupResponsePlist, &resp->content, &resp->contentlength); - plist_free(setupResponsePlist); - msg_add_header(resp, "Content-Type", "application/x-apple-binary-plist"); - } - plist_free(messagePlist); - if (clientNameString != NULL) - free(clientNameString); - debug_log_rtsp_message(2, " SETUP response", resp); -} -#endif - -void handle_setup(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { - debug(3, "Connection %d: SETUP", conn->connection_number); - resp->respcode = 451; // invalid arguments -- expect them - if (have_play_lock(conn)) { - uint16_t cport, tport; - char *ar = msg_get_header(req, "Active-Remote"); - if (ar) { - debug(2, "Connection %d: SETUP: Active-Remote string seen: \"%s\".", conn->connection_number, - ar); - // get the active remote - if (conn->dacp_active_remote) // this is in case SETUP was previously called - free(conn->dacp_active_remote); - conn->dacp_active_remote = strdup(ar); -#ifdef CONFIG_METADATA - send_metadata('ssnc', 'acre', ar, strlen(ar), req, 1); -#endif - } else { - debug(2, "Connection %d: SETUP: Note: no Active-Remote information seen.", - conn->connection_number); - if (conn->dacp_active_remote) { // this is in case SETUP was previously called - free(conn->dacp_active_remote); - conn->dacp_active_remote = NULL; - } - } - - ar = msg_get_header(req, "DACP-ID"); - if (ar) { - debug(2, "Connection %d: SETUP: DACP-ID string seen: \"%s\".", conn->connection_number, ar); - if (conn->dacp_id) // this is in case SETUP was previously called - free(conn->dacp_id); - conn->dacp_id = strdup(ar); -#ifdef CONFIG_METADATA - send_metadata('ssnc', 'daid', ar, strlen(ar), req, 1); -#endif - } else { - debug(2, "Connection %d: SETUP doesn't include DACP-ID string information.", - conn->connection_number); - if (conn->dacp_id) { // this is in case SETUP was previously called - free(conn->dacp_id); - conn->dacp_id = NULL; - } - } - - char *hdr = msg_get_header(req, "Transport"); - if (hdr) { - char *p; - p = strstr(hdr, "control_port="); - if (p) { - p = strchr(p, '=') + 1; - cport = atoi(p); - - p = strstr(hdr, "timing_port="); - if (p) { - p = strchr(p, '=') + 1; - tport = atoi(p); - - if (conn->rtp_running) { - if ((conn->remote_control_port != cport) || (conn->remote_timing_port != tport)) { - warn("Connection %d: Duplicate SETUP message with different control (old %u, new %u) " - "or " - "timing (old %u, new " - "%u) ports! This is probably fatal!", - conn->connection_number, conn->remote_control_port, cport, - conn->remote_timing_port, tport); - } else { - warn("Connection %d: Duplicate SETUP message with the same control (%u) and timing " - "(%u) " - "ports. This is " - "probably not fatal.", - conn->connection_number, conn->remote_control_port, conn->remote_timing_port); - } - } else { - rtp_setup(&conn->local, &conn->remote, cport, tport, conn); - } - if (conn->local_audio_port != 0) { - - char resphdr[256] = ""; - snprintf(resphdr, sizeof(resphdr), - "RTP/AVP/" - "UDP;unicast;interleaved=0-1;mode=record;control_port=%d;" - "timing_port=%d;server_" - "port=%d", - conn->local_control_port, conn->local_timing_port, conn->local_audio_port); - - msg_add_header(resp, "Transport", resphdr); - - msg_add_header(resp, "Session", "1"); - - resp->respcode = 200; // it all worked out okay - debug(2, - "Connection %d: SETUP DACP-ID \"%s\" from %s to %s with UDP ports Control: " - "%d, Timing: %d and Audio: %d.", - conn->connection_number, conn->dacp_id, &conn->client_ip_string, - &conn->self_ip_string, conn->local_control_port, conn->local_timing_port, - conn->local_audio_port); - - } else { - debug(1, "Connection %d: SETUP seems to specify a null audio port.", - conn->connection_number); - } - } else { - debug(1, "Connection %d: SETUP doesn't specify a timing_port.", conn->connection_number); - } - } else { - debug(1, "Connection %d: SETUP doesn't specify a control_port.", conn->connection_number); - } - } else { - debug(1, "Connection %d: SETUP doesn't contain a Transport header.", conn->connection_number); - } - if (resp->respcode == 200) { -#ifdef CONFIG_METADATA - send_ssnc_metadata('clip', conn->client_ip_string, strlen(conn->client_ip_string), 1); - send_ssnc_metadata('svip', conn->self_ip_string, strlen(conn->self_ip_string), 1); -#endif - } else { - debug(1, "Connection %d: SETUP error -- releasing the player lock.", conn->connection_number); - release_play_lock(conn); - } - - } else { - warn("Connection %d SETUP received without having the player (no ANNOUNCE?)", - conn->connection_number); - } -} - -/* -static void handle_ignore(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { - debug(1, "Connection thread %d: IGNORE", conn->connection_number); - resp->respcode = 200; -} -*/ - -void handle_set_parameter_parameter(rtsp_conn_info *conn, rtsp_message *req, - __attribute__((unused)) rtsp_message *resp) { - - char *cp = req->content; - int cp_left = req->contentlength; - /* - int k = cp_left; - if (k>max_bytes) - k = max_bytes; - for (i = 0; i < k; i++) - snprintf((char *)buf + 2 * i, 3, "%02x", cp[i]); - debug(1, "handle_set_parameter_parameter: \"%s\".",buf); - */ - - char *next; - while (cp_left && cp) { - next = nextline(cp, cp_left); - // note: "next" will return NULL if there is no \r or \n or \r\n at the end of this - // but we are always guaranteed that if cp is not null, it will be pointing to something - // NUL-terminated - - if (next) - cp_left -= (next - cp); - else - cp_left = 0; - - if (!strncmp(cp, "volume: ", strlen("volume: "))) { - float volume = atof(cp + strlen("volume: ")); - debug(2, "Connection %d: request to set AirPlay Volume to: %f.", conn->connection_number, - volume); - // if we are playing, go ahead and change the volume -#ifdef CONFIG_DBUS_INTERFACE - if (dbus_service_is_running()) { - shairport_sync_set_volume(shairportSyncSkeleton, volume); - } else { -#endif - lock_player(); - if (playing_conn == conn) - player_volume(volume, conn); - unlock_player(); -#ifdef CONFIG_DBUS_INTERFACE - } -#endif - } else if (strncmp(cp, "progress: ", strlen("progress: ")) == - 0) { // this can be sent even when metadata is not solicited - -#ifdef CONFIG_METADATA - char *progress = cp + strlen("progress: "); - // debug(2, "progress: \"%s\"",progress); // rtpstampstart/rtpstampnow/rtpstampend 44100 per - // second - send_ssnc_metadata('prgr', progress, strlen(progress), 1); -#endif - - } else { - debug(1, "Connection %d, unrecognised parameter: \"%s\" (%d)\n", conn->connection_number, cp, - strlen(cp)); - } - cp = next; - } -} - -#ifdef CONFIG_METADATA -// Metadata is not used by shairport-sync. -// Instead we send all metadata to a fifo pipe, so that other apps can listen to -// the pipe and use the metadata. - -// We use two 4-character codes to identify each piece of data and we send the -// data itself, if any, -// in base64 form. - -// The first 4-character code, called the "type", is either: -// 'core' for all the regular metadadata coming from iTunes, etc., or -// 'ssnc' (for 'shairport-sync') for all metadata coming from Shairport Sync -// itself, such as -// start/end delimiters, etc. - -// For 'core' metadata, the second 4-character code is the 4-character metadata -// code coming from -// iTunes etc. -// For 'ssnc' metadata, the second 4-character code is used to distinguish the -// messages. - -// Cover art is not tagged in the same way as other metadata, it seems, so is -// sent as an 'ssnc' type -// metadata message with the code 'PICT' -// Here are the 'ssnc' codes defined so far: -// 'PICT' -- the payload is a picture, either a JPEG or a PNG. Check the -// first few bytes to see -// which. -// 'abeg' -- active mode entered. No arguments -// 'aend' -- active mode exited. No arguments -// 'pbeg' -- play stream begin. No arguments -// 'pend' -- play stream end. No arguments -// 'pfls' -- play stream flush. The argument is an unsigned 32-bit -// frame number. It seems that all frames up to but not -// including this frame are to be flushed. -// -// 'prsm' -- play stream resume. No arguments. (deprecated) -// 'paus' -- buffered audio stream paused. No arguments. -// 'pres' -- buffered audio stream resumed. No arguments. -// 'pffr' -- the first frame of a play session has been received and has been validly -// timed. -// 'pvol' -- play volume. The volume is sent as a string -- -// "airplay_volume,volume,lowest_volume,highest_volume" -// volume, lowest_volume and highest_volume are given in dB. -// The "airplay_volume" is what's sent to the player, and is from -// 0.00 down to -30.00, -// with -144.00 meaning mute. -// This is linear on the volume control slider of iTunes or iOS -// AirPlay. -// 'prgr' -- progress -- this is metadata from AirPlay consisting of RTP -// timestamps for the start -// of the current play sequence, the current play point and the end of the -// play sequence. -// I guess the timestamps wrap at 2^32. -// 'mdst' -- a sequence of metadata is about to start; will have, as data, -// the rtptime associated with the metadata, if available -// 'mden' -- a sequence of metadata has ended; will have, as data, the -// rtptime associated with the metadata, if available -// 'pcst' -- a picture is about to be sent; will have, as data, the rtptime -// associated with the picture, if available -// 'pcen' -- a picture has been sent; will have, as data, the rtptime -// associated with the metadata, if available -// 'snam' -- A device -- e.g. "Joe's iPhone" -- has opened a play session. -// Specifically, it's the "X-Apple-Client-Name" string -// 'snua' -- A "user agent" -- e.g. "iTunes/12..." -- has opened a play -// session. Specifically, it's the "User-Agent" string -// The next two two tokens are to facilitate remote control of the source. -// There is some information at http://nto.github.io/AirPlay.html about -// remote control of the source. -// -// 'daid' -- this is the source's DACP-ID (if it has one -- it's not -// guaranteed), useful if you want to remotely control the source. Use this -// string to identify the source's remote control on the network. -// 'acre' -- this is the source's Active-Remote token, necessary if you want -// to send commands to the source's remote control (if it has one). -// `clip` -- the payload is the IP number of the client, i.e. the sender of audio. -// Can be an IPv4 or an IPv6 number. In AirPlay 2 operation, it is sent as soon -// as the client has exclusive access to the player and after any existing -// play session has been interrupted and terminated. -// `conn` -- the payload is the IP number of the client, i.e. the sender of audio. -// Can be an IPv4 or an IPv6 number. This is an AirPlay-2-only message. -// It is sent as soon as the client requests access to the player. -// If Shairport Sync is already playing, this message is sent before the current -// play session is stopped. -// `svip` -- the payload is the IP number of the server, i.e. the player itself. -// Can be an IPv4 or an IPv6 number. -// `disc` -- the payload is the IP number of the client, i.e. the sender of audio. -// Can be an IPv4 or an IPv6 number. This is an AirPlay-2-only message. -// It is sent when a client has been disconnected. -// `dapo` -- the payload is the port number (as text) on the server to which remote -// control commands should be sent. It is 3689 for iTunes but varies for iOS devices. -// `` - -// A special sub-protocol is used for sending large data items over UDP -// If the payload exceeded 4 MB, it is chunked using the following format: -// "ssnc", "chnk", packet_ix, packet_counts, packet_tag, packet_type, chunked_data. -// Notice that the number of items is different to the standard - -// including a simple base64 encoder to minimise malloc/free activity - -// From Stack Overflow, with thanks: -// http://stackoverflow.com/questions/342409/how-do-i-base64-encode-decode-in-c -// minor mods to make independent of C99. -// more significant changes make it not malloc memory -// needs to initialise the docoding table first - -// add _so to end of name to avoid confusion with polarssl's implementation - -static char encoding_table[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', - 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', - 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', - 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'}; - -static size_t mod_table[] = {0, 2, 1}; - -// pass in a pointer to the data, its length, a pointer to the output buffer and -// a pointer to an int -// containing its maximum length -// the actual length will be returned. - -char *base64_encode_so(const unsigned char *data, size_t input_length, char *encoded_data, - size_t *output_length) { - - size_t calculated_output_length = 4 * ((input_length + 2) / 3); - if (calculated_output_length > *output_length) - return (NULL); - *output_length = calculated_output_length; - - size_t i, j; - for (i = 0, j = 0; i < input_length;) { - - uint32_t octet_a = i < input_length ? (unsigned char)data[i++] : 0; - uint32_t octet_b = i < input_length ? (unsigned char)data[i++] : 0; - uint32_t octet_c = i < input_length ? (unsigned char)data[i++] : 0; - - uint32_t triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c; - - encoded_data[j++] = encoding_table[(triple >> 3 * 6) & 0x3F]; - encoded_data[j++] = encoding_table[(triple >> 2 * 6) & 0x3F]; - encoded_data[j++] = encoding_table[(triple >> 1 * 6) & 0x3F]; - encoded_data[j++] = encoding_table[(triple >> 0 * 6) & 0x3F]; - } - - for (i = 0; i < mod_table[input_length % 3]; i++) - encoded_data[*output_length - 1 - i] = '='; - - return encoded_data; -} - -// with thanks! -// - -static int fd = -1; -// static int dirty = 0; - -pc_queue metadata_queue; -#define metadata_queue_size 500 -metadata_package metadata_queue_items[metadata_queue_size]; -pthread_t metadata_thread; - -#ifdef CONFIG_METADATA_HUB -pc_queue metadata_hub_queue; -#define metadata_hub_queue_size 500 -metadata_package metadata_hub_queue_items[metadata_hub_queue_size]; -pthread_t metadata_hub_thread; -#endif - -#ifdef CONFIG_MQTT -pc_queue metadata_mqtt_queue; -#define metadata_mqtt_queue_size 500 -metadata_package metadata_mqtt_queue_items[metadata_mqtt_queue_size]; -pthread_t metadata_mqtt_thread; -#endif - -static int metadata_sock = -1; -static struct sockaddr_in metadata_sockaddr; -static char *metadata_sockmsg; -pc_queue metadata_multicast_queue; -#define metadata_multicast_queue_size 500 -metadata_package metadata_multicast_queue_items[metadata_queue_size]; -pthread_t metadata_multicast_thread; - -void metadata_create_multicast_socket(void) { - if (config.metadata_enabled == 0) - return; - - // Unlike metadata pipe, socket is opened once and stays open, - // so we can call it in create - if (config.metadata_sockaddr && config.metadata_sockport) { - metadata_sock = socket(AF_INET, SOCK_DGRAM, 0); - if (metadata_sock < 0) { - debug(1, "Could not open metadata socket"); - } else { - int buffer_size = METADATA_SNDBUF; - setsockopt(metadata_sock, SOL_SOCKET, SO_SNDBUF, &buffer_size, sizeof(buffer_size)); - bzero((char *)&metadata_sockaddr, sizeof(metadata_sockaddr)); - metadata_sockaddr.sin_family = AF_INET; - metadata_sockaddr.sin_addr.s_addr = inet_addr(config.metadata_sockaddr); - metadata_sockaddr.sin_port = htons(config.metadata_sockport); - metadata_sockmsg = malloc(config.metadata_sockmsglength); - if (metadata_sockmsg) { - memset(metadata_sockmsg, 0, config.metadata_sockmsglength); - } else { - die("Could not malloc metadata multicast socket buffer"); - } - } - } -} - -void metadata_delete_multicast_socket(void) { - if (config.metadata_enabled == 0) - return; - shutdown(metadata_sock, SHUT_RDWR); // we want to immediately deallocate the buffer - close(metadata_sock); - if (metadata_sockmsg) - free(metadata_sockmsg); -} - -void metadata_open(void) { - if (config.metadata_enabled == 0) - return; - - size_t pl = strlen(config.metadata_pipename) + 1; - - char *path = malloc(pl + 1); - snprintf(path, pl + 1, "%s", config.metadata_pipename); - - fd = try_to_open_pipe_for_writing(path); - free(path); -} - -static void metadata_close(void) { - if (fd < 0) - return; - close(fd); - fd = -1; -} - -void metadata_multicast_process(uint32_t type, uint32_t code, char *data, uint32_t length) { - // debug(1, "Process multicast metadata with type %x, code %x and length %u.", type, code, - // length); - if (metadata_sock >= 0 && length < config.metadata_sockmsglength - 8) { - char *ptr = metadata_sockmsg; - uint32_t v; - v = htonl(type); - memcpy(ptr, &v, 4); - ptr += 4; - v = htonl(code); - memcpy(ptr, &v, 4); - ptr += 4; - memcpy(ptr, data, length); - sendto(metadata_sock, metadata_sockmsg, length + 8, 0, (struct sockaddr *)&metadata_sockaddr, - sizeof(metadata_sockaddr)); - } else if (metadata_sock >= 0) { - // send metadata in numbered chunks using the protocol: - // ("ssnc", "chnk", packet_ix, packet_counts, packet_tag, packet_type, chunked_data) - - uint32_t chunk_ix = 0; - if (config.metadata_sockmsglength == 24) - die("A divide by zero almost occurred (config.metadata_sockmsglength = 24)."); - uint32_t chunk_total = length / (config.metadata_sockmsglength - 24); - if (chunk_total * (config.metadata_sockmsglength - 24) < length) { - chunk_total++; - } - uint32_t remaining = length; - uint32_t v; - char *data_crsr = data; - do { - char *ptr = metadata_sockmsg; - memcpy(ptr, "ssncchnk", 8); - ptr += 8; - v = htonl(chunk_ix); - memcpy(ptr, &v, 4); - ptr += 4; - v = htonl(chunk_total); - memcpy(ptr, &v, 4); - ptr += 4; - v = htonl(type); - memcpy(ptr, &v, 4); - ptr += 4; - v = htonl(code); - memcpy(ptr, &v, 4); - ptr += 4; - size_t datalen = remaining; - if (datalen > config.metadata_sockmsglength - 24) { - datalen = config.metadata_sockmsglength - 24; - } - memcpy(ptr, data_crsr, datalen); - data_crsr += datalen; - sendto(metadata_sock, metadata_sockmsg, datalen + 24, 0, - (struct sockaddr *)&metadata_sockaddr, sizeof(metadata_sockaddr)); - chunk_ix++; - remaining -= datalen; - if (remaining == 0) - break; - } while (1); - } -} - -void metadata_process(uint32_t type, uint32_t code, char *data, uint32_t length) { - // debug(1, "Process metadata with type %x, code %x and length %u.", type, code, length); - int ret = 0; - // readers may go away and come back - - if (fd < 0) - metadata_open(); - if (fd < 0) - return; - char thestring[1024]; - snprintf(thestring, 1024, "%x%x%u", type, code, - length); - // ret = non_blocking_write(fd, thestring, strlen(thestring)); - ret = write(fd, thestring, strlen(thestring)); - if (ret < 0) { - // debug(1,"metadata_process error %d exit 1",ret); - return; - } - if ((data != NULL) && (length > 0)) { - snprintf(thestring, 1024, "\n\n"); - // ret = non_blocking_write(fd, thestring, strlen(thestring)); - ret = write(fd, thestring, strlen(thestring)); - if (ret < 0) { - // debug(1,"metadata_process error %d exit 2",ret); - return; - } - // here, we write the data in base64 form using our nice base64 encoder - // but, we break it into lines of 76 output characters, except for the last - // one. - // thus, we send groups of (76/4)*3 = 57 bytes to the encoder at a time - size_t remaining_count = length; - char *remaining_data = data; - // size_t towrite_count; - char outbuf[76]; - while ((remaining_count) && (ret >= 0)) { - size_t towrite_count = remaining_count; - if (towrite_count > 57) - towrite_count = 57; - size_t outbuf_size = 76; // size of output buffer on entry, length of result on exit - if (base64_encode_so((unsigned char *)remaining_data, towrite_count, outbuf, &outbuf_size) == - NULL) - debug(1, "Error encoding base64 data."); - // debug(1,"Remaining count: %d ret: %d, outbuf_size: - // %d.",remaining_count,ret,outbuf_size); - // ret = non_blocking_write(fd, outbuf, outbuf_size); - ret = write(fd, outbuf, outbuf_size); - if (ret < 0) { - // debug(1,"metadata_process error %d exit 3",ret); - return; - } - remaining_data += towrite_count; - remaining_count -= towrite_count; - } - snprintf(thestring, 1024, ""); - // ret = non_blocking_write(fd, thestring, strlen(thestring)); - ret = write(fd, thestring, strlen(thestring)); - if (ret < 0) { - // debug(1,"metadata_process error %d exit 4",ret); - return; - } - } - snprintf(thestring, 1024, "\n"); - // ret = non_blocking_write(fd, thestring, strlen(thestring)); - ret = write(fd, thestring, strlen(thestring)); - if (ret < 0) { - // debug(1,"metadata_process error %d exit 5",ret); - return; - } -} - -void metadata_pack_cleanup_function(void *arg) { - // debug(1, "metadata_pack_cleanup_function called"); - metadata_package *pack = (metadata_package *)arg; - if (pack->carrier) - msg_free(&pack->carrier); // release the message - else if (pack->data) - free(pack->data); - // debug(1, "metadata_pack_cleanup_function exit"); -} - -void metadata_thread_cleanup_function(__attribute__((unused)) void *arg) { - // debug(2, "metadata_thread_cleanup_function called"); - metadata_close(); - pc_queue_delete(&metadata_queue); -} - -void *metadata_thread_function(__attribute__((unused)) void *ignore) { - // create a pc_queue for passing information to a threaded metadata handler - pc_queue_init(&metadata_queue, (char *)&metadata_queue_items, sizeof(metadata_package), - metadata_queue_size, "pipe"); - metadata_create_multicast_socket(); - metadata_package pack; - pthread_cleanup_push(metadata_thread_cleanup_function, NULL); - while (1) { - pc_queue_get_item(&metadata_queue, &pack); - pthread_cleanup_push(metadata_pack_cleanup_function, (void *)&pack); - if (config.metadata_enabled) { - if (pack.carrier) { - debug(3, " pipe: type %x, code %x, length %u, message %d.", pack.type, pack.code, - pack.length, pack.carrier->index_number); - } else { - debug(3, " pipe: type %x, code %x, length %u.", pack.type, pack.code, pack.length); - } - metadata_process(pack.type, pack.code, pack.data, pack.length); - debug(3, " pipe: done."); - } - pthread_cleanup_pop(1); - } - pthread_cleanup_pop(1); // will never happen - pthread_exit(NULL); -} - -void metadata_multicast_thread_cleanup_function(__attribute__((unused)) void *arg) { - // debug(2, "metadata_multicast_thread_cleanup_function called"); - metadata_delete_multicast_socket(); - pc_queue_delete(&metadata_multicast_queue); -} - -void *metadata_multicast_thread_function(__attribute__((unused)) void *ignore) { - // create a pc_queue for passing information to a threaded metadata handler - pc_queue_init(&metadata_multicast_queue, (char *)&metadata_multicast_queue_items, - sizeof(metadata_package), metadata_multicast_queue_size, "multicast"); - metadata_create_multicast_socket(); - metadata_package pack; - pthread_cleanup_push(metadata_multicast_thread_cleanup_function, NULL); - while (1) { - pc_queue_get_item(&metadata_multicast_queue, &pack); - pthread_cleanup_push(metadata_pack_cleanup_function, (void *)&pack); - if (config.metadata_enabled) { - if (pack.carrier) { - debug(3, - " multicast: type " - "%x, code %x, length %u, message %d.", - pack.type, pack.code, pack.length, pack.carrier->index_number); - } else { - debug(3, - " multicast: type " - "%x, code %x, length %u.", - pack.type, pack.code, pack.length); - } - metadata_multicast_process(pack.type, pack.code, pack.data, pack.length); - debug(3, - " multicast: done."); - } - pthread_cleanup_pop(1); - } - pthread_cleanup_pop(1); // will never happen - pthread_exit(NULL); -} - -#ifdef CONFIG_METADATA_HUB -void metadata_hub_close(void) {} - -void metadata_hub_thread_cleanup_function(__attribute__((unused)) void *arg) { - // debug(2, "metadata_hub_thread_cleanup_function called"); - metadata_hub_close(); - pc_queue_delete(&metadata_hub_queue); -} - -void *metadata_hub_thread_function(__attribute__((unused)) void *ignore) { - // create a pc_queue for passing information to a threaded metadata handler - pc_queue_init(&metadata_hub_queue, (char *)&metadata_hub_queue_items, sizeof(metadata_package), - metadata_hub_queue_size, "hub"); - metadata_package pack; - pthread_cleanup_push(metadata_hub_thread_cleanup_function, NULL); - while (1) { - pc_queue_get_item(&metadata_hub_queue, &pack); - pthread_cleanup_push(metadata_pack_cleanup_function, (void *)&pack); - if (pack.carrier) { - debug(3, " hub: type %x, code %x, length %u, message %d.", pack.type, - pack.code, pack.length, pack.carrier->index_number); - } else { - debug(3, " hub: type %x, code %x, length %u.", pack.type, pack.code, - pack.length); - } - metadata_hub_process_metadata(pack.type, pack.code, pack.data, pack.length); - debug(3, " hub: done."); - pthread_cleanup_pop(1); - } - pthread_cleanup_pop(1); // will never happen - pthread_exit(NULL); -} -#endif - -#ifdef CONFIG_MQTT -void metadata_mqtt_close(void) {} - -void metadata_mqtt_thread_cleanup_function(__attribute__((unused)) void *arg) { - // debug(2, "metadata_mqtt_thread_cleanup_function called"); - metadata_mqtt_close(); - pc_queue_delete(&metadata_mqtt_queue); - // debug(2, "metadata_mqtt_thread_cleanup_function done"); -} - -void *metadata_mqtt_thread_function(__attribute__((unused)) void *ignore) { - // create a pc_queue for passing information to a threaded metadata handler - pc_queue_init(&metadata_mqtt_queue, (char *)&metadata_mqtt_queue_items, sizeof(metadata_package), - metadata_mqtt_queue_size, "mqtt"); - metadata_package pack; - pthread_cleanup_push(metadata_mqtt_thread_cleanup_function, NULL); - while (1) { - pc_queue_get_item(&metadata_mqtt_queue, &pack); - pthread_cleanup_push(metadata_pack_cleanup_function, (void *)&pack); - if (config.mqtt_enabled) { - if (pack.carrier) { - debug(3, - " mqtt: type %x, code %x, length %u, message " - "%d.", - pack.type, pack.code, pack.length, pack.carrier->index_number); - } else { - debug(3, " mqtt: type %x, code %x, length %u.", - pack.type, pack.code, pack.length); - } - mqtt_process_metadata(pack.type, pack.code, pack.data, pack.length); - debug(3, " mqtt: done."); - } - - pthread_cleanup_pop(1); - } - pthread_cleanup_pop(1); // will never happen - pthread_exit(NULL); -} -#endif - -void metadata_init(void) { - if (config.metadata_enabled) { - // create the metadata pipe, if necessary - size_t pl = strlen(config.metadata_pipename) + 1; - char *path = malloc(pl + 1); - snprintf(path, pl + 1, "%s", config.metadata_pipename); - mode_t oldumask = umask(000); - if (mkfifo(path, 0666) && errno != EEXIST) - die("Could not create metadata pipe \"%s\".", path); - umask(oldumask); - debug(1, "metadata pipe name is \"%s\".", path); - - // try to open it - fd = try_to_open_pipe_for_writing(path); - // we check that it's not a "real" error. From the "man 2 open" page: - // "ENXIO O_NONBLOCK | O_WRONLY is set, the named file is a FIFO, and no process has the FIFO - // open for reading." Which is okay. - if ((fd == -1) && (errno != ENXIO)) { - char errorstring[1024]; - strerror_r(errno, (char *)errorstring, sizeof(errorstring)); - debug(1, "metadata_hub_thread_function -- error %d (\"%s\") opening pipe: \"%s\".", errno, - (char *)errorstring, path); - warn("can not open metadata pipe -- error %d (\"%s\") opening pipe: \"%s\".", errno, - (char *)errorstring, path); - } - free(path); - if (pthread_create(&metadata_thread, NULL, metadata_thread_function, NULL) != 0) - debug(1, "Failed to create metadata thread!"); - - if (pthread_create(&metadata_multicast_thread, NULL, metadata_multicast_thread_function, - NULL) != 0) - debug(1, "Failed to create metadata multicast thread!"); - } -#ifdef CONFIG_METADATA_HUB - if (pthread_create(&metadata_hub_thread, NULL, metadata_hub_thread_function, NULL) != 0) - debug(1, "Failed to create metadata hub thread!"); -#endif -#ifdef CONFIG_MQTT - if (pthread_create(&metadata_mqtt_thread, NULL, metadata_mqtt_thread_function, NULL) != 0) - debug(1, "Failed to create metadata mqtt thread!"); -#endif - metadata_running = 1; -} - -void metadata_stop(void) { - if (metadata_running) { - debug(2, "metadata_stop called."); -#ifdef CONFIG_MQTT - // debug(2, "metadata stop mqtt thread."); - pthread_cancel(metadata_mqtt_thread); - pthread_join(metadata_mqtt_thread, NULL); - // debug(2, "metadata stop mqtt done."); -#endif -#ifdef CONFIG_METADATA_HUB - // debug(2, "metadata stop hub thread."); - pthread_cancel(metadata_hub_thread); - pthread_join(metadata_hub_thread, NULL); - // debug(2, "metadata stop hub done."); -#endif - if (config.metadata_enabled) { - // debug(2, "metadata stop multicast thread."); - if (metadata_multicast_thread) { - pthread_cancel(metadata_multicast_thread); - pthread_join(metadata_multicast_thread, NULL); - // debug(2, "metadata stop multicast done."); - } - if (metadata_thread) { - // debug(2, "metadata stop metadata_thread thread."); - pthread_cancel(metadata_thread); - pthread_join(metadata_thread, NULL); - // debug(2, "metadata_stop finished successfully."); - } - } - } -} - -int send_metadata_to_queue(pc_queue *queue, uint32_t type, uint32_t code, char *data, - uint32_t length, rtsp_message *carrier, int block) { - - // clang-format off - // parameters: - // type, - // code, - // pointer to data or NULL, - // length of data or NULL, - // the rtsp_message or NULL, - - // the rtsp_message is sent for 'core' messages, because it contains the data - // and must not be freed until the data has been read. - // So, it is passed to send_metadata to be retained, sent to the thread where metadata - // is processed and released (and probably freed). - - // The rtsp_message is also sent for certain non-'core' messages. - - // The reading of the parameters is a bit complex: - // If the rtsp_message field is non-null, then it represents an rtsp_message - // and the data pointer is assumed to point to something within it. - // The reference counter of the rtsp_message is incremented here and - // should be decremented by the metadata handler when finished. - // If the reference count reduces to zero, the message will be freed. - - // If the rtsp_message is NULL, then if the pointer is non-null then the data it - // points to, of the length specified, is memcpy'd and passed to the metadata - // handler. The handler should free it when done. - - // If the rtsp_message is NULL and the pointer is also NULL, nothing further - // is done. - // clang-format on - - metadata_package pack; - pack.type = type; - pack.code = code; - pack.length = length; - pack.carrier = carrier; - pack.data = data; - if (pack.carrier) { - msg_retain(pack.carrier); - } else { - if (data) - pack.data = memdup(data, length); // only if it's not a null - } - int rc = pc_queue_add_item(queue, &pack, block); - if (rc != 0) { - if (pack.carrier) { - if (rc == EWOULDBLOCK) - debug(2, - "metadata queue \"%s\" full, dropping message item: type %x, code %x, data %x, " - "length %u, message %d.", - queue->name, pack.type, pack.code, pack.data, pack.length, - pack.carrier->index_number); - msg_free(&pack.carrier); - } else { - if (rc == EWOULDBLOCK) - debug( - 2, - "metadata queue \"%s\" full, dropping data item: type %x, code %x, data %x, length %u.", - queue->name, pack.type, pack.code, pack.data, pack.length); - if (pack.data) - free(pack.data); - } - } - return rc; -} - -int send_metadata(uint32_t type, uint32_t code, char *data, uint32_t length, rtsp_message *carrier, - int block) { - int rc = 0; - if (config.metadata_enabled) { - rc = send_metadata_to_queue(&metadata_queue, type, code, data, length, carrier, block); - rc = - send_metadata_to_queue(&metadata_multicast_queue, type, code, data, length, carrier, block); - } - -#ifdef CONFIG_METADATA_HUB - rc = send_metadata_to_queue(&metadata_hub_queue, type, code, data, length, carrier, block); -#endif - -#ifdef CONFIG_MQTT - rc = send_metadata_to_queue(&metadata_mqtt_queue, type, code, data, length, carrier, block); -#endif - - return rc; -} - -static void handle_set_parameter_metadata(__attribute__((unused)) rtsp_conn_info *conn, - rtsp_message *req, - __attribute__((unused)) rtsp_message *resp) { - char *cp = req->content; - unsigned int cl = req->contentlength; - - unsigned int off = 8; - - uint32_t itag, vl; - while (off < cl) { - // pick up the metadata tag as an unsigned longint - memcpy(&itag, (uint32_t *)(cp + off), sizeof(uint32_t)); /* can be misaligned, thus memcpy */ - itag = ntohl(itag); - off += sizeof(uint32_t); - - // pick up the length of the data - memcpy(&vl, (uint32_t *)(cp + off), sizeof(uint32_t)); /* can be misaligned, thus memcpy */ - vl = ntohl(vl); - off += sizeof(uint32_t); - - // pass the data over - if (vl == 0) - send_metadata('core', itag, NULL, 0, NULL, 1); - else - send_metadata('core', itag, (char *)(cp + off), vl, req, 1); - - // move on to the next item - off += vl; - } -} - -#endif - -static void handle_get_parameter(__attribute__((unused)) rtsp_conn_info *conn, rtsp_message *req, - rtsp_message *resp) { - // debug(1, "Connection %d: GET_PARAMETER", conn->connection_number); - // debug_print_msg_headers(1,req); - // debug_print_msg_content(1,req); - - if ((req->content) && (req->contentlength == strlen("volume\r\n")) && - strstr(req->content, "volume") == req->content) { - debug(1, "Connection %d: Current volume (%.6f) requested", conn->connection_number, - config.airplay_volume); - char *p = malloc(128); // will be automatically deallocated with the response is deleted - if (p) { - resp->content = p; - resp->contentlength = snprintf(p, 128, "\r\nvolume: %.6f\r\n", config.airplay_volume); - } else { - debug(1, "Couldn't allocate space for a response."); - } - } - resp->respcode = 200; -} - -static void handle_set_parameter(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { - debug(3, "Connection %d: SET_PARAMETER", conn->connection_number); - // if (!req->contentlength) - // debug(1, "received empty SET_PARAMETER request."); - - // debug_print_msg_headers(1,req); - - char *ct = msg_get_header(req, "Content-Type"); - - if (ct) { - // debug(2, "SET_PARAMETER Content-Type:\"%s\".", ct); - -#ifdef CONFIG_METADATA - // It seems that the rtptime of the message is used as a kind of an ID that - // can be used - // to link items of metadata, including pictures, that refer to the same - // entity. - // If they refer to the same item, they have the same rtptime. - // So we send the rtptime before and after both the metadata items and the - // picture item - // get the rtptime - char *p = NULL; - char *hdr = msg_get_header(req, "RTP-Info"); - - if (hdr) { - p = strstr(hdr, "rtptime="); - if (p) { - p = strchr(p, '='); - } - } - - // not all items have RTP-time stuff in them, which is okay - - if (!strncmp(ct, "application/x-dmap-tagged", 25)) { - debug(3, "received metadata tags in SET_PARAMETER request."); - if (p == NULL) - debug(1, "Missing RTP-Time info for metadata"); - if (p) - send_metadata('ssnc', 'mdst', p + 1, strlen(p + 1), req, 1); // metadata starting - else - send_metadata('ssnc', 'mdst', NULL, 0, NULL, - 0); // metadata starting, if rtptime is not available - - handle_set_parameter_metadata(conn, req, resp); - - if (p) - send_metadata('ssnc', 'mden', p + 1, strlen(p + 1), req, 1); // metadata ending - else - send_metadata('ssnc', 'mden', NULL, 0, NULL, - 0); // metadata starting, if rtptime is not available - - } else if (!strncmp(ct, "image", 5)) { - // Some server simply ignore the md field from the TXT record. If The - // config says 'please, do not include any cover art', we are polite and - // do not write them to the pipe. - if (config.get_coverart) { - // debug(1, "received image in SET_PARAMETER request."); - // note: the image/type tag isn't reliable, so it's not being sent - // -- best look at the first few bytes of the image - if (p == NULL) - debug(1, "Missing RTP-Time info for picture item"); - if (p) - send_metadata('ssnc', 'pcst', p + 1, strlen(p + 1), req, 1); // picture starting - else - send_metadata('ssnc', 'pcst', NULL, 0, NULL, - 0); // picture starting, if rtptime is not available - - send_metadata('ssnc', 'PICT', req->content, req->contentlength, req, 1); - - if (p) - send_metadata('ssnc', 'pcen', p + 1, strlen(p + 1), req, 1); // picture ending - else - send_metadata('ssnc', 'pcen', NULL, 0, NULL, - 0); // picture ending, if rtptime is not available - } else { - debug(1, "Ignore received picture item (include_cover_art = no)."); - } - } else -#endif - if (!strncmp(ct, "text/parameters", 15)) { - // debug(2, "received parameters in SET_PARAMETER request."); - handle_set_parameter_parameter(conn, req, resp); // this could be volume or progress - } else { - debug(1, "Connection %d: received unknown Content-Type \"%s\" in SET_PARAMETER request.", - conn->connection_number, ct); - debug_print_msg_headers(1, req); - } - } else { - debug(1, "Connection %d: missing Content-Type header in SET_PARAMETER request.", - conn->connection_number); - } - resp->respcode = 200; -} - -static void handle_announce(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { - debug(3, "Connection %d: ANNOUNCE", conn->connection_number); - int get_play_status = get_play_lock(conn, config.allow_session_interruption); - if (get_play_status != -1) { - debug(3, "Connection %d: ANNOUNCE has acquired play lock.", conn->connection_number); - - // now, if this new session did not break in, then it's okay to reset the next UDP ports - // to the start of the range - - if (get_play_status == 1) { // will be zero if it wasn't waiting to break in - resetFreeUDPPort(); - } - - /* - { - char *cp = req->content; - int cp_left = req->contentlength; - while (cp_left > 1) { - if (strlen(cp) != 0) - debug(1,">>>>>> %s", cp); - cp += strlen(cp) + 1; - cp_left -= strlen(cp) + 1; - } - } - */ - // In AirPlay 2, an ANNOUNCE signifies the start of an AirPlay 1 session. -#ifdef CONFIG_AIRPLAY_2 - conn->airplay_type = ap_1; - conn->timing_type = ts_ntp; - debug(1, "Connection %d: Classic AirPlay connection from %s:%u to self at %s:%u.", - conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, - conn->self_ip_string, conn->self_rtsp_port); -#endif - - conn->stream.type = ast_unknown; - resp->respcode = 456; // 456 - Header Field Not Valid for Resource - char *pssid = NULL; - char *paesiv = NULL; - char *prsaaeskey = NULL; - char *pfmtp = NULL; - char *pminlatency = NULL; - char *pmaxlatency = NULL; - // char *pAudioMediaInfo = NULL; - char *pUncompressedCDAudio = NULL; - char *cp = req->content; - int cp_left = req->contentlength; - char *next; - while (cp_left && cp) { - next = nextline(cp, cp_left); - cp_left -= next - cp; - - if (!strncmp(cp, "a=rtpmap:96 L16/44100/2", strlen("a=rtpmap:96 L16/44100/2"))) - pUncompressedCDAudio = cp + strlen("a=rtpmap:96 L16/44100/2"); - - // if (!strncmp(cp, "m=audio", strlen("m=audio"))) - // pAudioMediaInfo = cp + strlen("m=audio"); - - if (!strncmp(cp, "o=iTunes", strlen("o=iTunes"))) - pssid = cp + strlen("o=iTunes"); - - if (!strncmp(cp, "a=fmtp:", strlen("a=fmtp:"))) - pfmtp = cp + strlen("a=fmtp:"); - - if (!strncmp(cp, "a=aesiv:", strlen("a=aesiv:"))) - paesiv = cp + strlen("a=aesiv:"); - - if (!strncmp(cp, "a=rsaaeskey:", strlen("a=rsaaeskey:"))) - prsaaeskey = cp + strlen("a=rsaaeskey:"); - - if (!strncmp(cp, "a=min-latency:", strlen("a=min-latency:"))) - pminlatency = cp + strlen("a=min-latency:"); - - if (!strncmp(cp, "a=max-latency:", strlen("a=max-latency:"))) - pmaxlatency = cp + strlen("a=max-latency:"); - - cp = next; - } - - if (pUncompressedCDAudio) { - debug(2, "An uncompressed PCM stream has been detected."); - conn->stream.type = ast_uncompressed; - conn->max_frames_per_packet = 352; // number of audio frames per packet. - conn->input_rate = 44100; - conn->input_num_channels = 2; - conn->input_bit_depth = 16; - conn->input_bytes_per_frame = conn->input_num_channels * ((conn->input_bit_depth + 7) / 8); - - /* - int y = strlen(pAudioMediaInfo); - if (y > 0) { - char obf[4096]; - if (y > 4096) - y = 4096; - char *p = pAudioMediaInfo; - char *obfp = obf; - int obfc; - for (obfc = 0; obfc < y; obfc++) { - snprintf(obfp, 3, "%02X", (unsigned int)*p); - p++; - obfp += 2; - }; - *obfp = 0; - debug(1, "AudioMediaInfo: \"%s\".", obf); - } - */ - } - - if (pssid) { - uint32_t ssid = uatoi(pssid); - debug(3, "Synchronisation Source Identifier: %08X,%u", ssid, ssid); - } - - if (pminlatency) { - conn->minimum_latency = atoi(pminlatency); - debug(3, "Minimum latency %d specified", conn->minimum_latency); - } - - if (pmaxlatency) { - conn->maximum_latency = atoi(pmaxlatency); - debug(3, "Maximum latency %d specified", conn->maximum_latency); - } - - if ((paesiv == NULL) && (prsaaeskey == NULL)) { - // debug(1,"Unencrypted session requested?"); - conn->stream.encrypted = 0; - } else if ((paesiv != NULL) && (prsaaeskey != NULL)){ - conn->stream.encrypted = 1; - // debug(1,"Encrypted session requested"); - } else { - warn("Invalid Announce message -- missing paesiv or prsaaeskey."); - goto out; - } - if (conn->stream.encrypted) { - int len, keylen; - uint8_t *aesiv = base64_dec(paesiv, &len); - if (len != 16) { - warn("client announced aeskey of %d bytes, wanted 16", len); - free(aesiv); - goto out; - } - memcpy(conn->stream.aesiv, aesiv, 16); - free(aesiv); - - uint8_t *rsaaeskey = base64_dec(prsaaeskey, &len); - uint8_t *aeskey = rsa_apply(rsaaeskey, len, &keylen, RSA_MODE_KEY); - free(rsaaeskey); - if (keylen != 16) { - warn("client announced rsaaeskey of %d bytes, wanted 16", keylen); - free(aeskey); - goto out; - } - memcpy(conn->stream.aeskey, aeskey, 16); - free(aeskey); - } - - if (pfmtp) { - conn->stream.type = ast_apple_lossless; - debug(3, "An ALAC stream has been detected."); - - // Set reasonable connection defaults - conn->stream.fmtp[0] = 96; - conn->stream.fmtp[1] = 352; - conn->stream.fmtp[2] = 0; - conn->stream.fmtp[3] = 16; - conn->stream.fmtp[4] = 40; - conn->stream.fmtp[5] = 10; - conn->stream.fmtp[6] = 14; - conn->stream.fmtp[7] = 2; - conn->stream.fmtp[8] = 255; - conn->stream.fmtp[9] = 0; - conn->stream.fmtp[10] = 0; - conn->stream.fmtp[11] = 44100; - - unsigned int i = 0; - unsigned int max_param = sizeof(conn->stream.fmtp) / sizeof(conn->stream.fmtp[0]); - char *found; - while ((found = strsep(&pfmtp, " \t")) != NULL && i < max_param) { - conn->stream.fmtp[i++] = atoi(found); - } - // here we should check the sanity of the fmtp values - // for (i = 0; i < sizeof(conn->stream.fmtp) / sizeof(conn->stream.fmtp[0]); i++) - // debug(1," fmtp[%2d] is: %10d",i,conn->stream.fmtp[i]); - - // set the parameters of the player (as distinct from the parameters of the decoder -- that's - // done later). - conn->max_frames_per_packet = conn->stream.fmtp[1]; // number of audio frames per packet. - conn->input_rate = conn->stream.fmtp[11]; - conn->input_num_channels = conn->stream.fmtp[7]; - conn->input_bit_depth = conn->stream.fmtp[3]; - conn->input_bytes_per_frame = conn->input_num_channels * ((conn->input_bit_depth + 7) / 8); - } - - if (conn->stream.type == ast_unknown) { - warn("Can not process the following ANNOUNCE message:"); - // print each line of the request content - // the problem is that nextline has replace all returns, newlines, etc. by - // NULLs - char *cp = req->content; - int cp_left = req->contentlength; - while (cp_left > 1) { - if (strlen(cp) != 0) - warn(" %s", cp); - cp += strlen(cp) + 1; - cp_left -= strlen(cp) + 1; - } - goto out; - } - - char *hdr = msg_get_header(req, "X-Apple-Client-Name"); - if (hdr) { - debug(1, "Play connection from device named \"%s\" on RTSP conversation thread %d.", hdr, - conn->connection_number); -#ifdef CONFIG_METADATA - send_metadata('ssnc', 'snam', hdr, strlen(hdr), req, 1); -#endif - } - hdr = msg_get_header(req, "User-Agent"); - if (hdr) { - conn->UserAgent = strdup(hdr); - debug(2, "Play connection from user agent \"%s\" on RTSP conversation thread %d.", hdr, - conn->connection_number); - // if the user agent is AirPlay and has a version number of 353 or less (from iOS 11.1,2) - // use the older way of calculating the latency - - char *p = strstr(hdr, "AirPlay"); - if (p) { - p = strchr(p, '/'); - if (p) { - conn->AirPlayVersion = atoi(p + 1); - debug(2, "AirPlay version %d detected.", conn->AirPlayVersion); - } - } - -#ifdef CONFIG_METADATA - send_metadata('ssnc', 'snua', hdr, strlen(hdr), req, 1); -#endif - } - resp->respcode = 200; - } else { - resp->respcode = 453; - debug(1, "Connection %d: ANNOUNCE failed because another connection is already playing.", - conn->connection_number); - } - -out: - if (resp->respcode != 200 && resp->respcode != 453) { - debug(1, "Connection %d: Error in handling ANNOUNCE. Unlocking the play lock.", - conn->connection_number); - release_play_lock(conn); - } -} - -#ifdef CONFIG_AIRPLAY_2 -static struct method_handler { - char *method; - void (*ap1_handler)(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp); // for AirPlay 1 - void (*ap2_handler)(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp); // for AirPlay 2 -} method_handlers[] = {{"OPTIONS", handle_options, handle_options}, - {"ANNOUNCE", handle_announce, handle_announce}, - {"FLUSH", handle_flush, handle_flush}, - {"TEARDOWN", handle_teardown, handle_teardown_2}, - {"SETUP", handle_setup, handle_setup_2}, - {"GET_PARAMETER", handle_get_parameter, handle_get_parameter}, - {"SET_PARAMETER", handle_set_parameter, handle_set_parameter}, - {"RECORD", handle_record, handle_record_2}, - {"GET", handle_get, handle_get}, - {"POST", handle_post, handle_post}, - {"SETPEERS", handle_unimplemented_ap1, handle_setpeers}, - {"SETRATEANCHORTI", handle_unimplemented_ap1, handle_setrateanchori}, - {"FLUSHBUFFERED", handle_unimplemented_ap1, handle_flushbuffered}, - {"SETRATE", handle_unimplemented_ap1, handle_setrate}, - {NULL, NULL, NULL}}; -#else -static struct method_handler { - char *method; - void (*handler)(rtsp_conn_info *conn, rtsp_message *req, - rtsp_message *resp); // for AirPlay 1 only -} method_handlers[] = {{"OPTIONS", handle_options}, - {"GET", handle_get}, - {"POST", handle_post}, - {"ANNOUNCE", handle_announce}, - {"FLUSH", handle_flush}, - {"TEARDOWN", handle_teardown}, - {"SETUP", handle_setup}, - {"GET_PARAMETER", handle_get_parameter}, - {"SET_PARAMETER", handle_set_parameter}, - {"RECORD", handle_record}, - {NULL, NULL}}; -#endif - -static void apple_challenge(int fd, rtsp_message *req, rtsp_message *resp) { - char *hdr = msg_get_header(req, "Apple-Challenge"); - if (!hdr) - return; - SOCKADDR fdsa; - socklen_t sa_len = sizeof(fdsa); - getsockname(fd, (struct sockaddr *)&fdsa, &sa_len); - - int chall_len; - uint8_t *chall = base64_dec(hdr, &chall_len); - if (chall == NULL) - die("null chall in apple_challenge"); - uint8_t buf[48], *bp = buf; - int i; - memset(buf, 0, sizeof(buf)); - - if (chall_len > 16) { - warn("oversized Apple-Challenge!"); - free(chall); - return; - } - memcpy(bp, chall, chall_len); - free(chall); - bp += chall_len; - -#ifdef AF_INET6 - if (fdsa.SAFAMILY == AF_INET6) { - struct sockaddr_in6 *sa6 = (struct sockaddr_in6 *)(&fdsa); - memcpy(bp, sa6->sin6_addr.s6_addr, 16); - bp += 16; - } else -#endif - { - struct sockaddr_in *sa = (struct sockaddr_in *)(&fdsa); - memcpy(bp, &sa->sin_addr.s_addr, 4); - bp += 4; - } - - for (i = 0; i < 6; i++) - *bp++ = config.hw_addr[i]; - - int buflen, resplen; - buflen = bp - buf; - if (buflen < 0x20) - buflen = 0x20; - - uint8_t *challresp = rsa_apply(buf, buflen, &resplen, RSA_MODE_AUTH); - char *encoded = base64_enc(challresp, resplen); - if (encoded == NULL) - die("could not allocate memory for \"encoded\""); - // strip the padding. - char *padding = strchr(encoded, '='); - if (padding) - *padding = 0; - - msg_add_header(resp, "Apple-Response", encoded); // will be freed when the response is freed. - free(challresp); - free(encoded); -} - -static char *make_nonce(void) { - uint8_t random[8]; - int fd = open("/dev/urandom", O_RDONLY); - if (fd < 0) - die("could not open /dev/urandom!"); - // int ignore = - if (read(fd, random, sizeof(random)) != sizeof(random)) - debug(1, "Error reading /dev/urandom"); - close(fd); - return base64_enc(random, 8); // returns a pointer to malloc'ed memory -} - -static int rtsp_auth(char **nonce, rtsp_message *req, rtsp_message *resp) { - - if (!config.password) - return 0; - if (!*nonce) { - *nonce = make_nonce(); - goto authenticate; - } - - char *hdr = msg_get_header(req, "Authorization"); - if (!hdr || strncmp(hdr, "Digest ", 7)) - goto authenticate; - - char *realm = strstr(hdr, "realm=\""); - char *username = strstr(hdr, "username=\""); - char *response = strstr(hdr, "response=\""); - char *uri = strstr(hdr, "uri=\""); - - if (!realm || !username || !response || !uri) - goto authenticate; - - char *quote; - realm = strchr(realm, '"') + 1; - if (!(quote = strchr(realm, '"'))) - goto authenticate; - *quote = 0; - username = strchr(username, '"') + 1; - if (!(quote = strchr(username, '"'))) - goto authenticate; - *quote = 0; - response = strchr(response, '"') + 1; - if (!(quote = strchr(response, '"'))) - goto authenticate; - *quote = 0; - uri = strchr(uri, '"') + 1; - if (!(quote = strchr(uri, '"'))) - goto authenticate; - *quote = 0; - - uint8_t digest_urp[16], digest_mu[16], digest_total[16]; - -#ifdef CONFIG_OPENSSL - MD5_CTX ctx; - - int oldState; - pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); - MD5_Init(&ctx); - MD5_Update(&ctx, username, strlen(username)); - MD5_Update(&ctx, ":", 1); - MD5_Update(&ctx, realm, strlen(realm)); - MD5_Update(&ctx, ":", 1); - MD5_Update(&ctx, config.password, strlen(config.password)); - MD5_Final(digest_urp, &ctx); - MD5_Init(&ctx); - MD5_Update(&ctx, req->method, strlen(req->method)); - MD5_Update(&ctx, ":", 1); - MD5_Update(&ctx, uri, strlen(uri)); - MD5_Final(digest_mu, &ctx); - pthread_setcancelstate(oldState, NULL); -#endif - -#ifdef CONFIG_MBEDTLS -#if MBEDTLS_VERSION_MINOR >= 7 - mbedtls_md5_context tctx; - mbedtls_md5_starts_ret(&tctx); - mbedtls_md5_update_ret(&tctx, (const unsigned char *)username, strlen(username)); - mbedtls_md5_update_ret(&tctx, (unsigned char *)":", 1); - mbedtls_md5_update_ret(&tctx, (const unsigned char *)realm, strlen(realm)); - mbedtls_md5_update_ret(&tctx, (unsigned char *)":", 1); - mbedtls_md5_update_ret(&tctx, (const unsigned char *)config.password, strlen(config.password)); - mbedtls_md5_finish_ret(&tctx, digest_urp); - mbedtls_md5_starts_ret(&tctx); - mbedtls_md5_update_ret(&tctx, (const unsigned char *)req->method, strlen(req->method)); - mbedtls_md5_update_ret(&tctx, (unsigned char *)":", 1); - mbedtls_md5_update_ret(&tctx, (const unsigned char *)uri, strlen(uri)); - mbedtls_md5_finish_ret(&tctx, digest_mu); -#else - mbedtls_md5_context tctx; - mbedtls_md5_starts(&tctx); - mbedtls_md5_update(&tctx, (const unsigned char *)username, strlen(username)); - mbedtls_md5_update(&tctx, (unsigned char *)":", 1); - mbedtls_md5_update(&tctx, (const unsigned char *)realm, strlen(realm)); - mbedtls_md5_update(&tctx, (unsigned char *)":", 1); - mbedtls_md5_update(&tctx, (const unsigned char *)config.password, strlen(config.password)); - mbedtls_md5_finish(&tctx, digest_urp); - mbedtls_md5_starts(&tctx); - mbedtls_md5_update(&tctx, (const unsigned char *)req->method, strlen(req->method)); - mbedtls_md5_update(&tctx, (unsigned char *)":", 1); - mbedtls_md5_update(&tctx, (const unsigned char *)uri, strlen(uri)); - mbedtls_md5_finish(&tctx, digest_mu); -#endif -#endif - -#ifdef CONFIG_POLARSSL - md5_context tctx; - md5_starts(&tctx); - md5_update(&tctx, (const unsigned char *)username, strlen(username)); - md5_update(&tctx, (unsigned char *)":", 1); - md5_update(&tctx, (const unsigned char *)realm, strlen(realm)); - md5_update(&tctx, (unsigned char *)":", 1); - md5_update(&tctx, (const unsigned char *)config.password, strlen(config.password)); - md5_finish(&tctx, digest_urp); - md5_starts(&tctx); - md5_update(&tctx, (const unsigned char *)req->method, strlen(req->method)); - md5_update(&tctx, (unsigned char *)":", 1); - md5_update(&tctx, (const unsigned char *)uri, strlen(uri)); - md5_finish(&tctx, digest_mu); -#endif - - int i; - unsigned char buf[33]; - for (i = 0; i < 16; i++) - snprintf((char *)buf + 2 * i, 3, "%02x", digest_urp[i]); - -#ifdef CONFIG_OPENSSL - pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); - MD5_Init(&ctx); - MD5_Update(&ctx, buf, 32); - MD5_Update(&ctx, ":", 1); - MD5_Update(&ctx, *nonce, strlen(*nonce)); - MD5_Update(&ctx, ":", 1); - for (i = 0; i < 16; i++) - snprintf((char *)buf + 2 * i, 3, "%02x", digest_mu[i]); - MD5_Update(&ctx, buf, 32); - MD5_Final(digest_total, &ctx); - pthread_setcancelstate(oldState, NULL); -#endif - -#ifdef CONFIG_MBEDTLS -#if MBEDTLS_VERSION_MINOR >= 7 - mbedtls_md5_starts_ret(&tctx); - mbedtls_md5_update_ret(&tctx, buf, 32); - mbedtls_md5_update_ret(&tctx, (unsigned char *)":", 1); - mbedtls_md5_update_ret(&tctx, (const unsigned char *)*nonce, strlen(*nonce)); - mbedtls_md5_update_ret(&tctx, (unsigned char *)":", 1); - for (i = 0; i < 16; i++) - snprintf((char *)buf + 2 * i, 3, "%02x", digest_mu[i]); - mbedtls_md5_update_ret(&tctx, buf, 32); - mbedtls_md5_finish_ret(&tctx, digest_total); -#else - mbedtls_md5_starts(&tctx); - mbedtls_md5_update(&tctx, buf, 32); - mbedtls_md5_update(&tctx, (unsigned char *)":", 1); - mbedtls_md5_update(&tctx, (const unsigned char *)*nonce, strlen(*nonce)); - mbedtls_md5_update(&tctx, (unsigned char *)":", 1); - for (i = 0; i < 16; i++) - snprintf((char *)buf + 2 * i, 3, "%02x", digest_mu[i]); - mbedtls_md5_update(&tctx, buf, 32); - mbedtls_md5_finish(&tctx, digest_total); -#endif -#endif - -#ifdef CONFIG_POLARSSL - md5_starts(&tctx); - md5_update(&tctx, buf, 32); - md5_update(&tctx, (unsigned char *)":", 1); - md5_update(&tctx, (const unsigned char *)*nonce, strlen(*nonce)); - md5_update(&tctx, (unsigned char *)":", 1); - for (i = 0; i < 16; i++) - snprintf((char *)buf + 2 * i, 3, "%02x", digest_mu[i]); - md5_update(&tctx, buf, 32); - md5_finish(&tctx, digest_total); -#endif - - for (i = 0; i < 16; i++) - snprintf((char *)buf + 2 * i, 3, "%02x", digest_total[i]); - - if (!strcmp(response, (const char *)buf)) - return 0; - warn("Password authorization failed."); - -authenticate: - resp->respcode = 401; - int hdrlen = strlen(*nonce) + 40; - char *authhdr = malloc(hdrlen); - snprintf(authhdr, hdrlen, "Digest realm=\"raop\", nonce=\"%s\"", *nonce); - msg_add_header(resp, "WWW-Authenticate", authhdr); - free(authhdr); - return 1; -} - -void rtsp_conversation_thread_cleanup_function(void *arg) { - rtsp_conn_info *conn = (rtsp_conn_info *)arg; - int oldState; - pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); - - debug(2, "Connection %d: rtsp_conversation_thread_func_cleanup_function called.", - conn->connection_number); -#ifdef CONFIG_AIRPLAY_2 - // AP2 - teardown_phase_one(conn); - teardown_phase_two(conn); -#else - // AP1 - if (have_play_lock(conn)) { - teardown(conn); - release_play_lock(conn); - } -#endif - - debug(3, "Connection %d: terminating -- closing timing, control and audio sockets...", - conn->connection_number); - if (conn->control_socket) { - debug(3, "Connection %d: terminating -- closing control_socket %d.", - conn->connection_number, conn->control_socket); - close(conn->control_socket); - conn->control_socket = 0; - } - if (conn->timing_socket) { - debug(3, "Connection %d: terminating -- closing timing_socket %d.", - conn->connection_number, conn->timing_socket); - close(conn->timing_socket); - conn->timing_socket = 0; - } - if (conn->audio_socket) { - debug(3, "Connection %d: terminating -- closing audio_socket %d.", - conn->connection_number, conn->audio_socket); - close(conn->audio_socket); - conn->audio_socket = 0; - } - if (conn->fd > 0) { - debug(1, "Connection %d: terminating -- closing RTSP connection socket %d: from %s:%u to self at %s:%u.", - conn->connection_number, conn->fd, conn->client_ip_string, conn->client_rtsp_port, - conn->self_ip_string, conn->self_rtsp_port); - close(conn->fd); - conn->fd = 0; - } - if (conn->auth_nonce) { - free(conn->auth_nonce); - conn->auth_nonce = NULL; - } - -#ifdef CONFIG_AIRPLAY_2 - buf_drain(&conn->ap2_pairing_context.control_cipher_bundle.plaintext_read_buffer, -1); - buf_drain(&conn->ap2_pairing_context.control_cipher_bundle.encrypted_read_buffer, -1); - pair_cipher_free(conn->ap2_pairing_context.control_cipher_bundle.cipher_ctx); - pair_setup_free(conn->ap2_pairing_context.setup_ctx); - pair_verify_free(conn->ap2_pairing_context.verify_ctx); - if (conn->airplay_gid) { - free(conn->airplay_gid); - conn->airplay_gid = NULL; - } -#endif - - rtp_terminate(conn); - - if (conn->dacp_id) { - free(conn->dacp_id); - conn->dacp_id = NULL; - } - - if (conn->UserAgent) { - free(conn->UserAgent); - conn->UserAgent = NULL; - } - - // remove flow control and mutexes - int rc = pthread_mutex_destroy(&conn->volume_control_mutex); - if (rc) - debug(1, "Connection %d: error %d destroying volume_control_mutex.", conn->connection_number, - rc); - rc = pthread_cond_destroy(&conn->flowcontrol); - if (rc) - debug(1, "Connection %d: error %d destroying flow control condition variable.", - conn->connection_number, rc); - rc = pthread_mutex_destroy(&conn->ab_mutex); - if (rc) - debug(1, "Connection %d: error %d destroying ab_mutex.", conn->connection_number, rc); - rc = pthread_mutex_destroy(&conn->flush_mutex); - if (rc) - debug(1, "Connection %d: error %d destroying flush_mutex.", conn->connection_number, rc); - - debug(3, "Cancel watchdog thread."); - pthread_cancel(conn->player_watchdog_thread); - debug(3, "Join watchdog thread."); - pthread_join(conn->player_watchdog_thread, NULL); - debug(3, "Delete watchdog mutex."); - pthread_mutex_destroy(&conn->watchdog_mutex); - - // debug(3, "Connection %d: Checking play lock.", conn->connection_number); - // release_play_lock(conn); - - debug(2, "Connection %d: Closed.", conn->connection_number); - conn->running = 0; - pthread_setcancelstate(oldState, NULL); -} - -void msg_cleanup_function(void *arg) { - // debug(3, "msg_cleanup_function called."); - msg_free((rtsp_message **)arg); -} - -static void *rtsp_conversation_thread_func(void *pconn) { - rtsp_conn_info *conn = pconn; - - // create the watchdog mutex, initialise the watchdog time and start the watchdog thread; - conn->watchdog_bark_time = get_absolute_time_in_ns(); - pthread_mutex_init(&conn->watchdog_mutex, NULL); - pthread_create(&conn->player_watchdog_thread, NULL, &player_watchdog_thread_code, (void *)conn); - - int rc = pthread_mutex_init(&conn->flush_mutex, NULL); - if (rc) - die("Connection %d: error %d initialising flush_mutex.", conn->connection_number, rc); - rc = pthread_mutex_init(&conn->ab_mutex, NULL); - if (rc) - die("Connection %d: error %d initialising ab_mutex.", conn->connection_number, rc); - rc = pthread_cond_init(&conn->flowcontrol, NULL); - if (rc) - die("Connection %d: error %d initialising flow control condition variable.", - conn->connection_number, rc); - rc = pthread_mutex_init(&conn->volume_control_mutex, NULL); - if (rc) - die("Connection %d: error %d initialising volume_control_mutex.", conn->connection_number, rc); - - // nothing before this is cancellable - pthread_cleanup_push(rtsp_conversation_thread_cleanup_function, (void *)conn); - - rtp_initialise(conn); - char *hdr = NULL; - - enum rtsp_read_request_response reply; - - int rtsp_read_request_attempt_count = 1; // 1 means exit immediately - rtsp_message *req, *resp; - -#ifdef CONFIG_AIRPLAY_2 - conn->ap2_audio_buffer_size = 1024 * 1024 * 8; -#endif - - while (conn->stop == 0) { - int debug_level = 3; // for printing the request and response - reply = rtsp_read_request(conn, &req); - if (reply == rtsp_read_request_response_ok) { - pthread_cleanup_push(msg_cleanup_function, (void *)&req); - resp = msg_init(); - pthread_cleanup_push(msg_cleanup_function, (void *)&resp); - resp->respcode = 501; // Not Implemented - int dl = debug_level; - if ((strcmp(req->method, "OPTIONS") == 0) || - (strcmp(req->method, "POST") == - 0)) // the options message is very common, so don't log it until level 3 - dl = 3; - - if (conn->airplay_stream_category == remote_control_stream) { - debug(dl, "Connection %d (RC): Received an RTSP Packet of type \"%s\":", - conn->connection_number, req->method), - debug_log_rtsp_message(dl, NULL, req); - } else { - debug(dl, "Connection %d: Received an RTSP Packet of type \"%s\":", conn->connection_number, - req->method), - debug_log_rtsp_message(dl, NULL, req); - } - apple_challenge(conn->fd, req, resp); - hdr = msg_get_header(req, "CSeq"); - if (hdr) - msg_add_header(resp, "CSeq", hdr); - // msg_add_header(resp, "Audio-Jack-Status", "connected; type=analog"); -#ifdef CONFIG_AIRPLAY_2 - msg_add_header(resp, "Server", "AirTunes/366.0"); -#else - msg_add_header(resp, "Server", "AirTunes/105.1"); -#endif - - if ((conn->authorized == 1) || (rtsp_auth(&conn->auth_nonce, req, resp)) == 0) { - conn->authorized = 1; // it must have been authorized or didn't need a password - struct method_handler *mh; - int method_selected = 0; - for (mh = method_handlers; mh->method; mh++) { - if (!strcmp(mh->method, req->method)) { - method_selected = 1; -#ifdef CONFIG_AIRPLAY_2 - if (conn->airplay_type == ap_1) - mh->ap1_handler(conn, req, resp); - else - mh->ap2_handler(conn, req, resp); -#else - mh->handler(conn, req, resp); -#endif - break; - } - } - if (method_selected == 0) { - debug(1, - "Connection %d: Unrecognised and unhandled rtsp request \"%s\". HTTP Response Code " - "501 (\"Not Implemented\") returned.", - conn->connection_number, req->method); - - int y = req->contentlength; - if (y > 0) { - char obf[4096]; - if (y > 4096) - y = 4096; - char *p = req->content; - char *obfp = obf; - int obfc; - for (obfc = 0; obfc < y; obfc++) { - snprintf(obfp, 3, "%02X", (unsigned int)*p); - p++; - obfp += 2; - }; - *obfp = 0; - debug(1, "Content: \"%s\".", obf); - } - } - } - if (conn->airplay_stream_category == remote_control_stream) { - debug(dl, "Connection %d (RC): RTSP Response:", conn->connection_number); - debug_log_rtsp_message(dl, NULL, resp); - } else { - debug(dl, "Connection %d: RTSP Response:", conn->connection_number); - debug_log_rtsp_message(dl, NULL, resp); - } - if (conn->stop == 0) { - int err = msg_write_response(conn, resp); - if (err) { - debug(1, - "Connection %d: Unable to write an RTSP message response. Terminating the " - "connection.", - conn->connection_number); - struct linger so_linger; - so_linger.l_onoff = 1; // "true" - so_linger.l_linger = 0; - err = setsockopt(conn->fd, SOL_SOCKET, SO_LINGER, &so_linger, sizeof so_linger); - if (err) - debug(1, "Could not set the RTSP socket to abort due to a write error on closing."); - conn->stop = 1; - // if (debuglev >= 1) - // debuglev = 3; // see what happens next - } - } - pthread_cleanup_pop(1); - pthread_cleanup_pop(1); - } else { - int tstop = 0; - if (reply == rtsp_read_request_response_immediate_shutdown_requested) - tstop = 1; - else if ((reply == rtsp_read_request_response_channel_closed) || - (reply == rtsp_read_request_response_read_error)) { - if (conn->player_thread) { - rtsp_read_request_attempt_count--; - if (rtsp_read_request_attempt_count == 0) { - tstop = 1; - if (reply == rtsp_read_request_response_read_error) { - struct linger so_linger; - so_linger.l_onoff = 1; // "true" - so_linger.l_linger = 0; - int err = setsockopt(conn->fd, SOL_SOCKET, SO_LINGER, &so_linger, sizeof so_linger); - if (err) - debug(1, "Could not set the RTSP socket to abort due to a read error on closing."); - } - // debuglev = 3; // see what happens next - } else { - if (reply == rtsp_read_request_response_channel_closed) - debug(2, - "Connection %d: RTSP channel unexpectedly closed -- will try again %d time(s).", - conn->connection_number, rtsp_read_request_attempt_count); - if (reply == rtsp_read_request_response_read_error) - debug(2, "Connection %d: RTSP channel read error -- will try again %d time(s).", - conn->connection_number, rtsp_read_request_attempt_count); - usleep(20000); - } - } else { - tstop = 1; - } - } else if (reply == rtsp_read_request_response_bad_packet) { - char *response_text = "RTSP/1.0 400 Bad Request\r\nServer: AirTunes/105.1\r\n\r\n"; - ssize_t reply = write(conn->fd, response_text, strlen(response_text)); - if (reply == -1) { - char errorstring[1024]; - strerror_r(errno, (char *)errorstring, sizeof(errorstring)); - debug(1, "rtsp_read_request_response_bad_packet write response error %d: \"%s\".", errno, - (char *)errorstring); - } else if (reply != (ssize_t)strlen(response_text)) { - debug(1, "rtsp_read_request_response_bad_packet write %d bytes requested but %d written.", - strlen(response_text), reply); - } - } else { - debug(1, "Connection %d: rtsp_read_request error %d, packet ignored.", - conn->connection_number, (int)reply); - } - if (tstop) { - debug(3, "Connection %d: Terminate RTSP connection.", conn->connection_number); - conn->stop = 1; - } - } - } - pthread_cleanup_pop(1); - pthread_exit(NULL); - debug(1, "Connection %d: RTSP thread exit.", conn->connection_number); -} - -/* -// this function is not thread safe. -static const char *format_address(struct sockaddr *fsa) { - static char string[INETx_ADDRSTRLEN]; - void *addr; -#ifdef AF_INET6 - if (fsa->sa_family == AF_INET6) { - struct sockaddr_in6 *sa6 = (struct sockaddr_in6 *)(fsa); - addr = &(sa6->sin6_addr); - } else -#endif - { - struct sockaddr_in *sa = (struct sockaddr_in *)(fsa); - addr = &(sa->sin_addr); - } - return inet_ntop(fsa->sa_family, addr, string, sizeof(string)); -} -*/ - -void rtsp_listen_loop_cleanup_handler(__attribute__((unused)) void *arg) { - int oldState; - pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); - debug(2, "rtsp_listen_loop_cleanup_handler called."); - cancel_all_RTSP_threads(unspecified_stream_category, 0); // kill all RTSP listeners - int *sockfd = (int *)arg; - if (sockfd) { - int i; - for (i = 1; i <= sockfd[0]; i++) { - debug(2, "closing socket %d.", sockfd[i]); - close(sockfd[i]); - } - free(sockfd); - } - pthread_setcancelstate(oldState, NULL); -} - -void *rtsp_listen_loop(__attribute((unused)) void *arg) { - int oldState; - pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); - struct addrinfo hints, *info, *p; - char portstr[6]; - int *sockfd = NULL; - int nsock = 0; - int i, ret; - - playing_conn = NULL; // the data structure representing the connection that has the player. - - memset(&hints, 0, sizeof(hints)); - hints.ai_family = AF_UNSPEC; - hints.ai_socktype = SOCK_STREAM; - hints.ai_flags = AI_PASSIVE; - - snprintf(portstr, 6, "%d", config.port); - - // debug(1,"listen socket port request is \"%s\".",portstr); - - ret = getaddrinfo(NULL, portstr, &hints, &info); - if (ret) { - die("getaddrinfo failed: %s", gai_strerror(ret)); - } - - for (p = info; p; p = p->ai_next) { - ret = 0; - int fd = socket(p->ai_family, p->ai_socktype, IPPROTO_TCP); - int yes = 1; - - // Handle socket open failures if protocol unavailable (or IPV6 not handled) - if (fd != -1) { - // Set the RTSP socket to close on exec() of child processes - // otherwise background run_this_before_play_begins or run_this_after_play_ends commands - // that are sleeping prevent the daemon from being restarted because - // the listening RTSP port is still in use. - // See: https://github.com/mikebrady/shairport-sync/issues/329 - fcntl(fd, F_SETFD, FD_CLOEXEC); - ret = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); - - struct timeval tv; - tv.tv_sec = 3; // three seconds write timeout - tv.tv_usec = 0; - if (setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, (const char *)&tv, sizeof tv) == -1) - debug(1, "Error %d setting send timeout for rtsp writeback.", errno); - - if ((config.dont_check_timeout == 0) && (config.timeout != 0)) { - tv.tv_sec = config.timeout; // 120 seconds read timeout by default. - tv.tv_usec = 0; - if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (const char *)&tv, sizeof tv) == -1) - debug(1, "Error %d setting read timeout for rtsp connection.", 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 - // always. - if (p->ai_family == AF_INET6) { - ret |= setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &yes, sizeof(yes)); - } -#endif - - if (!ret) - ret = bind(fd, p->ai_addr, p->ai_addrlen); - - // one of the address families will fail on some systems that - // report its availability. do not complain. - - if (ret) { - char *family; -#ifdef AF_INET6 - if (p->ai_family == AF_INET6) { - family = "IPv6"; - } else -#endif - family = "IPv4"; - debug(1, "unable to listen on %s port %d. The error is: \"%s\".", family, config.port, - strerror(errno)); - } else { - listen(fd, 255); - nsock++; - sockfd = realloc(sockfd, (nsock + 1) * sizeof(int)); - sockfd[nsock] = fd; - sockfd[0] = nsock; // the first entry is the number of sockets in the array - } - } - - /* - listen(fd, 5); - nsock++; - sockfd = realloc(sockfd, nsock * sizeof(int)); - sockfd[nsock - 1] = fd; - */ - } - - freeaddrinfo(info); - - if (nsock) { - int maxfd = -1; - fd_set fds; - FD_ZERO(&fds); - // skip the first element in sockfd -- it's the count - for (i = 1; i <= nsock; i++) { - if (sockfd[i] > maxfd) - maxfd = sockfd[i]; - } - - char **t1 = txt_records; // ap1 test records - char **t2 = NULL; // possibly two text records -#ifdef CONFIG_AIRPLAY_2 - // make up a secondary set of text records - t2 = secondary_txt_records; // second set of text records in AirPlay 2 only -#endif - build_bonjour_strings(NULL); // no conn yet - mdns_register(t1, t2); // note that the dacp thread could still be using the mdns stuff after - // all player threads have been terminated, so mdns_unregister can't be - // in the rtsp_listen_loop cleanup. - - pthread_setcancelstate(oldState, NULL); - int acceptfd; - struct timeval tv; - pthread_cleanup_push(rtsp_listen_loop_cleanup_handler, (void *)sockfd); - do { - pthread_testcancel(); - tv.tv_sec = 60; - tv.tv_usec = 0; - - // skip the first element in sockfd -- it's the count - for (i = 1; i <= nsock; i++) - FD_SET(sockfd[i], &fds); - - ret = select(maxfd + 1, &fds, 0, 0, &tv); - if (ret < 0) { - if (errno == EINTR) - continue; - break; - } - - cleanup_threads(); - - acceptfd = -1; - // skip the first element in sockfd -- it's the count - for (i = 1; i <= nsock; i++) { - if (FD_ISSET(sockfd[i], &fds)) { - acceptfd = sockfd[i]; - break; - } - } - if (acceptfd < 0) // timeout - continue; - - rtsp_conn_info *conn = malloc(sizeof(rtsp_conn_info)); - if (conn == 0) - die("Couldn't allocate memory for an rtsp_conn_info record."); - memset(conn, 0, sizeof(rtsp_conn_info)); - conn->connection_number = RTSP_connection_index++; -#ifdef CONFIG_AIRPLAY_2 - conn->airplay_type = ap_2; // changed if an ANNOUNCE is received - conn->timing_type = ts_ptp; // changed if an ANNOUNCE is received -#endif - - socklen_t size_of_reply = sizeof(SOCKADDR); - conn->fd = accept(acceptfd, (struct sockaddr *)&conn->remote, &size_of_reply); - if (conn->fd < 0) { - debug(1, "Connection %d: New connection on port %d not accepted:", conn->connection_number, - config.port); - perror("failed to accept connection"); - free(conn); - } else { - size_of_reply = sizeof(SOCKADDR); - if (getsockname(conn->fd, (struct sockaddr *)&conn->local, &size_of_reply) == 0) { - // initialise the connection info - void *client_addr = NULL, *self_addr = NULL; - conn->connection_ip_family = conn->local.SAFAMILY; - -#ifdef AF_INET6 - if (conn->connection_ip_family == AF_INET6) { - struct sockaddr_in6 *sa6 = (struct sockaddr_in6 *)&conn->remote; - client_addr = &(sa6->sin6_addr); - conn->client_rtsp_port = ntohs(sa6->sin6_port); - - sa6 = (struct sockaddr_in6 *)&conn->local; - self_addr = &(sa6->sin6_addr); - conn->self_rtsp_port = ntohs(sa6->sin6_port); - conn->self_scope_id = sa6->sin6_scope_id; - } -#endif - if (conn->connection_ip_family == AF_INET) { - struct sockaddr_in *sa4 = (struct sockaddr_in *)&conn->remote; - client_addr = &(sa4->sin_addr); - conn->client_rtsp_port = ntohs(sa4->sin_port); - - sa4 = (struct sockaddr_in *)&conn->local; - self_addr = &(sa4->sin_addr); - conn->self_rtsp_port = ntohs(sa4->sin_port); - } - - inet_ntop(conn->connection_ip_family, client_addr, conn->client_ip_string, - sizeof(conn->client_ip_string)); - inet_ntop(conn->connection_ip_family, self_addr, conn->self_ip_string, - sizeof(conn->self_ip_string)); - - debug(2, "Connection %d: New connection from %s:%u to self at %s:%u.", - conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, - conn->self_ip_string, conn->self_rtsp_port); - conn->connection_start_time = get_absolute_time_in_ns(); - } else { - debug(1, "Error figuring out Shairport Sync's own IP number."); - } - - ret = pthread_create(&conn->thread, NULL, rtsp_conversation_thread_func, - conn); // also acts as a memory barrier - if (ret) { - char errorstring[1024]; - strerror_r(ret, (char *)errorstring, sizeof(errorstring)); - die("Connection %d: cannot create an RTSP conversation thread. Error %d: \"%s\".", - conn->connection_number, ret, (char *)errorstring); - } - debug(3, "Successfully created RTSP receiver thread %d.", conn->connection_number); - conn->running = 1; // this must happen before the thread is tracked - track_thread(conn); - } - } while (1); - pthread_cleanup_pop(1); // should never happen - } else { - warn("could not establish a service on port %d -- program terminating. Is another instance of " - "Shairport Sync running on this device?", - config.port); - } - debug(1, "Oops -- fell out of the RTSP select loop"); - pthread_exit(NULL); -} diff --git a/org.gnome.ShairportSync.xml b/org.gnome.ShairportSync.xml index 47aa3537..97b7de3f 100644 --- a/org.gnome.ShairportSync.xml +++ b/org.gnome.ShairportSync.xml @@ -25,6 +25,8 @@ + + From 8d5cac55d918850d68feb5bf7372a4c9e94d5e35 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 12 Nov 2022 17:53:57 +0000 Subject: [PATCH 16/51] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 49f21269..d381973b 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,13 @@ +Version 4.1-dev-781-g9af924dd +==== +**Metadata Enhancements** + +The following new metadata items have been added to the metadata pipe, MQTT and the native D-Bus interface. +1. `core`/`asdk`. This is [glossed](https://github.com/tchapi/shairport-sync-ui/blob/master/DMAP_DAAP_Codes.md) as `daap.songdatakind`. It seems to indicate whether the audio is timed or not, e.g. a track or album has the value `0`, and a continuous untimed stream such as a radio station is `1`, so it is potentially very useful for remote control displays, as it seems to indicate whether a progress bar, etc. could be used or not. The value is included, if available, in the native DBus interface in the metadata bundle with the tag `sps:songdatakind`. +2. `ssnc`/`styp`. This is the type of data stream currently being received by Shairport Sync. Values are `Classic`, `Buffered` or `Realtime`. Included in the native D-Bus interface as `StreamType` in the `org.gnome.ShairportSync.RemoteControl` interface. +3. `ssnc`/`ofmt`. This is the output format of audio going to the output device. Values are `S32`, `S24` and so on. S=signed, U=unsigned, 32/24/16/8 is the bit depth, BE/LE is Big-Endian or Little-Endian. Note that `S24` means signed 24 bits in a 32-bit space; `S24_3LE` means Signed 24 bits in a 3-byte little-endian form, etc. Each frame is always a stereo pair. Included in the native D-Bus interface as `OutputFormat` in the `org.gnome.ShairportSync` interface. +4. `ssnc`/`ofps`. This is the output rate in frames per second, usually `44100`. Included in the native D-Bus interface as `OutputRate` in the `org.gnome.ShairportSync` interface. + Version 4.1-dev-776-g27754a62 ==== **Docker Enhancements** From d4f1863c9e6f028c664a9122a668b45090cab982 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 16 Nov 2022 10:00:48 +0000 Subject: [PATCH 17/51] Quieten a noisy debug message. --- shairport.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shairport.c b/shairport.c index 7083b99f..2ccf40ce 100644 --- a/shairport.c +++ b/shairport.c @@ -1596,10 +1596,10 @@ void exit_function() { #ifdef CONFIG_SOXR // be careful -- not sure if the thread can be cancelled cleanly, so wait for it to shut down if (soxr_time_check_thread_started != 0) { - debug(1, "Waiting for SoXr timecheck to terminate..."); + debug(2, "Waiting for SoXr timecheck to terminate..."); pthread_join(soxr_time_check_thread, NULL); soxr_time_check_thread_started = 0; - debug(1, "Waiting for SoXr timecheck to terminate done"); + debug(2, "Waiting for SoXr timecheck to terminate done"); } #endif From 60e913dd6e52547caa44a179478a6bd7f832c6d2 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 16 Nov 2022 10:02:00 +0000 Subject: [PATCH 18/51] In the PulseAudio backend, Whenever a play or latency request is made, open the stream if it is closed. --- audio_pa.c | 176 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 108 insertions(+), 68 deletions(-) diff --git a/audio_pa.c b/audio_pa.c index c91b76f6..e744e356 100644 --- a/audio_pa.c +++ b/audio_pa.c @@ -155,12 +155,11 @@ static int init(__attribute__((unused)) int argc, __attribute__((unused)) char * static void deinit(void) { pa_threaded_mainloop_stop(mainloop); pa_threaded_mainloop_free(mainloop); - // debug(1, "pa deinit done"); + debug(1, "pa deinit done"); } -static void start(__attribute__((unused)) int sample_rate, - __attribute__((unused)) int sample_format) { - +static void do_start() { + // debug(1, "pa_start"); uint32_t buffer_size_in_bytes = (uint32_t)2 * 2 * RATE * 0.1; // hard wired in here // debug(1, "pa_buffer size is %u bytes.", buffer_size_in_bytes); @@ -223,88 +222,129 @@ static void start(__attribute__((unused)) int sample_rate, pa_threaded_mainloop_unlock(mainloop); } -static int play(void *buf, int samples, __attribute__((unused)) int sample_type, - __attribute__((unused)) uint32_t timestamp, - __attribute__((unused)) uint64_t playtime) { - // debug(1,"pa_play of %d samples.",samples); - // copy the samples into the queue - size_t bytes_to_transfer = samples * 2 * 2; - size_t space_to_end_of_buffer = audio_umb - audio_eoq; - if (space_to_end_of_buffer >= bytes_to_transfer) { - memcpy(audio_eoq, buf, bytes_to_transfer); - audio_occupancy += bytes_to_transfer; - pthread_mutex_lock(&buffer_mutex); - audio_eoq += bytes_to_transfer; - pthread_mutex_unlock(&buffer_mutex); - } else { - memcpy(audio_eoq, buf, space_to_end_of_buffer); - buf += space_to_end_of_buffer; - memcpy(audio_lmb, buf, bytes_to_transfer - space_to_end_of_buffer); - pthread_mutex_lock(&buffer_mutex); - audio_occupancy += bytes_to_transfer; - pthread_mutex_unlock(&buffer_mutex); - audio_eoq = audio_lmb + bytes_to_transfer - space_to_end_of_buffer; - } - if ((audio_occupancy >= 11025 * 2 * 2) && (pa_stream_is_corked(stream))) { - // debug(1,"Uncorked"); - pa_threaded_mainloop_lock(mainloop); - pa_stream_cork(stream, 0, stream_success_cb, mainloop); - pa_threaded_mainloop_unlock(mainloop); - } - return 0; + +static void start(__attribute__((unused)) int sample_rate, + __attribute__((unused)) int sample_format) { + do_start(); } -int pa_delay(long *the_delay) { - long result = 0; - int reply = -ENODEV; +static int stream_is_open() { + int response = 0; // default to no pa_usec_t latency; int negative; pa_threaded_mainloop_lock(mainloop); int gl = pa_stream_get_latency(stream, &latency, &negative); pa_threaded_mainloop_unlock(mainloop); - if (gl == PA_ERR_NODATA) { - // debug(1, "No latency data yet."); - reply = -ENODEV; - } else if (gl != 0) { - // debug(1,"Error %d getting latency.",gl); - reply = -EIO; + if (gl >= 0) + response = 1; + return response; +} + +static int play(void *buf, int samples, __attribute__((unused)) int sample_type, + __attribute__((unused)) uint32_t timestamp, + __attribute__((unused)) uint64_t playtime) { + // debug(1,"pa_play of %d samples.",samples); + if (stream_is_open() == 0) { + // debug(1,"pa open stream before play."); + do_start(); + } + if (stream_is_open() != 0) { + // copy the samples into the queue + size_t bytes_to_transfer = samples * 2 * 2; + size_t space_to_end_of_buffer = audio_umb - audio_eoq; + if (space_to_end_of_buffer >= bytes_to_transfer) { + memcpy(audio_eoq, buf, bytes_to_transfer); + audio_occupancy += bytes_to_transfer; + pthread_mutex_lock(&buffer_mutex); + audio_eoq += bytes_to_transfer; + pthread_mutex_unlock(&buffer_mutex); + } else { + memcpy(audio_eoq, buf, space_to_end_of_buffer); + buf += space_to_end_of_buffer; + memcpy(audio_lmb, buf, bytes_to_transfer - space_to_end_of_buffer); + pthread_mutex_lock(&buffer_mutex); + audio_occupancy += bytes_to_transfer; + pthread_mutex_unlock(&buffer_mutex); + audio_eoq = audio_lmb + bytes_to_transfer - space_to_end_of_buffer; + } + if ((audio_occupancy >= 11025 * 2 * 2) && (pa_stream_is_corked(stream))) { + // debug(1,"Uncorked"); + pa_threaded_mainloop_lock(mainloop); + pa_stream_cork(stream, 0, stream_success_cb, mainloop); + pa_threaded_mainloop_unlock(mainloop); + } } else { - result = (audio_occupancy / (2 * 2)) + (latency * 44100) / 1000000; - reply = 0; + // debug(2, "could not open pa stream for play"); + } + return 0; +} + +int pa_delay(long *the_delay) { + // debug(1,"pa_delay"); + int reply = -ENODEV; + long result = 0; + if (stream_is_open() == 0) { + // debug(1,"pa open stream before delay."); + do_start(); + } + if (stream_is_open() != 0) { + pa_usec_t latency; + int negative; + pa_threaded_mainloop_lock(mainloop); + int gl = pa_stream_get_latency(stream, &latency, &negative); + pa_threaded_mainloop_unlock(mainloop); + if (gl == PA_ERR_NODATA) { + // debug(1, "No latency data yet."); + reply = -ENODEV; + } else if (gl != 0) { + // debug(1,"Error %d getting latency.",gl); + reply = -EIO; + } else { + result = (audio_occupancy / (2 * 2)) + (latency * 44100) / 1000000; + reply = 0; + } + } else { + // debug(2, "could not open pa stream for delay"); } *the_delay = result; return reply; } void flush(void) { - // Cork the stream so it will stop playing - pa_threaded_mainloop_lock(mainloop); - if (pa_stream_is_corked(stream) == 0) { - // debug(1,"Flush and cork for flush."); - pa_stream_flush(stream, stream_success_cb, NULL); - pa_stream_cork(stream, 1, stream_success_cb, mainloop); + // debug(1,"Flush."); + if (stream_is_open() != 0) { + // Cork the stream so it will stop playing + pa_threaded_mainloop_lock(mainloop); + if (pa_stream_is_corked(stream) == 0) { + // debug(1,"Flush and cork for flush."); + pa_stream_flush(stream, stream_success_cb, NULL); + pa_stream_cork(stream, 1, stream_success_cb, mainloop); + } + pa_threaded_mainloop_unlock(mainloop); + audio_toq = audio_eoq = audio_lmb; + audio_umb = audio_lmb + audio_size; + audio_occupancy = 0; } - pa_threaded_mainloop_unlock(mainloop); - audio_toq = audio_eoq = audio_lmb; - audio_umb = audio_lmb + audio_size; - audio_occupancy = 0; } -static void stop(void) { - // Cork the stream so it will stop playing - pa_threaded_mainloop_lock(mainloop); - if (pa_stream_is_corked(stream) == 0) { - // debug(1,"Flush and cork for stop."); - pa_stream_flush(stream, stream_success_cb, NULL); - pa_stream_cork(stream, 1, stream_success_cb, mainloop); - } - pa_threaded_mainloop_unlock(mainloop); - audio_toq = audio_eoq = audio_lmb; - audio_umb = audio_lmb + audio_size; - audio_occupancy = 0; +static void stop(void) { + // debug(1,"Stop."); + if (stream_is_open() != 0) { + // Cork the stream so it will stop playing + pa_threaded_mainloop_lock(mainloop); + if (pa_stream_is_corked(stream) == 0) { + // debug(1,"Flush and cork for stop."); + pa_stream_flush(stream, stream_success_cb, NULL); + pa_stream_cork(stream, 1, stream_success_cb, mainloop); + } + pa_threaded_mainloop_unlock(mainloop); + audio_toq = audio_eoq = audio_lmb; + audio_umb = audio_lmb + audio_size; + audio_occupancy = 0; - // debug(1,"pa stop"); - pa_stream_disconnect(stream); + // debug(1,"pa stop"); + pa_stream_disconnect(stream); + } } audio_output audio_pa = {.name = "pa", From 2d93180df211374eb7aed342b0dc111c8ad119cb Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 16 Nov 2022 10:05:36 +0000 Subject: [PATCH 19/51] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index d381973b..778caf5e 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version 4.1-dev-784-g60e913dd +==== +**Bug Fix** +* In the PulseAudio backend, Whenever a play or latency request is made, reopen the stream if it is closed. Addresses issue [#1580](https://github.com/mikebrady/shairport-sync/issues/1580). Thanks, as ever, to [Noel Hibbard](https://github.com/noelhibbard) for the report. + Version 4.1-dev-781-g9af924dd ==== **Metadata Enhancements** From fc117e73029e73401f08d459fef4050e659e6c11 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 16 Nov 2022 14:25:53 +0000 Subject: [PATCH 20/51] Modify the generation of the 12-digit classic AirPlay service name prefix so that it depends on the service name as well as the hardware address. --- mdns.c | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++- shairport.c | 37 +++---------------------------------- 2 files changed, 54 insertions(+), 35 deletions(-) diff --git a/mdns.c b/mdns.c index 9a5465c1..5dbbc102 100644 --- a/mdns.c +++ b/mdns.c @@ -48,6 +48,19 @@ extern mdns_backend mdns_external_avahi; extern mdns_backend mdns_external_dns_sd; #endif +#ifdef CONFIG_MBEDTLS +#include +#include +#endif + +#ifdef CONFIG_POLARSSL +#include +#endif + +#ifdef CONFIG_OPENSSL +#include +#endif + static mdns_backend *mdns_backends[] = { #ifdef CONFIG_AVAHI &mdns_avahi, @@ -65,11 +78,48 @@ static mdns_backend *mdns_backends[] = { NULL}; void mdns_register(char **txt_records, char **secondary_txt_records) { + + // calculate the 12-hex-digit prefix by hashing the service name. + uint8_t ap_md5[16]; + +debug(1,"size of hw_addr is %u.", sizeof(config.hw_addr)); +#ifdef CONFIG_OPENSSL + MD5_CTX ctx; + MD5_Init(&ctx); + MD5_Update(&ctx, config.service_name, strlen(config.service_name)); + MD5_Update(&ctx, config.hw_addr, sizeof(config.hw_addr)); + MD5_Final(ap_md5, &ctx); +#endif + +#ifdef CONFIG_MBEDTLS +#if MBEDTLS_VERSION_MINOR >= 7 + mbedtls_md5_context tctx; + mbedtls_md5_starts_ret(&tctx); + mbedtls_md5_update_ret(&tctx, (unsigned char *)config.service_name, strlen(config.service_name)); + mbedtls_md5_update_ret(&tctx, (unsigned char *)config.hw_addr, sizeof(config.hw_addr)); + mbedtls_md5_finish_ret(&tctx, ap_md5); +#else + mbedtls_md5_context tctx; + mbedtls_md5_starts(&tctx); + mbedtls_md5_update(&tctx, (unsigned char *)config.service_name, strlen(config.service_name)); + mbedtls_md5_update(&tctx, (unsigned char *)config.hw_addr, sizeof(config.hw_addr)); + mbedtls_md5_finish(&tctx, ap_md5); +#endif +#endif + +#ifdef CONFIG_POLARSSL + md5_context tctx; + md5_starts(&tctx); + md5_update(&tctx, (unsigned char *)config.service_name, strlen(config.service_name)); + md5_update(&tctx, (unsigned char *)config.hw_addr, sizeof(config.hw_addr)); + md5_finish(&tctx, ap_md5); +#endif + char *ap1_service_name = alloca(strlen(config.service_name) + 14); char *p = ap1_service_name; int i; for (i = 0; i < 6; i++) { - snprintf(p, 3, "%02X", config.hw_addr[i]); + snprintf(p, 3, "%02X", ap_md5[i]); p += 2; } *p++ = '@'; diff --git a/shairport.c b/shairport.c index 2ccf40ce..ac40c11f 100644 --- a/shairport.c +++ b/shairport.c @@ -2454,35 +2454,6 @@ int main(int argc, char **argv) { soxr_time_check_thread_started = 1; #endif - /* - uint8_t ap_md5[16]; - - #ifdef CONFIG_OPENSSL - MD5_CTX ctx; - MD5_Init(&ctx); - MD5_Update(&ctx, config.service_name, strlen(config.service_name)); - MD5_Final(ap_md5, &ctx); - #endif - - #ifdef CONFIG_MBEDTLS - #if MBEDTLS_VERSION_MINOR >= 7 - mbedtls_md5_context tctx; - mbedtls_md5_starts_ret(&tctx); - mbedtls_md5_update_ret(&tctx, (unsigned char *)config.service_name, - strlen(config.service_name)); mbedtls_md5_finish_ret(&tctx, ap_md5); #else mbedtls_md5_context - tctx; mbedtls_md5_starts(&tctx); mbedtls_md5_update(&tctx, (unsigned char *)config.service_name, - strlen(config.service_name)); mbedtls_md5_finish(&tctx, ap_md5); #endif #endif - - #ifdef CONFIG_POLARSSL - md5_context tctx; - md5_starts(&tctx); - md5_update(&tctx, (unsigned char *)config.service_name, strlen(config.service_name)); - md5_finish(&tctx, ap_md5); - #endif - - memcpy(config.hw_addr, ap_md5, sizeof(config.hw_addr)); - */ - #ifdef CONFIG_METADATA metadata_init(); // create the metadata pipe if necessary #endif @@ -2540,12 +2511,10 @@ int main(int argc, char **argv) { #ifdef CONFIG_METADATA send_ssnc_metadata('svna', config.service_name, strlen(config.service_name), 1); char buffer[256] = ""; - snprintf(buffer, sizeof(buffer), "%d", - config.output_rate); + snprintf(buffer, sizeof(buffer), "%d", config.output_rate); send_ssnc_metadata('ofps', buffer, strlen(buffer), 1); - snprintf(buffer, sizeof(buffer), "%s", - sps_format_description_string(config.output_format)); - send_ssnc_metadata('ofmt', buffer, strlen(buffer), 1); + snprintf(buffer, sizeof(buffer), "%s", sps_format_description_string(config.output_format)); + send_ssnc_metadata('ofmt', buffer, strlen(buffer), 1); #endif activity_monitor_start(); // not yet for AP2 From e0e0ab644f613f190e20e2b5b60dd82eed3bfb85 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 16 Nov 2022 14:39:01 +0000 Subject: [PATCH 21/51] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 778caf5e..cfd11e52 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version 4.1-dev-786-gfc117e73 +==== +**Bug Fix** +* Fix a bug caused in the transition to version 4.1, where a change was made to how the 12-hex-digit prefix to the classic AirPlay service name was derived. Previously it was a hash of the service name, and in 4.1 this was changed to deriving it from one of the device's MAC addresses. This seems to cause problems with multiple instances of classic Shairport Sync running on the same system, as they would all get the same prefix. The fix was to generate the prefix from a hash of the service name and the device's MAC address, which should be unique for every instance. Addresses issue [#1581](https://github.com/mikebrady/shairport-sync/issues/1581). Thanks to [Christoph Bloemer (?)](https://github.com/chbloemer) for the report. + Version 4.1-dev-784-g60e913dd ==== **Bug Fix** From dfcc4e377a177260404dd18025f29e9feac99976 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 16 Nov 2022 16:45:37 +0000 Subject: [PATCH 22/51] Revert the change to the 12-digit calculation --- mdns.c | 52 +--------------------------------------------------- 1 file changed, 1 insertion(+), 51 deletions(-) diff --git a/mdns.c b/mdns.c index 5dbbc102..9a5465c1 100644 --- a/mdns.c +++ b/mdns.c @@ -48,19 +48,6 @@ extern mdns_backend mdns_external_avahi; extern mdns_backend mdns_external_dns_sd; #endif -#ifdef CONFIG_MBEDTLS -#include -#include -#endif - -#ifdef CONFIG_POLARSSL -#include -#endif - -#ifdef CONFIG_OPENSSL -#include -#endif - static mdns_backend *mdns_backends[] = { #ifdef CONFIG_AVAHI &mdns_avahi, @@ -78,48 +65,11 @@ static mdns_backend *mdns_backends[] = { NULL}; void mdns_register(char **txt_records, char **secondary_txt_records) { - - // calculate the 12-hex-digit prefix by hashing the service name. - uint8_t ap_md5[16]; - -debug(1,"size of hw_addr is %u.", sizeof(config.hw_addr)); -#ifdef CONFIG_OPENSSL - MD5_CTX ctx; - MD5_Init(&ctx); - MD5_Update(&ctx, config.service_name, strlen(config.service_name)); - MD5_Update(&ctx, config.hw_addr, sizeof(config.hw_addr)); - MD5_Final(ap_md5, &ctx); -#endif - -#ifdef CONFIG_MBEDTLS -#if MBEDTLS_VERSION_MINOR >= 7 - mbedtls_md5_context tctx; - mbedtls_md5_starts_ret(&tctx); - mbedtls_md5_update_ret(&tctx, (unsigned char *)config.service_name, strlen(config.service_name)); - mbedtls_md5_update_ret(&tctx, (unsigned char *)config.hw_addr, sizeof(config.hw_addr)); - mbedtls_md5_finish_ret(&tctx, ap_md5); -#else - mbedtls_md5_context tctx; - mbedtls_md5_starts(&tctx); - mbedtls_md5_update(&tctx, (unsigned char *)config.service_name, strlen(config.service_name)); - mbedtls_md5_update(&tctx, (unsigned char *)config.hw_addr, sizeof(config.hw_addr)); - mbedtls_md5_finish(&tctx, ap_md5); -#endif -#endif - -#ifdef CONFIG_POLARSSL - md5_context tctx; - md5_starts(&tctx); - md5_update(&tctx, (unsigned char *)config.service_name, strlen(config.service_name)); - md5_update(&tctx, (unsigned char *)config.hw_addr, sizeof(config.hw_addr)); - md5_finish(&tctx, ap_md5); -#endif - char *ap1_service_name = alloca(strlen(config.service_name) + 14); char *p = ap1_service_name; int i; for (i = 0; i < 6; i++) { - snprintf(p, 3, "%02X", ap_md5[i]); + snprintf(p, 3, "%02X", config.hw_addr[i]); p += 2; } *p++ = '@'; From 8a9e9ec9cf96a7df78c31864d2980adba83a2447 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 16 Nov 2022 16:49:06 +0000 Subject: [PATCH 23/51] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index cfd11e52..70a3bfd7 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version 4.1-dev-789-g75aa150e +==== +**Oops** +* Reverted the fix below. Something still to be done... + Version 4.1-dev-786-gfc117e73 ==== **Bug Fix** From 03d291f4646ecc89ef206b25068d0926573ed36c Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 17 Nov 2022 10:31:51 +0000 Subject: [PATCH 24/51] Second attempt. Modify the generation of the 12-digit classic AirPlay service name prefix so that it depends on the service name as well as the hardware address. --- common.h | 1 + dacp.c | 8 ++++---- mdns.c | 2 +- player.c | 14 +++++++------- rtsp.c | 8 ++++---- shairport.c | 39 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 56 insertions(+), 16 deletions(-) diff --git a/common.h b/common.h index a3f21126..9f323e1b 100644 --- a/common.h +++ b/common.h @@ -159,6 +159,7 @@ typedef struct { int mqtt_enable_remote; char *mqtt_empty_payload_substitute; #endif + uint8_t ap1_prefix[6]; uint8_t hw_addr[8]; // only needs 6 but 8 is handy when converting this to a number int port; int udp_port_base; diff --git a/dacp.c b/dacp.c index d557de24..49692491 100644 --- a/dacp.c +++ b/dacp.c @@ -1221,8 +1221,8 @@ int dacp_get_volume(int32_t *the_actual_volume) { http_response = dacp_get_speaker_list((dacp_spkr_stuff *)&speaker_info, 50, &speaker_count); if (http_response == 200) { // get our machine number - uint16_t *hn = (uint16_t *)config.hw_addr; - uint32_t *ln = (uint32_t *)(config.hw_addr + 2); + uint16_t *hn = (uint16_t *)config.ap1_prefix; + uint32_t *ln = (uint32_t *)(config.ap1_prefix + 2); uint64_t t1 = ntohs(*hn); uint64_t t2 = ntohl(*ln); int64_t machine_number = (t1 << 32) + t2; // this form is useful @@ -1275,8 +1275,8 @@ int dacp_set_volume(int32_t vo) { http_response = dacp_get_speaker_list((dacp_spkr_stuff *)&speaker_info, 50, &speaker_count); if (http_response == 200) { // get our machine number - uint16_t *hn = (uint16_t *)config.hw_addr; - uint32_t *ln = (uint32_t *)(config.hw_addr + 2); + uint16_t *hn = (uint16_t *)config.ap1_prefix; + uint32_t *ln = (uint32_t *)(config.ap1_prefix + 2); uint64_t t1 = ntohs(*hn); uint64_t t2 = ntohl(*ln); int64_t machine_number = (t1 << 32) + t2; // this form is useful diff --git a/mdns.c b/mdns.c index 9a5465c1..05366413 100644 --- a/mdns.c +++ b/mdns.c @@ -69,7 +69,7 @@ void mdns_register(char **txt_records, char **secondary_txt_records) { char *p = ap1_service_name; int i; for (i = 0; i < 6; i++) { - snprintf(p, 3, "%02X", config.hw_addr[i]); + snprintf(p, 3, "%02X", config.ap1_prefix[i]); p += 2; } *p++ = '@'; diff --git a/player.c b/player.c index ec282c1c..d7fa7fdc 100644 --- a/player.c +++ b/player.c @@ -380,7 +380,6 @@ static void init_buffer(rtsp_conn_info *conn) { buffers_allocated++; } } - debug(1, "%" PRId64 " buffers allocated, %" PRId64 " buffers released.", buffers_allocated, buffers_released); } static void free_audio_buffers(rtsp_conn_info *conn) { @@ -389,6 +388,7 @@ static void free_audio_buffers(rtsp_conn_info *conn) { free(conn->audio_buffer[i].data); buffers_released++; } + debug(1, "%" PRId64 " buffers allocated, %" PRId64 " buffers released.", buffers_allocated, buffers_released); } int first_possibly_missing_frame = -1; @@ -1730,14 +1730,14 @@ void player_thread_cleanup_handler(void *arg) { int64_t elapsedMin = (time_playing / 60) % 60; int64_t elapsedSec = time_playing % 60; if (conn->frame_rate_valid) - inform("Connection %d: Playback Stopped. Total playing time %02" PRId64 ":%02" PRId64 + inform("Connection %d: Playback stopped. Total playing time %02" PRId64 ":%02" PRId64 ":%02" PRId64 ". " "Output: %0.2f (raw), %0.2f (corrected) " "frames per second.", conn->connection_number, elapsedHours, elapsedMin, elapsedSec, conn->raw_frame_rate, conn->corrected_frame_rate); else - inform("Connection %d: Playback Stopped. Total playing time %02" PRId64 ":%02" PRId64 + inform("Connection %d: Playback stopped. Total playing time %02" PRId64 ":%02" PRId64 ":%02" PRId64 ".", conn->connection_number, elapsedHours, elapsedMin, elapsedSec); } @@ -2745,14 +2745,14 @@ void *player_thread_func(void *arg) { send_ssnc_metadata('styp', "Classic", strlen("Classic"), 1); #endif if (config.statistics_requested) - inform("Connection %d: Playback Started at frame %" PRId64 " -- AirPlay 1 Compatible.", + inform("Connection %d: Playback started at frame %" PRId64 " -- AirPlay 1 Compatible.", conn->connection_number, inframe->given_timestamp); } else { #ifdef CONFIG_METADATA_HUB send_ssnc_metadata('styp', "Realtime", strlen("Realtime"), 1); #endif if (config.statistics_requested) - inform("Connection %d: Playback Started at frame %" PRId64 " -- AirPlay 2 Realtime.", + inform("Connection %d: Playback started at frame %" PRId64 " -- AirPlay 2 Realtime.", conn->connection_number, inframe->given_timestamp); } } else { @@ -2760,7 +2760,7 @@ void *player_thread_func(void *arg) { send_ssnc_metadata('styp', "Buffered", strlen("Buffered"), 1); #endif if (config.statistics_requested) - inform("Connection %d: Playback Started at frame %" PRId64 " -- AirPlay 2 Buffered.", + inform("Connection %d: Playback started at frame %" PRId64 " -- AirPlay 2 Buffered.", conn->connection_number, inframe->given_timestamp); } #else @@ -2768,7 +2768,7 @@ void *player_thread_func(void *arg) { send_ssnc_metadata('styp', "Classic", strlen("Classic"), 1); #endif if (config.statistics_requested) - inform("Connection %d: Playback Started at frame %" PRId64 " -- AirPlay 1.", conn->connection_number, inframe->given_timestamp); + inform("Connection %d: Playback started at frame %" PRId64 " -- AirPlay 1.", conn->connection_number, inframe->given_timestamp); #endif } // not too sure if abs() is implemented for int64_t, so we'll do it manually diff --git a/rtsp.c b/rtsp.c index 0a940b05..f5a97e68 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1333,7 +1333,7 @@ enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn, rtsp_mes while (msg_size < 0) { if (conn->stop != 0) { - debug(3, "Connection %d: shutdown requested.", conn->connection_number); + debug(3, "Connection %d: Shutdown requested by client.", conn->connection_number); reply = rtsp_read_request_response_immediate_shutdown_requested; goto shutdown; } @@ -1342,7 +1342,7 @@ enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn, rtsp_mes if (nread == 0) { // a blocking read that returns zero means eof -- implies connection closed by client - debug(1, "Connection %d: RTSP connection closed by client.", + debug(1, "Connection %d: Connection closed by client.", conn->connection_number); reply = rtsp_read_request_response_channel_closed; // Note: the socket will be closed when the thread exits @@ -4751,7 +4751,7 @@ static void apple_challenge(int fd, rtsp_message *req, rtsp_message *resp) { } for (i = 0; i < 6; i++) - *bp++ = config.hw_addr[i]; + *bp++ = config.ap1_prefix[i]; int buflen, resplen; buflen = bp - buf; @@ -5005,7 +5005,7 @@ void rtsp_conversation_thread_cleanup_function(void *arg) { conn->audio_socket = 0; } if (conn->fd > 0) { - debug(1, "Connection %d: terminating -- closing RTSP connection socket %d: from %s:%u to self at %s:%u.", + debug(2, "Connection %d: terminating -- closing RTSP connection socket %d: from %s:%u to self at %s:%u.", conn->connection_number, conn->fd, conn->client_ip_string, conn->client_rtsp_port, conn->self_ip_string, conn->self_rtsp_port); close(conn->fd); diff --git a/shairport.c b/shairport.c index ac40c11f..07d2c234 100644 --- a/shairport.c +++ b/shairport.c @@ -2454,6 +2454,45 @@ int main(int argc, char **argv) { soxr_time_check_thread_started = 1; #endif + // calculate the 12-hex-digit prefix by hashing the service name. + uint8_t ap_md5[16]; + +debug(1,"size of hw_addr is %u.", sizeof(config.hw_addr)); +#ifdef CONFIG_OPENSSL + MD5_CTX ctx; + MD5_Init(&ctx); + MD5_Update(&ctx, config.service_name, strlen(config.service_name)); + MD5_Update(&ctx, config.hw_addr, sizeof(config.hw_addr)); + MD5_Final(ap_md5, &ctx); +#endif + +#ifdef CONFIG_MBEDTLS +#if MBEDTLS_VERSION_MINOR >= 7 + mbedtls_md5_context tctx; + mbedtls_md5_starts_ret(&tctx); + mbedtls_md5_update_ret(&tctx, (unsigned char *)config.service_name, strlen(config.service_name)); + mbedtls_md5_update_ret(&tctx, (unsigned char *)config.hw_addr, sizeof(config.hw_addr)); + mbedtls_md5_finish_ret(&tctx, ap_md5); +#else + mbedtls_md5_context tctx; + mbedtls_md5_starts(&tctx); + mbedtls_md5_update(&tctx, (unsigned char *)config.service_name, strlen(config.service_name)); + mbedtls_md5_update(&tctx, (unsigned char *)config.hw_addr, sizeof(config.hw_addr)); + mbedtls_md5_finish(&tctx, ap_md5); +#endif +#endif + +#ifdef CONFIG_POLARSSL + md5_context tctx; + md5_starts(&tctx); + md5_update(&tctx, (unsigned char *)config.service_name, strlen(config.service_name)); + md5_update(&tctx, (unsigned char *)config.hw_addr, sizeof(config.hw_addr)); + md5_finish(&tctx, ap_md5); +#endif + + memcpy(config.ap1_prefix, ap_md5, sizeof(config.ap1_prefix)); + + #ifdef CONFIG_METADATA metadata_init(); // create the metadata pipe if necessary #endif From 2c87ade740c7eb7a24e508ec2dea076a61f2807a Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 17 Nov 2022 10:38:35 +0000 Subject: [PATCH 25/51] Release notes update. --- RELEASENOTES-DEVELOPMENT.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 70a3bfd7..fd2135b3 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,9 @@ +Version 4.1-dev-790-g03d291f4 +==== +**Bug Fix -- Second Attempt!** +* Second attempt to fix a bug caused in the transition to version 4.1, where a change was made to how the 12-hex-digit prefix to the classic AirPlay service name was derived. Previously it was a hash of the service name, and in 4.1 this was changed to deriving it from one of the device's MAC addresses. This seems to cause problems with multiple instances of classic Shairport Sync running on the same system, as they would all get the same prefix. The fix was to generate the prefix from a hash of the service name and the device's MAC address, which should be unique for every instance. Addresses issue [#1581](https://github.com/mikebrady/shairport-sync/issues/1581). Thanks to [Christoph Bloemer (?)](https://github.com/chbloemer) for the report. +The first attempt failed because not all the changes needed were done. + Version 4.1-dev-789-g75aa150e ==== **Oops** @@ -406,7 +412,8 @@ Version-4.1-dev-242-g3877acc7 Version-4.1-dev-238-g1dc481c3 ==== **Enhancement** -* Add some extra diagnostic information when large sync errors occur: if available, add the total number of frames sent to the output device and also, if available, add the current delay, in frames, in the output device. +* Add some extra diagnostic information when large sync errors occur: if available, add the total number of frames sent to the output device and also, if available, add th +e current delay, in frames, in the output device. Version-4.1-dev-234-ge8721889 ==== From e8b8c6edb7164f74d6b5e844dd7cd6645344a84f Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 18 Nov 2022 18:14:50 +0000 Subject: [PATCH 26/51] Clang cleanup --- activity_monitor.c | 6 +- audio_pa.c | 11 ++-- audio_pipe.c | 4 +- dacp.c | 5 +- dbus-service.c | 17 +++--- metadata_hub.c | 29 ++++----- mqtt.c | 5 +- player.c | 33 ++++++---- rtp.c | 4 +- rtsp.c | 148 +++++++++++++++++++++++---------------------- shairport.c | 3 +- 11 files changed, 142 insertions(+), 123 deletions(-) diff --git a/activity_monitor.c b/activity_monitor.c index 896a79f9..49f99d13 100644 --- a/activity_monitor.c +++ b/activity_monitor.c @@ -56,7 +56,8 @@ pthread_mutex_t activity_monitor_mutex; pthread_cond_t activity_monitor_cv; void going_active(int block) { - // debug(1, "activity_monitor: state transitioning to \"active\" with%s blocking", block ? "" : "out"); + // debug(1, "activity_monitor: state transitioning to \"active\" with%s blocking", block ? "" : + // "out"); if (config.cmd_active_start) command_execute(config.cmd_active_start, "", block); #ifdef CONFIG_METADATA @@ -75,7 +76,8 @@ void going_active(int block) { } void going_inactive(int block) { - // debug(1, "activity_monitor: state transitioning to \"inactive\" with%s blocking", block ? "" : "out"); + // debug(1, "activity_monitor: state transitioning to \"inactive\" with%s blocking", block ? "" : + // "out"); if (config.cmd_active_stop) command_execute(config.cmd_active_stop, "", block); #ifdef CONFIG_METADATA diff --git a/audio_pa.c b/audio_pa.c index e744e356..8f92ad39 100644 --- a/audio_pa.c +++ b/audio_pa.c @@ -222,10 +222,9 @@ static void do_start() { pa_threaded_mainloop_unlock(mainloop); } - static void start(__attribute__((unused)) int sample_rate, __attribute__((unused)) int sample_format) { - do_start(); + do_start(); } static int stream_is_open() { @@ -247,7 +246,7 @@ static int play(void *buf, int samples, __attribute__((unused)) int sample_type, if (stream_is_open() == 0) { // debug(1,"pa open stream before play."); do_start(); - } + } if (stream_is_open() != 0) { // copy the samples into the queue size_t bytes_to_transfer = samples * 2 * 2; @@ -287,7 +286,7 @@ int pa_delay(long *the_delay) { // debug(1,"pa open stream before delay."); do_start(); } - if (stream_is_open() != 0) { + if (stream_is_open() != 0) { pa_usec_t latency; int negative; pa_threaded_mainloop_lock(mainloop); @@ -304,7 +303,7 @@ int pa_delay(long *the_delay) { reply = 0; } } else { - // debug(2, "could not open pa stream for delay"); + // debug(2, "could not open pa stream for delay"); } *the_delay = result; return reply; @@ -327,7 +326,7 @@ void flush(void) { } } -static void stop(void) { +static void stop(void) { // debug(1,"Stop."); if (stream_is_open() != 0) { // Cork the stream so it will stop playing diff --git a/audio_pipe.c b/audio_pipe.c index 001f1385..e76c7300 100644 --- a/audio_pipe.c +++ b/audio_pipe.c @@ -141,7 +141,9 @@ static void deinit(void) { close(fd); } -static void help(void) { printf(" Provide the pipe's pathname. The default is \"%s\".\n", default_pipe_name); } +static void help(void) { + printf(" Provide the pipe's pathname. The default is \"%s\".\n", default_pipe_name); +} audio_output audio_pipe = {.name = "pipe", .help = &help, diff --git a/dacp.c b/dacp.c index 49692491..50cf0e17 100644 --- a/dacp.c +++ b/dacp.c @@ -803,8 +803,9 @@ void *dacp_monitor_thread_code(__attribute__((unused)) void *na) { case 'canp': // nowplaying 4 ids: dbid, plid, playlistItem, itemid (from mellowware // see reference above) debug(2, "DACP Composite ID seen"); - if ((metadata_store.item_composite_id_is_valid == 0) || (memcmp(metadata_store.item_composite_id, sp - item_size, - sizeof(metadata_store.item_composite_id)) != 0)) { + if ((metadata_store.item_composite_id_is_valid == 0) || + (memcmp(metadata_store.item_composite_id, sp - item_size, + sizeof(metadata_store.item_composite_id)) != 0)) { memcpy(metadata_store.item_composite_id, sp - item_size, sizeof(metadata_store.item_composite_id)); char st[33]; diff --git a/dbus-service.c b/dbus-service.c index 9c0bd718..d7a3e049 100644 --- a/dbus-service.c +++ b/dbus-service.c @@ -68,7 +68,8 @@ void dbus_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)) argc->airplay_volume); shairport_sync_remote_control_set_client(shairportSyncRemoteControlSkeleton, argc->client_ip); - shairport_sync_remote_control_set_client_name(shairportSyncRemoteControlSkeleton, argc->client_name); + shairport_sync_remote_control_set_client_name(shairportSyncRemoteControlSkeleton, + argc->client_name); // although it's a DACP server, the server is in fact, part of the the AirPlay "client" (their // term). @@ -102,11 +103,9 @@ void dbus_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)) if ((th == NULL) || (strcasecmp(th, argc->stream_type) != 0)) { // debug(1, "Stream type string should be changed"); shairport_sync_remote_control_set_stream_type(shairportSyncRemoteControlSkeleton, - argc->stream_type); + argc->stream_type); } } - - switch (argc->player_state) { case PS_NOT_AVAILABLE: @@ -224,8 +223,9 @@ void dbus_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)) } // Add in the Song Data Kind based on the 'asdk' metadata if it is valid - // It seems that this is 0 for a timed play, e.g. a track or an album, but is 1 for an untimed play, such as a stream. - + // It seems that this is 0 for a timed play, e.g. a track or an album, but is 1 for an untimed + // play, such as a stream. + if (argc->song_data_kind_is_valid != 0) { GVariant *songdatakind = g_variant_new_uint32(argc->song_data_kind); g_variant_builder_add(dict_builder, "{sv}", "sps:songdatakind", songdatakind); @@ -1042,7 +1042,8 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name shairport_sync_set_service_name(SHAIRPORT_SYNC(shairportSyncSkeleton), config.service_name); shairport_sync_set_output_rate(SHAIRPORT_SYNC(shairportSyncSkeleton), config.output_rate); - shairport_sync_set_output_format(SHAIRPORT_SYNC(shairportSyncSkeleton), sps_format_description_string(config.output_format)); + shairport_sync_set_output_format(SHAIRPORT_SYNC(shairportSyncSkeleton), + sps_format_description_string(config.output_format)); #ifdef CONFIG_AIRPLAY_2 shairport_sync_set_protocol(SHAIRPORT_SYNC(shairportSyncSkeleton), "AirPlay 2"); @@ -1157,7 +1158,7 @@ void stop_dbus_service() { if (ownerID) { g_bus_unown_name(ownerID); } else if (service_is_running != 0) { - debug(1, "Zero OwnerID for running \"org.gnome.ShairportSync\" dbus service."); + debug(1, "Zero OwnerID for running \"org.gnome.ShairportSync\" dbus service."); } service_is_running = 0; } diff --git a/metadata_hub.c b/metadata_hub.c index 29190f90..ec98e2a1 100644 --- a/metadata_hub.c +++ b/metadata_hub.c @@ -337,7 +337,7 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin // Some metadata items are contained in one metadata packet. // The start of the metadata packet is signalled by an 'ssnc' 'mdst' item and - // the end of it by an 'ssnc 'mden' item. + // the end of it by an 'ssnc 'mden' item. // We don't set "changed" for them individually; instead we set it when the 'mden' token // comes in if the metadata_packet_item_changed item is set by parsed items // within the packet. @@ -351,10 +351,11 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin switch (code) { case 'asdk': { // get the one-byte number as an unsigned number - int song_data_kind = data[0]; // one byte + int song_data_kind = data[0]; // one byte song_data_kind = song_data_kind & 0xFF; // unsigned debug(2, "MH Song Data Kind seen: \"%d\" of length %u.", song_data_kind, length); - if ((song_data_kind != metadata_store.song_data_kind) || (metadata_store.song_data_kind_is_valid == 0)) { + if ((song_data_kind != metadata_store.song_data_kind) || + (metadata_store.song_data_kind_is_valid == 0)) { metadata_store.song_data_kind = song_data_kind; metadata_store.song_data_kind_changed = 1; metadata_store.song_data_kind_is_valid = 1; @@ -380,7 +381,8 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin case 'astm': { uint32_t ui = ntohl(*(uint32_t *)data); debug(2, "MH Song Time seen: \"%u\" of length %u.", ui, length); - if ((ui != metadata_store.songtime_in_milliseconds) || (metadata_store.songtime_in_milliseconds_is_valid == 0)) { + if ((ui != metadata_store.songtime_in_milliseconds) || + (metadata_store.songtime_in_milliseconds_is_valid == 0)) { metadata_store.songtime_in_milliseconds = ui; metadata_store.songtime_in_milliseconds_changed = 1; metadata_store.songtime_in_milliseconds_is_valid = 1; @@ -584,8 +586,7 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin break; case 'styp': cs = strndup(data, length); - if (string_update(&metadata_store.stream_type, &metadata_store.stream_type_changed, - cs)) { + if (string_update(&metadata_store.stream_type, &metadata_store.stream_type_changed, cs)) { changed = 1; debug(2, "MH Stream Type set to: \"%s\"", metadata_store.stream_type); } @@ -624,14 +625,14 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin changed = (metadata_store.player_state != PS_PAUSED); metadata_store.player_state = PS_PAUSED; break; -/* -// not using this anymore. - case 'pffr': // this is sent when the first frame has been received - case 'prsm': - changed = (metadata_store.player_state != PS_PLAYING); - metadata_store.player_state = PS_PLAYING; - break; -*/ + /* + // not using this anymore. + case 'pffr': // this is sent when the first frame has been received + case 'prsm': + changed = (metadata_store.player_state != PS_PLAYING); + metadata_store.player_state = PS_PLAYING; + break; + */ case 'pvol': { // Note: it's assumed that the config.airplay volume has already been correctly set. // int32_t actual_volume; diff --git a/mqtt.c b/mqtt.c index cb426bdf..cf2e2388 100644 --- a/mqtt.c +++ b/mqtt.c @@ -179,9 +179,10 @@ void mqtt_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t le mqtt_publish("songalbum", data, length); break; case 'asdk': - mqtt_publish("songdatakind", data, length); // 0 seem to be a timed item, 1 an untimed stream + mqtt_publish("songdatakind", data, + length); // 0 seem to be a timed item, 1 an untimed stream break; - case 'clip': + case 'clip': mqtt_publish("client_ip", data, length); break; case 'cdid': diff --git a/player.c b/player.c index d7fa7fdc..ab85cb7b 100644 --- a/player.c +++ b/player.c @@ -374,7 +374,9 @@ static void init_buffer(rtsp_conn_info *conn) { // conn->max_frames_per_packet); void *allocation = malloc(8 * conn->max_frames_per_packet); if (allocation == NULL) { - die("could not allocate memory for audio buffers. %" PRId64 " buffers allocated, %" PRId64 " buffers released.", buffers_allocated, buffers_released); + die("could not allocate memory for audio buffers. %" PRId64 " buffers allocated, %" PRId64 + " buffers released.", + buffers_allocated, buffers_released); } else { conn->audio_buffer[i].data = allocation; buffers_allocated++; @@ -388,7 +390,8 @@ static void free_audio_buffers(rtsp_conn_info *conn) { free(conn->audio_buffer[i].data); buffers_released++; } - debug(1, "%" PRId64 " buffers allocated, %" PRId64 " buffers released.", buffers_allocated, buffers_released); + debug(1, "%" PRId64 " buffers allocated, %" PRId64 " buffers released.", buffers_allocated, + buffers_released); } int first_possibly_missing_frame = -1; @@ -2741,34 +2744,38 @@ void *player_thread_func(void *arg) { #ifdef CONFIG_AIRPLAY_2 if (conn->airplay_stream_type == realtime_stream) { if (conn->airplay_type == ap_1) { -#ifdef CONFIG_METADATA_HUB +#ifdef CONFIG_METADATA_HUB send_ssnc_metadata('styp', "Classic", strlen("Classic"), 1); #endif if (config.statistics_requested) - inform("Connection %d: Playback started at frame %" PRId64 " -- AirPlay 1 Compatible.", - conn->connection_number, inframe->given_timestamp); + inform("Connection %d: Playback started at frame %" PRId64 + " -- AirPlay 1 Compatible.", + conn->connection_number, inframe->given_timestamp); } else { -#ifdef CONFIG_METADATA_HUB +#ifdef CONFIG_METADATA_HUB send_ssnc_metadata('styp', "Realtime", strlen("Realtime"), 1); #endif if (config.statistics_requested) - inform("Connection %d: Playback started at frame %" PRId64 " -- AirPlay 2 Realtime.", - conn->connection_number, inframe->given_timestamp); + inform("Connection %d: Playback started at frame %" PRId64 + " -- AirPlay 2 Realtime.", + conn->connection_number, inframe->given_timestamp); } } else { -#ifdef CONFIG_METADATA_HUB +#ifdef CONFIG_METADATA_HUB send_ssnc_metadata('styp', "Buffered", strlen("Buffered"), 1); #endif if (config.statistics_requested) - inform("Connection %d: Playback started at frame %" PRId64 " -- AirPlay 2 Buffered.", - conn->connection_number, inframe->given_timestamp); + inform("Connection %d: Playback started at frame %" PRId64 + " -- AirPlay 2 Buffered.", + conn->connection_number, inframe->given_timestamp); } #else -#ifdef CONFIG_METADATA_HUB +#ifdef CONFIG_METADATA_HUB send_ssnc_metadata('styp', "Classic", strlen("Classic"), 1); #endif if (config.statistics_requested) - inform("Connection %d: Playback started at frame %" PRId64 " -- AirPlay 1.", conn->connection_number, inframe->given_timestamp); + inform("Connection %d: Playback started at frame %" PRId64 " -- AirPlay 1.", + conn->connection_number, inframe->given_timestamp); #endif } // not too sure if abs() is implemented for int64_t, so we'll do it manually diff --git a/rtp.c b/rtp.c index 88e05d0d..e61d521c 100644 --- a/rtp.c +++ b/rtp.c @@ -2842,12 +2842,12 @@ void *rtp_buffered_audio_processor(void *arg) { // https://libsodium.gitbook.io/doc/secret-key_cryptography/aead/chacha20-poly1305/ietf_chacha20-poly1305_construction // Note: the eight-byte nonce must be front-padded out to 12 bytes. - + response = crypto_aead_chacha20poly1305_ietf_decrypt( m + 7, // m &new_payload_length, // mlen_p NULL, // nsec, - packet + 12, // the ciphertext starts 12 bytes in and is followed by the MAC tag, + packet + 12, // the ciphertext starts 12 bytes in and is followed by the MAC tag, nread - (8 + 12), // clen -- the last 8 bytes are the nonce packet + 4, // authenticated additional data 8, // authenticated additional data length diff --git a/rtsp.c b/rtsp.c index f5a97e68..3cabe7b0 100644 --- a/rtsp.c +++ b/rtsp.c @@ -1219,12 +1219,13 @@ ssize_t timed_read_from_rtsp_connection(rtsp_conn_info *conn, uint64_t wait_time ssize_t result = 0; // closed if (conn->fd > 0) { int64_t remaining_time = 0; - uint64_t time_to_wait_to = get_absolute_time_in_ns();; + uint64_t time_to_wait_to = get_absolute_time_in_ns(); + ; time_to_wait_to = time_to_wait_to + wait_time; // remaining_time will be zero if wait_time is zero if (wait_time != 0) { - remaining_time = time_to_wait_to - get_absolute_time_in_ns(); + remaining_time = time_to_wait_to - get_absolute_time_in_ns(); } do { struct timeval tv; @@ -1240,7 +1241,8 @@ ssize_t timed_read_from_rtsp_connection(rtsp_conn_info *conn, uint64_t wait_time #ifdef CONFIG_AIRPLAY_2 if (conn->ap2_pairing_context.control_cipher_bundle.cipher_ctx) { conn->ap2_pairing_context.control_cipher_bundle.is_encrypted = 1; - result = read_encrypted(conn->fd, &conn->ap2_pairing_context.control_cipher_bundle, buf, count); + result = + read_encrypted(conn->fd, &conn->ap2_pairing_context.control_cipher_bundle, buf, count); } else { result = read(conn->fd, buf, count); } @@ -1250,11 +1252,13 @@ ssize_t timed_read_from_rtsp_connection(rtsp_conn_info *conn, uint64_t wait_time if (wait_time != 0) remaining_time = time_to_wait_to - get_absolute_time_in_ns(); if (((result == -1) && ((errno == EAGAIN) || (errno == EWOULDBLOCK))) && (remaining_time > 0)) - debug(1,"remaining time on a timed read is %" PRId64 " ns.", remaining_time); - } while (((result == -1) && ((errno == EAGAIN) || (errno == EWOULDBLOCK))) && (remaining_time > 0)); + debug(1, "remaining time on a timed read is %" PRId64 " ns.", remaining_time); + } while (((result == -1) && ((errno == EAGAIN) || (errno == EWOULDBLOCK))) && + (remaining_time > 0)); } else { - debug(1,"Connection %d: attempt to read from a closed RTSP connection.",conn->connection_number); + debug(1, "Connection %d: attempt to read from a closed RTSP connection.", + conn->connection_number); } return result; } @@ -1268,9 +1272,7 @@ void set_client_as_ptp_clock(rtsp_conn_info *conn) { ptp_send_control_message_string(timing_list_message); } -void clear_ptp_clock() { - ptp_send_control_message_string("T"); -} +void clear_ptp_clock() { ptp_send_control_message_string("T"); } #endif ssize_t read_from_rtsp_connection(rtsp_conn_info *conn, void *buf, size_t count) { @@ -1341,9 +1343,8 @@ enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn, rtsp_mes nread = read_from_rtsp_connection(conn, buf + inbuf, buflen - inbuf); if (nread == 0) { - // a blocking read that returns zero means eof -- implies connection closed by client - debug(1, "Connection %d: Connection closed by client.", - conn->connection_number); + // a blocking read that returns zero means eof -- implies connection closed by client + debug(1, "Connection %d: Connection closed by client.", conn->connection_number); reply = rtsp_read_request_response_channel_closed; // Note: the socket will be closed when the thread exits goto shutdown; @@ -1669,7 +1670,6 @@ void handle_record(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) #ifdef CONFIG_AIRPLAY_2 - void handle_get_info(__attribute((unused)) rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) { debug_log_rtsp_message(2, "GET /info:", req); @@ -1891,8 +1891,8 @@ void handle_flushbuffered(rtsp_conn_info *conn, rtsp_message *req, rtsp_message if ((conn->ap2_flush_requested != 0) && (conn->ap2_flush_from_valid != 0) && (flushFromValid != 0)) { // if there is a request already, and it's a deferred request, and the current request is also - // deferred... do nothing! -- leave the starting point in place. Yeah, yeah, we know de Morgan's - // Law, but this seems clearer + // deferred... do nothing! -- leave the starting point in place. Yeah, yeah, we know de + // Morgan's Law, but this seems clearer } else { conn->ap2_flush_from_sequence_number = flushFromSeq; conn->ap2_flush_from_rtp_timestamp = flushFromTS; @@ -1912,7 +1912,7 @@ void handle_flushbuffered(rtsp_conn_info *conn, rtsp_message *req, rtsp_message debug(2, "Deferred Flush Requested"); else debug(2, "Immediate Flush Requested"); - + plist_free(messagePlist); // display_all_flush_requests(conn); } @@ -2011,7 +2011,7 @@ void handle_setrateanchori(rtsp_conn_info *conn, rtsp_message *req, rtsp_message #ifdef CONFIG_METADATA send_ssnc_metadata('paus', NULL, 0, 1); // pause -- contains cancellation points #endif - if (config.output->stop) { + if (config.output->stop) { debug(2, "Connection %d: Stop the output backend.", conn->connection_number); config.output->stop(); } @@ -2567,38 +2567,38 @@ void handle_setpeers(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp debug(2, "Connection %d: SETPEERS %s Content-Length %d", conn->connection_number, req->path, req->contentlength); debug_log_rtsp_message(2, "SETPEERS request", req); -/* - char timing_list_message[4096]; - timing_list_message[0] = 'T'; - timing_list_message[1] = 0; + /* + char timing_list_message[4096]; + timing_list_message[0] = 'T'; + timing_list_message[1] = 0; - // ensure the client itself is first -- it's okay if it's duplicated later - strncat(timing_list_message, " ", sizeof(timing_list_message) - 1 - strlen(timing_list_message)); - strncat(timing_list_message, (const char *)&conn->client_ip_string, - sizeof(timing_list_message) - 1 - strlen(timing_list_message)); + // ensure the client itself is first -- it's okay if it's duplicated later + strncat(timing_list_message, " ", sizeof(timing_list_message) - 1 - + strlen(timing_list_message)); strncat(timing_list_message, (const char + *)&conn->client_ip_string, sizeof(timing_list_message) - 1 - strlen(timing_list_message)); - plist_t addresses_array = NULL; - plist_from_memory(req->content, req->contentlength, &addresses_array); - uint32_t items = plist_array_get_size(addresses_array); - if (items) { - uint32_t item; - for (item = 0; item < items; item++) { - plist_t n = plist_array_get_item(addresses_array, item); - char *ip_address = NULL; - plist_get_string_val(n, &ip_address); - // debug(1,ip_address); - strncat(timing_list_message, " ", - sizeof(timing_list_message) - 1 - strlen(timing_list_message)); - strncat(timing_list_message, ip_address, - sizeof(timing_list_message) - 1 - strlen(timing_list_message)); - if (ip_address != NULL) - free(ip_address); + plist_t addresses_array = NULL; + plist_from_memory(req->content, req->contentlength, &addresses_array); + uint32_t items = plist_array_get_size(addresses_array); + if (items) { + uint32_t item; + for (item = 0; item < items; item++) { + plist_t n = plist_array_get_item(addresses_array, item); + char *ip_address = NULL; + plist_get_string_val(n, &ip_address); + // debug(1,ip_address); + strncat(timing_list_message, " ", + sizeof(timing_list_message) - 1 - strlen(timing_list_message)); + strncat(timing_list_message, ip_address, + sizeof(timing_list_message) - 1 - strlen(timing_list_message)); + if (ip_address != NULL) + free(ip_address); + } + ptp_send_control_message_string(timing_list_message); } - ptp_send_control_message_string(timing_list_message); - } - plist_free(addresses_array); -*/ - //set_client_as_ptp_clock(conn); + plist_free(addresses_array); + */ + // set_client_as_ptp_clock(conn); resp->respcode = 200; } #endif @@ -2845,7 +2845,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) plist_t messagePlist = plist_from_rtsp_content(req); plist_t setupResponsePlist = plist_new_dict(); resp->respcode = 400; - + // see if we can get a name for the client char *clientNameString = NULL; plist_t nameItem = plist_dict_get_item(messagePlist, "name"); @@ -2864,7 +2864,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) "and open a TCP port.", conn->connection_number); conn->airplay_stream_category = unspecified_stream_category; - + // figure out what category of stream it is, by looking at the plist plist_t timingProtocol = plist_dict_get_item(messagePlist, "timingProtocol"); if (timingProtocol != NULL) { @@ -2873,8 +2873,8 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) if (timingProtocolString) { if (strcmp(timingProtocolString, "PTP") == 0) { debug(1, "Connection %d: AP2 PTP connection from %s:%u (\"%s\") to self at %s:%u.", - conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, clientNameString, - conn->self_ip_string, conn->self_rtsp_port); + conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, + clientNameString, conn->self_ip_string, conn->self_rtsp_port); conn->airplay_stream_category = ptp_stream; conn->timing_type = ts_ptp; #ifdef CONFIG_METADATA @@ -2888,23 +2888,27 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) #endif } else if (strcmp(timingProtocolString, "NTP") == 0) { debug(1, "Connection %d: SETUP: NTP setup from %s:%u (\"%s\") to self at %s:%u.", - conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, clientNameString, - conn->self_ip_string, conn->self_rtsp_port); + conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, + clientNameString, conn->self_ip_string, conn->self_rtsp_port); conn->airplay_stream_category = ntp_stream; conn->timing_type = ts_ntp; } else if (strcmp(timingProtocolString, "None") == 0) { - debug(3, "Connection %d: SETUP: a \"None\" setup detected from %s:%u (\"%s\") to self at %s:%u.", - conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, clientNameString, - conn->self_ip_string, conn->self_rtsp_port); + debug(3, + "Connection %d: SETUP: a \"None\" setup detected from %s:%u (\"%s\") to self at " + "%s:%u.", + conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, + clientNameString, conn->self_ip_string, conn->self_rtsp_port); // now check to see if it's got the "isRemoteControlOnly" item and check it's true plist_t isRemoteControlOnly = plist_dict_get_item(messagePlist, "isRemoteControlOnly"); if (isRemoteControlOnly != NULL) { uint8_t isRemoteControlOnlyBoolean = 0; plist_get_bool_val(isRemoteControlOnly, &isRemoteControlOnlyBoolean); if (isRemoteControlOnlyBoolean != 0) { - debug(1, "Connection %d: Remote Control connection from %s:%u (\"%s\") to self at %s:%u.", - conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, clientNameString, - conn->self_ip_string, conn->self_rtsp_port); + debug( + 1, + "Connection %d: Remote Control connection from %s:%u (\"%s\") to self at %s:%u.", + conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, + clientNameString, conn->self_ip_string, conn->self_rtsp_port); conn->airplay_stream_category = remote_control_stream; } else { debug(1, @@ -3037,7 +3041,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) freeifaddrs(addrs); // debug(1,"initial timing peer command: \"%s\".", timing_list_message); - //ptp_send_control_message_string(timing_list_message); + // ptp_send_control_message_string(timing_list_message); set_client_as_ptp_clock(conn); plist_dict_set_item(timingPeerInfoPlist, "Addresses", addresses); plist_dict_set_item(timingPeerInfoPlist, "ID", plist_new_string(conn->self_ip_string)); @@ -3158,7 +3162,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) // get the session key -- it must have one plist_t item = plist_dict_get_item(stream0, "shk"); // session key - uint64_t item_value = 0; // the length + uint64_t item_value = 0; // the length plist_get_data_val(item, (char **)&conn->session_key, &item_value); // more stuff @@ -3336,7 +3340,7 @@ void handle_setup_2(rtsp_conn_info *conn, rtsp_message *req, rtsp_message *resp) plist_array_append_item(streams_array, stream0dict); plist_dict_set_item(setupResponsePlist, "streams", streams_array); resp->respcode = 200; - + } else if (conn->airplay_stream_category == remote_control_stream) { debug(2, "Connection %d (RC): SETUP: Remote Control Stream received from %s.", conn->connection_number, conn->client_ip_string); @@ -4452,8 +4456,8 @@ static void handle_announce(rtsp_conn_info *conn, rtsp_message *req, rtsp_messag conn->airplay_type = ap_1; conn->timing_type = ts_ntp; debug(1, "Connection %d: Classic AirPlay connection from %s:%u to self at %s:%u.", - conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, - conn->self_ip_string, conn->self_rtsp_port); + conn->connection_number, conn->client_ip_string, conn->client_rtsp_port, + conn->self_ip_string, conn->self_rtsp_port); #endif conn->stream.type = ast_unknown; @@ -4547,7 +4551,7 @@ static void handle_announce(rtsp_conn_info *conn, rtsp_message *req, rtsp_messag if ((paesiv == NULL) && (prsaaeskey == NULL)) { // debug(1,"Unencrypted session requested?"); conn->stream.encrypted = 0; - } else if ((paesiv != NULL) && (prsaaeskey != NULL)){ + } else if ((paesiv != NULL) && (prsaaeskey != NULL)) { conn->stream.encrypted = 1; // debug(1,"Encrypted session requested"); } else { @@ -4987,25 +4991,27 @@ void rtsp_conversation_thread_cleanup_function(void *arg) { debug(3, "Connection %d: terminating -- closing timing, control and audio sockets...", conn->connection_number); if (conn->control_socket) { - debug(3, "Connection %d: terminating -- closing control_socket %d.", - conn->connection_number, conn->control_socket); + debug(3, "Connection %d: terminating -- closing control_socket %d.", conn->connection_number, + conn->control_socket); close(conn->control_socket); conn->control_socket = 0; } if (conn->timing_socket) { - debug(3, "Connection %d: terminating -- closing timing_socket %d.", - conn->connection_number, conn->timing_socket); + debug(3, "Connection %d: terminating -- closing timing_socket %d.", conn->connection_number, + conn->timing_socket); close(conn->timing_socket); conn->timing_socket = 0; } if (conn->audio_socket) { - debug(3, "Connection %d: terminating -- closing audio_socket %d.", - conn->connection_number, conn->audio_socket); + debug(3, "Connection %d: terminating -- closing audio_socket %d.", conn->connection_number, + conn->audio_socket); close(conn->audio_socket); conn->audio_socket = 0; } if (conn->fd > 0) { - debug(2, "Connection %d: terminating -- closing RTSP connection socket %d: from %s:%u to self at %s:%u.", + debug(2, + "Connection %d: terminating -- closing RTSP connection socket %d: from %s:%u to self at " + "%s:%u.", conn->connection_number, conn->fd, conn->client_ip_string, conn->client_rtsp_port, conn->self_ip_string, conn->self_rtsp_port); close(conn->fd); diff --git a/shairport.c b/shairport.c index 07d2c234..3503deb7 100644 --- a/shairport.c +++ b/shairport.c @@ -2457,7 +2457,7 @@ int main(int argc, char **argv) { // calculate the 12-hex-digit prefix by hashing the service name. uint8_t ap_md5[16]; -debug(1,"size of hw_addr is %u.", sizeof(config.hw_addr)); + debug(1, "size of hw_addr is %u.", sizeof(config.hw_addr)); #ifdef CONFIG_OPENSSL MD5_CTX ctx; MD5_Init(&ctx); @@ -2492,7 +2492,6 @@ debug(1,"size of hw_addr is %u.", sizeof(config.hw_addr)); memcpy(config.ap1_prefix, ap_md5, sizeof(config.ap1_prefix)); - #ifdef CONFIG_METADATA metadata_init(); // create the metadata pipe if necessary #endif From 7f279efa3d5e9256a67f5bf9c3db7275c850460e Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 18 Nov 2022 22:50:21 +0000 Subject: [PATCH 27/51] Update BUILD.md --- BUILD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILD.md b/BUILD.md index 527a41a3..f6dc3ad3 100644 --- a/BUILD.md +++ b/BUILD.md @@ -55,7 +55,7 @@ For AirPlay 2 operation, _before you install the libraries_, please ensure the y # yum update # yum install make automake gcc gcc-c++ \ git autoconf automake avahi-devel libconfig-devel openssl-devel popt-devel soxr-devel \ - ffmpeg ffmpeg-devel libplist-devel libsodium-devel libgcrypt-dev libuuid-devel vim-common \ + ffmpeg ffmpeg-devel libplist-devel libsodium-devel libgcrypt-devel libuuid-devel vim-common \ alsa-lib-devel ``` If you are building classic Shairport Sync, the list of packages is shorter: From 0b03f237952391c2ef8400adef11c7c39911d417 Mon Sep 17 00:00:00 2001 From: = <4265913+mikebrady@users.noreply.github.com> Date: Sat, 19 Nov 2022 09:05:26 +0000 Subject: [PATCH 28/51] Fix a bug in audio_alsa.c when there is no hardware device name. --- audio_alsa.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/audio_alsa.c b/audio_alsa.c index 62db843f..9a37f3f6 100644 --- a/audio_alsa.c +++ b/audio_alsa.c @@ -189,7 +189,9 @@ static int precision_delay_available() { // this is very crude -- if the device is a hardware device, then it's assumed the delay is // precise const char *output_device_name = snd_pcm_name(alsa_handle); - int is_a_real_hardware_device = (strstr(output_device_name, "hw:") == output_device_name); + int is_a_real_hardware_device = 0; + if (output_device_name != NULL) + is_a_real_hardware_device = (strstr(output_device_name, "hw:") == output_device_name); // The criteria as to whether precision delay is available // is whether the device driver returns non-zero update timestamps From 0b8fdf0d820cef44e8b9d44fd39de8e91979f205 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 19 Nov 2022 09:21:31 +0000 Subject: [PATCH 29/51] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index fd2135b3..2410bc89 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version 4.1-dev-795-g7f279efa +==== +**Bug Fix** +* Fix a bug if the ALSA device name isn't available. (Happens in Fedora 37 on Arm in VMWare Fusion on an Apple Silicon Mac.) + Version 4.1-dev-790-g03d291f4 ==== **Bug Fix -- Second Attempt!** From 803eddbdfb5f91771a1dd457ab6d368f6f323b40 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 26 Nov 2022 11:35:12 +0000 Subject: [PATCH 30/51] Docker: launch Shairport Sync as root user rather than as the user shairport-sync. --- docker/Dockerfile | 2 +- docker/classic/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index b32c7bdf..89308350 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -109,4 +109,4 @@ RUN addgroup -g 29 docker_audio && addgroup shairport-sync docker_audio && addgr # Remove anything we don't need. RUN rm -rf /lib/apk/db/* -ENTRYPOINT [ "/init", "s6-setuidgid", "shairport-sync", "/usr/local/bin/shairport-sync" ] +ENTRYPOINT [ "/init", "/usr/local/bin/shairport-sync" ] diff --git a/docker/classic/Dockerfile b/docker/classic/Dockerfile index 36f0b9c4..65ba957d 100644 --- a/docker/classic/Dockerfile +++ b/docker/classic/Dockerfile @@ -88,4 +88,4 @@ RUN addgroup -g 29 docker_audio && addgroup shairport-sync docker_audio && addgr # Remove anything we don't need. RUN rm -rf /lib/apk/db/* -ENTRYPOINT [ "/init", "s6-setuidgid", "shairport-sync", "/usr/local/bin/shairport-sync" ] +ENTRYPOINT [ "/init", "/usr/local/bin/shairport-sync" ] From f0aae9601eb612f61bc6498cfeacc9e01e1dcfb3 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 26 Nov 2022 11:47:02 +0000 Subject: [PATCH 31/51] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 2410bc89..eb6720a4 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,7 @@ +Version 4.1-dev-798-g803eddbd +**Docker** +* Within the Docker container, launch Shairport Sync as `root` user rather than as the user `shairport-sync`. This is experimental, to investigate possible device access issues. + Version 4.1-dev-795-g7f279efa ==== **Bug Fix** From 7c1a5fd0ba8fabf98a1c61082da05763286cd582 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 26 Nov 2022 15:56:25 +0000 Subject: [PATCH 32/51] Add ability to send frame/time updates at intervals as metadata. --- common.h | 1 + player.c | 62 +++++++++++++++++++++++++++++++++++++++++++++++------ shairport.c | 5 +++++ 3 files changed, 61 insertions(+), 7 deletions(-) diff --git a/common.h b/common.h index 9f323e1b..639c1c0f 100644 --- a/common.h +++ b/common.h @@ -141,6 +141,7 @@ typedef struct { int metadata_sockport; size_t metadata_sockmsglength; int get_coverart; + double metadata_progress_interval; // 0 means no progress reports #endif #ifdef CONFIG_MQTT int mqtt_enabled; diff --git a/player.c b/player.c index ab85cb7b..ebab2c01 100644 --- a/player.c +++ b/player.c @@ -1837,7 +1837,9 @@ void player_thread_cleanup_handler(void *arg) { void *player_thread_func(void *arg) { rtsp_conn_info *conn = (rtsp_conn_info *)arg; - +#ifdef CONFIG_METADATA + uint64_t time_of_last_metadata_progress_update; +#endif uint64_t previous_frames_played = 0; // initialised to avoid a "possibly uninitialised" warning uint64_t previous_raw_measurement_time = 0; // initialised to avoid a "possibly uninitialised" warning @@ -2197,7 +2199,7 @@ void *player_thread_func(void *arg) { debug(2, "Play begin"); while (1) { - + int this_is_the_first_frame = 0; //will be set if it is // check a few parameters to ensure they are non-zero if (config.output_rate == 0) debug(1, "config.output_rate is zero!"); @@ -2690,6 +2692,7 @@ void *player_thread_func(void *arg) { if (at_least_one_frame_seen_this_session == 0) { at_least_one_frame_seen_this_session = 1; + this_is_the_first_frame = 1; // debug(2,"first frame real sync error (positive --> late): %" PRId64 " frames.", // sync_error); @@ -2740,11 +2743,10 @@ void *player_thread_func(void *arg) { sync_error = 0; // say the error was fixed! } // since this is the first frame of audio, inform the user if requested... - #ifdef CONFIG_AIRPLAY_2 if (conn->airplay_stream_type == realtime_stream) { if (conn->airplay_type == ap_1) { -#ifdef CONFIG_METADATA_HUB +#ifdef CONFIG_METADATA send_ssnc_metadata('styp', "Classic", strlen("Classic"), 1); #endif if (config.statistics_requested) @@ -2752,7 +2754,7 @@ void *player_thread_func(void *arg) { " -- AirPlay 1 Compatible.", conn->connection_number, inframe->given_timestamp); } else { -#ifdef CONFIG_METADATA_HUB +#ifdef CONFIG_METADATA send_ssnc_metadata('styp', "Realtime", strlen("Realtime"), 1); #endif if (config.statistics_requested) @@ -2761,7 +2763,7 @@ void *player_thread_func(void *arg) { conn->connection_number, inframe->given_timestamp); } } else { -#ifdef CONFIG_METADATA_HUB +#ifdef CONFIG_METADATA send_ssnc_metadata('styp', "Buffered", strlen("Buffered"), 1); #endif if (config.statistics_requested) @@ -2770,7 +2772,7 @@ void *player_thread_func(void *arg) { conn->connection_number, inframe->given_timestamp); } #else -#ifdef CONFIG_METADATA_HUB +#ifdef CONFIG_METADATA send_ssnc_metadata('styp', "Classic", strlen("Classic"), 1); #endif if (config.statistics_requested) @@ -3042,6 +3044,29 @@ void *player_thread_func(void *arg) { } uint64_t should_be_time; frame_to_local_time(inframe->given_timestamp, &should_be_time, conn); + +#ifdef CONFIG_METADATA + if (config.metadata_progress_interval != 0.0) { + char hb[128]; + if (this_is_the_first_frame != 0) { + memset(hb, 0, 128); + snprintf(hb, 127, "%" PRIu32 "/%" PRId64 "", inframe->given_timestamp, should_be_time); + send_ssnc_metadata('phbt', hb, strlen(hb), 1); + time_of_last_metadata_progress_update = local_time_now; + } else { + uint64_t mx = 1000000000; + uint64_t iv = config.metadata_progress_interval * mx; + iv = iv + time_of_last_metadata_progress_update; + int64_t delta = iv - local_time_now; + if (delta <= 0) { + memset(hb, 0, 128); + snprintf(hb, 127, "%" PRIu32 "/%" PRId64 "", inframe->given_timestamp, should_be_time); + send_ssnc_metadata('phbt', hb, strlen(hb), 1); + time_of_last_metadata_progress_update = local_time_now; + } + } + } +#endif config.output->play(conn->outbuf, play_samples, play_samples_are_timed, inframe->given_timestamp, should_be_time); } @@ -3075,6 +3100,7 @@ void *player_thread_func(void *arg) { // if this is the first frame, see if it's close to when it's supposed to be // release, which will be its time plus latency and any offset_time if (at_least_one_frame_seen_this_session == 0) { + this_is_the_first_frame = 1; at_least_one_frame_seen_this_session = 1; } @@ -3090,6 +3116,28 @@ void *player_thread_func(void *arg) { } uint64_t should_be_time; frame_to_local_time(inframe->given_timestamp, &should_be_time, conn); +#ifdef CONFIG_METADATA + if (config.metadata_progress_interval != 0.0) { + char hb[128]; + if (this_is_the_first_frame != 0) { + memset(hb, 0, 128); + snprintf(hb, 127, "%" PRIu32 "/%" PRId64 "", inframe->given_timestamp, should_be_time); + send_ssnc_metadata('phbt', hb, strlen(hb), 1); + time_of_last_metadata_progress_update = local_time_now; + } else { + uint64_t mx = 1000000000; + uint64_t iv = config.metadata_progress_interval * mx; + iv = iv + time_of_last_metadata_progress_update; + int64_t delta = iv - local_time_now; + if (delta <= 0) { + memset(hb, 0, 128); + snprintf(hb, 127, "%" PRIu32 "/%" PRId64 "", inframe->given_timestamp, should_be_time); + send_ssnc_metadata('phbt', hb, strlen(hb), 1); + time_of_last_metadata_progress_update = local_time_now; + } + } + } +#endif config.output->play(conn->outbuf, play_samples, play_samples_are_timed, inframe->given_timestamp, should_be_time); } diff --git a/shairport.c b/shairport.c index 3503deb7..a5e521ff 100644 --- a/shairport.c +++ b/shairport.c @@ -998,6 +998,11 @@ int parse_options(int argc, char **argv) { if (config_lookup_string(config.cfg, "metadata.pipe_name", &str)) { config.metadata_pipename = (char *)str; } + + if (config_lookup_float(config.cfg, "metadata.progress_interval", &dvalue)) { + config.metadata_progress_interval = dvalue; + } + if (config_lookup_string(config.cfg, "metadata.socket_address", &str)) { config.metadata_sockaddr = (char *)str; From 64858a238c7c5e9423104167b9a10b381bcdd6f5 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 26 Nov 2022 17:54:09 +0000 Subject: [PATCH 33/51] Add albumArtist and composer lists if available. Add a FramePosition property giving the RTP frame number and the time it is to be played. ASlso add a SetFramePositionUpdateInterval method to get it to be generated at intervals. --- dbus-service.c | 50 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/dbus-service.c b/dbus-service.c index d7a3e049..a61616c9 100644 --- a/dbus-service.c +++ b/dbus-service.c @@ -97,6 +97,16 @@ void dbus_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)) } } + if (argc->frame_position_string) { + // debug(1, "Check frame position string"); + th = shairport_sync_get_frame_position(shairportSyncSkeleton); + if ((th == NULL) || (strcasecmp(th, argc->frame_position_string) != 0)) { + // debug(1, "Frame position string should be changed"); + shairport_sync_set_frame_position(shairportSyncSkeleton, + argc->frame_position_string); + } + } + if (argc->stream_type) { // debug(1, "Check stream type"); th = shairport_sync_remote_control_get_stream_type(shairportSyncRemoteControlSkeleton); @@ -243,7 +253,7 @@ void dbus_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)) g_variant_builder_add(dict_builder, "{sv}", "xesam:album", album_name); } - // Add the artist name if it exists + // Add the artist name list if it exists if (argc->artist_name) { GVariantBuilder *artist_as = g_variant_builder_new(G_VARIANT_TYPE("as")); g_variant_builder_add(artist_as, "s", argc->artist_name); @@ -252,7 +262,25 @@ void dbus_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)) g_variant_builder_add(dict_builder, "{sv}", "xesam:artist", artists); } - // Add the genre if it exists + // Add the album artist list if it exists + if (argc->album_artist_name) { + GVariantBuilder *album_artist_as = g_variant_builder_new(G_VARIANT_TYPE("as")); + g_variant_builder_add(album_artist_as, "s", argc->album_artist_name); + GVariant *album_artists = g_variant_builder_end(album_artist_as); + g_variant_builder_unref(album_artist_as); + g_variant_builder_add(dict_builder, "{sv}", "xesam:albumArtist", album_artists); + } + + // Add the composer list if it exists + if (argc->composer) { + GVariantBuilder *composer_as = g_variant_builder_new(G_VARIANT_TYPE("as")); + g_variant_builder_add(composer_as, "s", argc->composer); + GVariant *composers = g_variant_builder_end(composer_as); + g_variant_builder_unref(composer_as); + g_variant_builder_add(dict_builder, "{sv}", "xesam:composer", composers); + } + + // Add the genre list if it exists if (argc->genre) { GVariantBuilder *genre_as = g_variant_builder_new(G_VARIANT_TYPE("as")); g_variant_builder_add(genre_as, "s", argc->genre); @@ -835,6 +863,18 @@ static gboolean on_handle_drop_session(ShairportSync *skeleton, GDBusMethodInvoc return TRUE; } +static gboolean on_handle_set_frame_position_update_interval(ShairportSync *skeleton, + GDBusMethodInvocation *invocation, + const gdouble seconds, + __attribute__((unused)) gpointer user_data) { + debug(1, ">> set frame position update interval to %.6f.", seconds); + config.metadata_progress_interval = seconds; + shairport_sync_complete_set_frame_position_update_interval(skeleton, invocation); + return TRUE; +} + + + static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name, __attribute__((unused)) gpointer user_data) { @@ -893,6 +933,10 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name g_signal_connect(shairportSyncSkeleton, "handle-drop-session", G_CALLBACK(on_handle_drop_session), NULL); + g_signal_connect(shairportSyncSkeleton, "handle-set-frame-position-update-interval", + G_CALLBACK(on_handle_set_frame_position_update_interval), + NULL); + g_signal_connect(shairportSyncDiagnosticsSkeleton, "notify::verbosity", G_CALLBACK(notify_verbosity_callback), NULL); @@ -952,7 +996,7 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name config.loudness_reference_volume_db); shairport_sync_set_drift_tolerance(SHAIRPORT_SYNC(shairportSyncSkeleton), config.tolerance); shairport_sync_set_volume(SHAIRPORT_SYNC(shairportSyncSkeleton), config.airplay_volume); - + #ifdef CONFIG_APPLE_ALAC if (config.use_apple_decoder == 0) { shairport_sync_set_alacdecoder(SHAIRPORT_SYNC(shairportSyncSkeleton), "hammerton"); From a9c1e5c42f915082674bdf860f72fdcd76469209 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 26 Nov 2022 17:55:27 +0000 Subject: [PATCH 34/51] Add a frame_position_string denoting the RTP frame being output and the time it is being played. --- metadata_hub.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/metadata_hub.h b/metadata_hub.h index 730e6e8c..63efb1c0 100644 --- a/metadata_hub.h +++ b/metadata_hub.h @@ -55,6 +55,9 @@ typedef struct metadata_bundle { char *progress_string; // progress string, emitted by the source from time to time int progress_string_changed; + char *frame_position_string; // frame position string emitted by SPS on request + int frame_position_string_changed; + int player_thread_active; // true if a play thread is running int dacp_server_active; // true if there's a reachable DACP server (assumed to be the Airplay // client) ; false otherwise From f3ac622321d8b159cdb44528e5e482c02a0d9873 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 26 Nov 2022 17:56:59 +0000 Subject: [PATCH 35/51] Add handling of a frame_position_string handler for the 'phbt' metadata token. --- metadata_hub.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/metadata_hub.c b/metadata_hub.c index ec98e2a1..db7eeef5 100644 --- a/metadata_hub.c +++ b/metadata_hub.c @@ -584,6 +584,15 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin } free(cs); break; + case 'phbt': + cs = strndup(data, length); + if (string_update(&metadata_store.frame_position_string, &metadata_store.frame_position_string_changed, + cs)) { + changed = 1; + debug(2, "MH Frame Position String set to: \"%s\"", metadata_store.progress_string); + } + free(cs); + break; case 'styp': cs = strndup(data, length); if (string_update(&metadata_store.stream_type, &metadata_store.stream_type_changed, cs)) { From 63c1f3e625920a3b209476c7764e5eeb14f964c0 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 26 Nov 2022 17:59:38 +0000 Subject: [PATCH 36/51] Add FramePosition property and SetFramePositionUpdateInterval method. --- org.gnome.ShairportSync.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/org.gnome.ShairportSync.xml b/org.gnome.ShairportSync.xml index 97b7de3f..75355a8b 100644 --- a/org.gnome.ShairportSync.xml +++ b/org.gnome.ShairportSync.xml @@ -27,6 +27,10 @@ + + + + From 50738c360d197b08081179dcc602af3f13665eb2 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 26 Nov 2022 18:09:57 +0000 Subject: [PATCH 37/51] SEnd metadata _after_ sending audio to the output. --- player.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/player.c b/player.c index ebab2c01..93512983 100644 --- a/player.c +++ b/player.c @@ -3045,7 +3045,10 @@ void *player_thread_func(void *arg) { uint64_t should_be_time; frame_to_local_time(inframe->given_timestamp, &should_be_time, conn); + config.output->play(conn->outbuf, play_samples, play_samples_are_timed, + inframe->given_timestamp, should_be_time); #ifdef CONFIG_METADATA + // debug(1,"config.metadata_progress_interval is %f.", config.metadata_progress_interval); if (config.metadata_progress_interval != 0.0) { char hb[128]; if (this_is_the_first_frame != 0) { @@ -3067,8 +3070,6 @@ void *player_thread_func(void *arg) { } } #endif - config.output->play(conn->outbuf, play_samples, play_samples_are_timed, - inframe->given_timestamp, should_be_time); } } @@ -3116,7 +3117,10 @@ void *player_thread_func(void *arg) { } uint64_t should_be_time; frame_to_local_time(inframe->given_timestamp, &should_be_time, conn); + config.output->play(conn->outbuf, play_samples, play_samples_are_timed, + inframe->given_timestamp, should_be_time); #ifdef CONFIG_METADATA + // debug(1,"config.metadata_progress_interval is %f.", config.metadata_progress_interval); if (config.metadata_progress_interval != 0.0) { char hb[128]; if (this_is_the_first_frame != 0) { @@ -3138,8 +3142,6 @@ void *player_thread_func(void *arg) { } } #endif - config.output->play(conn->outbuf, play_samples, play_samples_are_timed, - inframe->given_timestamp, should_be_time); } } From a3151c3c55b6e2b61906bc57c1ed0d38b2cb17c7 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 26 Nov 2022 18:45:34 +0000 Subject: [PATCH 38/51] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index eb6720a4..2ef31714 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,4 +1,13 @@ +Version 4.1-dev-805-g50738c36 +==== +**Enhancements** +* Very provisional additions to the metadata -- add some functionality for Shairport Sync to send information about what frame of audio is being played at a particular time. It can be set up to periodically generate a `phbt` (a Playing HeartBeaT) item comprising the RTP frame number of the audio being played at a particular instant -- the computer's system time in nanoseconds. The two numbers are presented as a string, e.g. `595422363/4697675115573193`. Set the interval in seconds with the new `metadata` setting `progress_interval` – 0.0 means no heartbeat is generated. +* Equivalent additions have been made to the native `DBus` interface: the `phbt` information appears as a `FramePosition` property and the update interval can be set using the `SetFramePositionUpdateInterval(Seconds)` method. + +Please note that this might all disappear or change radically. + Version 4.1-dev-798-g803eddbd +==== **Docker** * Within the Docker container, launch Shairport Sync as `root` user rather than as the user `shairport-sync`. This is experimental, to investigate possible device access issues. From 40446668978999ca36d7d5c843d08be3ec9f1e3f Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 26 Nov 2022 19:09:42 +0000 Subject: [PATCH 39/51] clang format --- dbus-service.c | 17 +++++++---------- metadata_hub.c | 4 ++-- player.c | 26 ++++++++++++++++---------- shairport.c | 3 +-- 4 files changed, 26 insertions(+), 24 deletions(-) diff --git a/dbus-service.c b/dbus-service.c index a61616c9..e957dfa5 100644 --- a/dbus-service.c +++ b/dbus-service.c @@ -102,8 +102,7 @@ void dbus_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)) th = shairport_sync_get_frame_position(shairportSyncSkeleton); if ((th == NULL) || (strcasecmp(th, argc->frame_position_string) != 0)) { // debug(1, "Frame position string should be changed"); - shairport_sync_set_frame_position(shairportSyncSkeleton, - argc->frame_position_string); + shairport_sync_set_frame_position(shairportSyncSkeleton, argc->frame_position_string); } } @@ -864,17 +863,16 @@ static gboolean on_handle_drop_session(ShairportSync *skeleton, GDBusMethodInvoc } static gboolean on_handle_set_frame_position_update_interval(ShairportSync *skeleton, - GDBusMethodInvocation *invocation, - const gdouble seconds, - __attribute__((unused)) gpointer user_data) { + GDBusMethodInvocation *invocation, + const gdouble seconds, + __attribute__((unused)) + gpointer user_data) { debug(1, ">> set frame position update interval to %.6f.", seconds); config.metadata_progress_interval = seconds; shairport_sync_complete_set_frame_position_update_interval(skeleton, invocation); return TRUE; } - - static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name, __attribute__((unused)) gpointer user_data) { @@ -934,8 +932,7 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name NULL); g_signal_connect(shairportSyncSkeleton, "handle-set-frame-position-update-interval", - G_CALLBACK(on_handle_set_frame_position_update_interval), - NULL); + G_CALLBACK(on_handle_set_frame_position_update_interval), NULL); g_signal_connect(shairportSyncDiagnosticsSkeleton, "notify::verbosity", G_CALLBACK(notify_verbosity_callback), NULL); @@ -996,7 +993,7 @@ static void on_dbus_name_acquired(GDBusConnection *connection, const gchar *name config.loudness_reference_volume_db); shairport_sync_set_drift_tolerance(SHAIRPORT_SYNC(shairportSyncSkeleton), config.tolerance); shairport_sync_set_volume(SHAIRPORT_SYNC(shairportSyncSkeleton), config.airplay_volume); - + #ifdef CONFIG_APPLE_ALAC if (config.use_apple_decoder == 0) { shairport_sync_set_alacdecoder(SHAIRPORT_SYNC(shairportSyncSkeleton), "hammerton"); diff --git a/metadata_hub.c b/metadata_hub.c index db7eeef5..1d9a9a28 100644 --- a/metadata_hub.c +++ b/metadata_hub.c @@ -586,8 +586,8 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin break; case 'phbt': cs = strndup(data, length); - if (string_update(&metadata_store.frame_position_string, &metadata_store.frame_position_string_changed, - cs)) { + if (string_update(&metadata_store.frame_position_string, + &metadata_store.frame_position_string_changed, cs)) { changed = 1; debug(2, "MH Frame Position String set to: \"%s\"", metadata_store.progress_string); } diff --git a/player.c b/player.c index 93512983..3c5b1d99 100644 --- a/player.c +++ b/player.c @@ -2199,7 +2199,7 @@ void *player_thread_func(void *arg) { debug(2, "Play begin"); while (1) { - int this_is_the_first_frame = 0; //will be set if it is + int this_is_the_first_frame = 0; // will be set if it is // check a few parameters to ensure they are non-zero if (config.output_rate == 0) debug(1, "config.output_rate is zero!"); @@ -3044,16 +3044,18 @@ void *player_thread_func(void *arg) { } uint64_t should_be_time; frame_to_local_time(inframe->given_timestamp, &should_be_time, conn); - + config.output->play(conn->outbuf, play_samples, play_samples_are_timed, inframe->given_timestamp, should_be_time); #ifdef CONFIG_METADATA - // debug(1,"config.metadata_progress_interval is %f.", config.metadata_progress_interval); + // debug(1,"config.metadata_progress_interval is %f.", + // config.metadata_progress_interval); if (config.metadata_progress_interval != 0.0) { char hb[128]; if (this_is_the_first_frame != 0) { memset(hb, 0, 128); - snprintf(hb, 127, "%" PRIu32 "/%" PRId64 "", inframe->given_timestamp, should_be_time); + snprintf(hb, 127, "%" PRIu32 "/%" PRId64 "", inframe->given_timestamp, + should_be_time); send_ssnc_metadata('phbt', hb, strlen(hb), 1); time_of_last_metadata_progress_update = local_time_now; } else { @@ -3063,11 +3065,12 @@ void *player_thread_func(void *arg) { int64_t delta = iv - local_time_now; if (delta <= 0) { memset(hb, 0, 128); - snprintf(hb, 127, "%" PRIu32 "/%" PRId64 "", inframe->given_timestamp, should_be_time); + snprintf(hb, 127, "%" PRIu32 "/%" PRId64 "", inframe->given_timestamp, + should_be_time); send_ssnc_metadata('phbt', hb, strlen(hb), 1); time_of_last_metadata_progress_update = local_time_now; } - } + } } #endif } @@ -3120,12 +3123,14 @@ void *player_thread_func(void *arg) { config.output->play(conn->outbuf, play_samples, play_samples_are_timed, inframe->given_timestamp, should_be_time); #ifdef CONFIG_METADATA - // debug(1,"config.metadata_progress_interval is %f.", config.metadata_progress_interval); + // debug(1,"config.metadata_progress_interval is %f.", + // config.metadata_progress_interval); if (config.metadata_progress_interval != 0.0) { char hb[128]; if (this_is_the_first_frame != 0) { memset(hb, 0, 128); - snprintf(hb, 127, "%" PRIu32 "/%" PRId64 "", inframe->given_timestamp, should_be_time); + snprintf(hb, 127, "%" PRIu32 "/%" PRId64 "", inframe->given_timestamp, + should_be_time); send_ssnc_metadata('phbt', hb, strlen(hb), 1); time_of_last_metadata_progress_update = local_time_now; } else { @@ -3135,11 +3140,12 @@ void *player_thread_func(void *arg) { int64_t delta = iv - local_time_now; if (delta <= 0) { memset(hb, 0, 128); - snprintf(hb, 127, "%" PRIu32 "/%" PRId64 "", inframe->given_timestamp, should_be_time); + snprintf(hb, 127, "%" PRIu32 "/%" PRId64 "", inframe->given_timestamp, + should_be_time); send_ssnc_metadata('phbt', hb, strlen(hb), 1); time_of_last_metadata_progress_update = local_time_now; } - } + } } #endif } diff --git a/shairport.c b/shairport.c index a5e521ff..96fdcbec 100644 --- a/shairport.c +++ b/shairport.c @@ -998,12 +998,11 @@ int parse_options(int argc, char **argv) { if (config_lookup_string(config.cfg, "metadata.pipe_name", &str)) { config.metadata_pipename = (char *)str; } - + if (config_lookup_float(config.cfg, "metadata.progress_interval", &dvalue)) { config.metadata_progress_interval = dvalue; } - if (config_lookup_string(config.cfg, "metadata.socket_address", &str)) { config.metadata_sockaddr = (char *)str; } From 37f060f48e90aa11d66f28dc1f06e91afa73ed3a Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 28 Nov 2022 15:09:43 +0000 Subject: [PATCH 40/51] Add "FirstFramePostion" property set when first frame of a play session is sent. --- dbus-service.c | 9 +++++++++ metadata_hub.c | 11 ++++++++++- metadata_hub.h | 3 +++ org.gnome.ShairportSync.xml | 1 + player.c | 2 ++ 5 files changed, 25 insertions(+), 1 deletion(-) diff --git a/dbus-service.c b/dbus-service.c index e957dfa5..4cfc8254 100644 --- a/dbus-service.c +++ b/dbus-service.c @@ -106,6 +106,15 @@ void dbus_metadata_watcher(struct metadata_bundle *argc, __attribute__((unused)) } } + if (argc->first_frame_position_string) { + // debug(1, "Check first frame position string"); + th = shairport_sync_get_first_frame_position(shairportSyncSkeleton); + if ((th == NULL) || (strcasecmp(th, argc->first_frame_position_string) != 0)) { + // debug(1, "First frame position string should be changed"); + shairport_sync_set_first_frame_position(shairportSyncSkeleton, argc->first_frame_position_string); + } + } + if (argc->stream_type) { // debug(1, "Check stream type"); th = shairport_sync_remote_control_get_stream_type(shairportSyncRemoteControlSkeleton); diff --git a/metadata_hub.c b/metadata_hub.c index 1d9a9a28..e39752fe 100644 --- a/metadata_hub.c +++ b/metadata_hub.c @@ -589,7 +589,16 @@ void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uin if (string_update(&metadata_store.frame_position_string, &metadata_store.frame_position_string_changed, cs)) { changed = 1; - debug(2, "MH Frame Position String set to: \"%s\"", metadata_store.progress_string); + debug(2, "MH Frame Position String set to: \"%s\"", metadata_store.frame_position_string); + } + free(cs); + break; + case 'phb0': + cs = strndup(data, length); + if (string_update(&metadata_store.first_frame_position_string, + &metadata_store.first_frame_position_string_changed, cs)) { + changed = 1; + debug(2, "MH First Frame Position String set to: \"%s\"", metadata_store.first_frame_position_string); } free(cs); break; diff --git a/metadata_hub.h b/metadata_hub.h index 63efb1c0..8235c6e4 100644 --- a/metadata_hub.h +++ b/metadata_hub.h @@ -58,6 +58,9 @@ typedef struct metadata_bundle { char *frame_position_string; // frame position string emitted by SPS on request int frame_position_string_changed; + char *first_frame_position_string; // first frame position string emitted by SPS on request + int first_frame_position_string_changed; + int player_thread_active; // true if a play thread is running int dacp_server_active; // true if there's a reachable DACP server (assumed to be the Airplay // client) ; false otherwise diff --git a/org.gnome.ShairportSync.xml b/org.gnome.ShairportSync.xml index 75355a8b..722fda5b 100644 --- a/org.gnome.ShairportSync.xml +++ b/org.gnome.ShairportSync.xml @@ -27,6 +27,7 @@ + diff --git a/player.c b/player.c index 3c5b1d99..9d8159a5 100644 --- a/player.c +++ b/player.c @@ -3056,6 +3056,7 @@ void *player_thread_func(void *arg) { memset(hb, 0, 128); snprintf(hb, 127, "%" PRIu32 "/%" PRId64 "", inframe->given_timestamp, should_be_time); + send_ssnc_metadata('phb0', hb, strlen(hb), 1); send_ssnc_metadata('phbt', hb, strlen(hb), 1); time_of_last_metadata_progress_update = local_time_now; } else { @@ -3131,6 +3132,7 @@ void *player_thread_func(void *arg) { memset(hb, 0, 128); snprintf(hb, 127, "%" PRIu32 "/%" PRId64 "", inframe->given_timestamp, should_be_time); + send_ssnc_metadata('phb0', hb, strlen(hb), 1); send_ssnc_metadata('phbt', hb, strlen(hb), 1); time_of_last_metadata_progress_update = local_time_now; } else { From f9afd747c781418c01a7d62fb3de6d397ae3c4ac Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Mon, 28 Nov 2022 15:15:17 +0000 Subject: [PATCH 41/51] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 2ef31714..d9979139 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version 4.1-dev-809-g8a69f5ea +==== +**Enhancements** +* Add another item of metadata with the code `phb0` and the DBus property name `FirstFramePosition`. This is similar to the `phbt` and `FramePosition` metadata, except that it is updated for the first frame of a play session. It can be ignored, as a `phbt` token is also sent when the first frame is output. + Version 4.1-dev-805-g50738c36 ==== **Enhancements** From 5bf379f7808eb442ab67b31e996e1afc049a227b Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 30 Nov 2022 16:23:26 +0000 Subject: [PATCH 42/51] Add detail about the new 'progress_interval' metadata setting. --- scripts/shairport-sync.conf | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/shairport-sync.conf b/scripts/shairport-sync.conf index 730b4c1c..660d2b2a 100644 --- a/scripts/shairport-sync.conf +++ b/scripts/shairport-sync.conf @@ -216,6 +216,9 @@ metadata = // cover_art_cache_directory = "/tmp/shairport-sync/.cache/coverart"; // artwork will be stored in this directory if the dbus or MPRIS interfaces are enabled or if the MQTT client is in use. Set it to "" to prevent caching, which may be useful on some systems // pipe_name = "/tmp/shairport-sync-metadata"; // pipe_timeout = 5000; // wait for this number of milliseconds for a blocked pipe to unblock before giving up +// progress_interval = 0.0; // if non-zero, progress 'phbt' messages will be sent at the interval specified in seconds. A 'phb0' message will also be sent when the first audio frame of a play session is about to be played. +// Each message consists of the RTPtime of a a frame of audio and the exact system time when it is to be played. The system time, in nanoseconds, is based the CLOCK_MONOTONIC_RAW of the machine -- if available -- or CLOCK_MONOTONIC otherwise. +// Messages are sent when the frame is placed in the output device's buffer, thus, they will be _approximately_ 'audio_backend_buffer_desired_length_in_seconds' (default 0.2 seconds) ahead of time. // socket_address = "226.0.0.1"; // if set to a host name or IP address, UDP packets containing metadata will be sent to this address. May be a multicast address. "socket-port" must be non-zero and "enabled" must be set to yes" // socket_port = 5555; // if socket_address is set, the port to send UDP packets to // socket_msglength = 65000; // the maximum packet size for any UDP metadata. This will be clipped to be between 500 or 65000. The default is 500. From e3532013769d7d32cec0086571e9790bf36aa9f1 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 30 Nov 2022 16:25:03 +0000 Subject: [PATCH 43/51] Add code to close the play session if the client connection drops for a minute. Uses the TCP keepalive facility. --- rtsp.c | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/rtsp.c b/rtsp.c index 3cabe7b0..7de596b6 100644 --- a/rtsp.c +++ b/rtsp.c @@ -47,6 +47,7 @@ #include #include #include +#include #include @@ -1222,6 +1223,11 @@ ssize_t timed_read_from_rtsp_connection(rtsp_conn_info *conn, uint64_t wait_time uint64_t time_to_wait_to = get_absolute_time_in_ns(); ; time_to_wait_to = time_to_wait_to + wait_time; + + int flags = 1; + if (setsockopt(conn->fd, SOL_SOCKET, SO_KEEPALIVE, (void *)&flags, sizeof(flags))) { + debug(1,"can't enable keepalive checking on the RTSP socket"); + } // remaining_time will be zero if wait_time is zero if (wait_time != 0) { @@ -1349,6 +1355,8 @@ enum rtsp_read_request_response rtsp_read_request(rtsp_conn_info *conn, rtsp_mes // Note: the socket will be closed when the thread exits goto shutdown; } + + // An ETIMEDOUT error usually means keepalive has failed. if (nread < 0) { if (errno == EINTR) @@ -5487,6 +5495,28 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { } else { size_of_reply = sizeof(SOCKADDR); if (getsockname(conn->fd, (struct sockaddr *)&conn->local, &size_of_reply) == 0) { + + // Thanks to https://holmeshe.me/network-essentials-setsockopt-SO_KEEPALIVE/ for this. + + // turn on keepalive stuff -- wait for keepidle + (keepcnt * keepinttvl time) seconds before giving up + // an ETIMEOUT error is returned if the keepalive check fails + + int flags = 35; // wait this many seconds before checking for a dropped client + if (setsockopt(conn->fd, SOL_TCP, TCP_KEEPIDLE, (void *)&flags, sizeof(flags))) { + debug(1,"can't set the keepidle wait time"); + } + + flags = 5; // check this many times + if (setsockopt(conn->fd, SOL_TCP, TCP_KEEPCNT, (void *)&flags, sizeof(flags))) { + debug(1,"can't set the keepidle missing count"); + } + + flags = 5; // wait this many seconds between checks + if (setsockopt(conn->fd, SOL_TCP, TCP_KEEPINTVL, (void *)&flags, sizeof(flags))) { + debug(1,"can't set the keepidle missing count interval"); + exit(0); + }; + // initialise the connection info void *client_addr = NULL, *self_addr = NULL; conn->connection_ip_family = conn->local.SAFAMILY; From 5f3f238b48132c4e116dc486b33870097ca1dbe0 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 30 Nov 2022 16:53:38 +0000 Subject: [PATCH 44/51] Make the keepalive stuff compile under FreeBSD --- rtsp.c | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/rtsp.c b/rtsp.c index 7de596b6..c105ec0a 100644 --- a/rtsp.c +++ b/rtsp.c @@ -5501,22 +5501,27 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { // turn on keepalive stuff -- wait for keepidle + (keepcnt * keepinttvl time) seconds before giving up // an ETIMEOUT error is returned if the keepalive check fails - int flags = 35; // wait this many seconds before checking for a dropped client - if (setsockopt(conn->fd, SOL_TCP, TCP_KEEPIDLE, (void *)&flags, sizeof(flags))) { + int keepAliveIdleTime = 35; // wait this many seconds before checking for a dropped client + int keepAliveCount = 5; // check this many times + int keepAliveInterval = 5; // wait this many seconds between checks + +#if defined COMPILE_FOR_BSD || defined COMPILE_FOR_OSX + #define SOL_OPTION IPPROTO_TCP +#else + #define SOL_OPTION SOL_TCP +#endif + + if (setsockopt(conn->fd, SOL_OPTION, TCP_KEEPIDLE, (void *)&keepAliveIdleTime, sizeof(keepAliveIdleTime))) { debug(1,"can't set the keepidle wait time"); } - flags = 5; // check this many times - if (setsockopt(conn->fd, SOL_TCP, TCP_KEEPCNT, (void *)&flags, sizeof(flags))) { + if (setsockopt(conn->fd, SOL_OPTION, TCP_KEEPCNT, (void *)&keepAliveCount, sizeof(keepAliveCount))) { debug(1,"can't set the keepidle missing count"); } - - flags = 5; // wait this many seconds between checks - if (setsockopt(conn->fd, SOL_TCP, TCP_KEEPINTVL, (void *)&flags, sizeof(flags))) { + if (setsockopt(conn->fd, SOL_OPTION , TCP_KEEPINTVL, (void *)&keepAliveInterval, sizeof(keepAliveInterval))) { debug(1,"can't set the keepidle missing count interval"); - exit(0); }; - + // initialise the connection info void *client_addr = NULL, *self_addr = NULL; conn->connection_ip_family = conn->local.SAFAMILY; From ebf5fefb558e50f496481616603862cb126af37b Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 30 Nov 2022 17:01:06 +0000 Subject: [PATCH 45/51] Make the keepalive stuff compile under macOS. --- rtsp.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/rtsp.c b/rtsp.c index c105ec0a..57d91061 100644 --- a/rtsp.c +++ b/rtsp.c @@ -5505,12 +5505,20 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { int keepAliveCount = 5; // check this many times int keepAliveInterval = 5; // wait this many seconds between checks + #if defined COMPILE_FOR_BSD || defined COMPILE_FOR_OSX #define SOL_OPTION IPPROTO_TCP #else #define SOL_OPTION SOL_TCP #endif +#ifdef COMPILE_FOR_OSX + #define KEEP_ALIVE_OR_IDLE_OPTION TCP_KEEPALIVE +#else + #define KEEP_ALIVE_OR_IDLE_OPTION TCP_KEEPIDLE +#endif + + if (setsockopt(conn->fd, SOL_OPTION, TCP_KEEPIDLE, (void *)&keepAliveIdleTime, sizeof(keepAliveIdleTime))) { debug(1,"can't set the keepidle wait time"); } From cb097034b6770425d6a855eed5c360e8f1d00eec Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 30 Nov 2022 17:05:37 +0000 Subject: [PATCH 46/51] BB --- rtsp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rtsp.c b/rtsp.c index 57d91061..c68feb43 100644 --- a/rtsp.c +++ b/rtsp.c @@ -5519,7 +5519,7 @@ void *rtsp_listen_loop(__attribute((unused)) void *arg) { #endif - if (setsockopt(conn->fd, SOL_OPTION, TCP_KEEPIDLE, (void *)&keepAliveIdleTime, sizeof(keepAliveIdleTime))) { + if (setsockopt(conn->fd, SOL_OPTION, KEEP_ALIVE_OR_IDLE_OPTION, (void *)&keepAliveIdleTime, sizeof(keepAliveIdleTime))) { debug(1,"can't set the keepidle wait time"); } From 2019e301a48761bf7904c136048cb10e5a711480 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Wed, 30 Nov 2022 22:13:41 +0000 Subject: [PATCH 47/51] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index d9979139..48437bfd 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,3 +1,8 @@ +Version 4.1-dev-815-gcb097034 +==== +**Enhancement** +* Use [TCP `keepalive`](https://en.wikipedia.org/wiki/Keepalive) to close down a play session when the client disappears, e.g. when the player goes out of range of WiFi. A play session is now terminated after a break in connection lasting more than approximately one minute. + Version 4.1-dev-809-g8a69f5ea ==== **Enhancements** From 885377ca75f3dfc5d0a5fe7b28fb6f5ed6983578 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 1 Dec 2022 15:47:55 +0000 Subject: [PATCH 48/51] Add a configuration check for the xxd program when building for AirPlay 2. --- configure.ac | 2 ++ 1 file changed, 2 insertions(+) diff --git a/configure.ac b/configure.ac index b88b18cc..55bfe880 100644 --- a/configure.ac +++ b/configure.ac @@ -406,6 +406,8 @@ AM_CONDITIONAL([USE_METADATA], [test "x$with_metadata" = "xyes"]) AC_ARG_WITH(airplay-2, [AS_HELP_STRING([--with-airplay-2],[Build for AirPlay 2])]) if test "x$with_airplay_2" = "xyes" ; then AC_DEFINE([CONFIG_AIRPLAY_2], 1, [Build for AirPlay 2]) + AC_CHECK_PROG(XXD_CHECK,xxd,yes) + AS_IF([test x"$XXD_CHECK" != x"yes"], [AC_MSG_ERROR([xxd can not be found. Please install xxd for building for AirPlay 2.])]) PKG_CHECK_MODULES([libplist], [libplist >= 2.0.0],[CFLAGS="${libplist_CFLAGS} ${CFLAGS}" LIBS="${libplist_LIBS} ${LIBS}"],[ PKG_CHECK_MODULES([libplist], [libplist-2.0 >= 2.0.0],[CFLAGS="${libplist_CFLAGS} ${CFLAGS}" LIBS="${libplist_LIBS} ${LIBS}"],[ AC_MSG_ERROR(AirPlay 2 support requires libplist 2.0.0 or later -- search for pkg libplist-dev on Debian or libplist-2.2.0 or later on FreeBSD!) From f1bb15ebfeb5b22380d4c81de196d97c71566b62 Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Thu, 1 Dec 2022 21:55:42 +0000 Subject: [PATCH 49/51] Update RELEASENOTES-DEVELOPMENT.md --- RELEASENOTES-DEVELOPMENT.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES-DEVELOPMENT.md b/RELEASENOTES-DEVELOPMENT.md index 48437bfd..0444e5dc 100644 --- a/RELEASENOTES-DEVELOPMENT.md +++ b/RELEASENOTES-DEVELOPMENT.md @@ -1,7 +1,12 @@ +Version 4.1-dev-818-gf15c3255 +==== +**Enhancement** +* Add a configuration check for the `xxd` program when building for AirPlay 2. Cleans up an annoying problem mentioned in Issue [#1341](https://github.com/mikebrady/shairport-sync/issues/1341). Thanks to [David Girón](https://github.com/duhow) for the reminder. + Version 4.1-dev-815-gcb097034 ==== **Enhancement** -* Use [TCP `keepalive`](https://en.wikipedia.org/wiki/Keepalive) to close down a play session when the client disappears, e.g. when the player goes out of range of WiFi. A play session is now terminated after a break in connection lasting more than approximately one minute. +* Use [TCP `keepalive`](https://en.wikipedia.org/wiki/Keepalive) to close down a play session when the client disappears, e.g. when the player goes out of range of WiFi. A play session is now terminated after a break in connection lasting more than approximately one minute. Address the problem reported in Issue [#1584](https://github.com/mikebrady/shairport-sync/issues/1584). Thanks to [JanLp](https://github.com/JanLp) for the report. Version 4.1-dev-809-g8a69f5ea ==== From ef8664298e3e74856e2143092ab70e373d98d0bf Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Fri, 2 Dec 2022 12:14:15 +0000 Subject: [PATCH 50/51] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 0f7006ba..9a65b3ec 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Shairport Sync does not support AirPlay video or photo streaming. * Runtime settings are documented [here](scripts/shairport-sync.conf). * Build configuration options are detailed in [CONFIGURATION FLAGS.md](CONFIGURATION%20FLAGS.md). * The `man` page, detailing command line options, is [here](https://htmlpreview.github.io/?https://github.com/mikebrady/shairport-sync/blob/development/man/shairport-sync.html). +* Some advanced topics and developed in [ADVANCED TOPICS](https://github.com/mikebrady/shairport-sync/tree/master/ADVANCED%20TOPICS). # Features * Outputs AirPlay audio to [ALSA](https://www.alsa-project.org/wiki/Main_Page), [sndio](http://www.sndio.org), [PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio/), [Jack Audio](http://jackaudio.org), to a unix pipe or to `STDOUT`. It also has experimental support for [PipeWire](https://pipewire.org) and limited support for [libao](https://xiph.org/ao/) and for [libsoundio](http://libsound.io). From 02c0a159ce290b9334494c32fc0183e37571851d Mon Sep 17 00:00:00 2001 From: Mike Brady <4265913+mikebrady@users.noreply.github.com> Date: Sat, 3 Dec 2022 10:28:16 +0000 Subject: [PATCH 51/51] Tidy up a few messages and fix a new compilation warning. --- player.c | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/player.c b/player.c index 9d8159a5..9206e5da 100644 --- a/player.c +++ b/player.c @@ -1753,8 +1753,8 @@ void player_thread_cleanup_handler(void *arg) { #endif // four possibilities - // 1 -- regular AirPlay 1 - // 2 -- AirPlay 2 in AirPlay 1 mode + // 1 -- Classic Airplay -- "AirPlay 1" + // 2 -- AirPlay 2 in Classic Airplay mode // 3 -- AirPlay 2 in Buffered Audio Mode // 4 -- AirPlay 3 in Realtime Audio Mode. @@ -2199,7 +2199,9 @@ void *player_thread_func(void *arg) { debug(2, "Play begin"); while (1) { +#ifdef CONFIG_METADATA int this_is_the_first_frame = 0; // will be set if it is +#endif // check a few parameters to ensure they are non-zero if (config.output_rate == 0) debug(1, "config.output_rate is zero!"); @@ -2692,7 +2694,9 @@ void *player_thread_func(void *arg) { if (at_least_one_frame_seen_this_session == 0) { at_least_one_frame_seen_this_session = 1; +#ifdef CONFIG_METADATA this_is_the_first_frame = 1; +#endif // debug(2,"first frame real sync error (positive --> late): %" PRId64 " frames.", // sync_error); @@ -2751,7 +2755,7 @@ void *player_thread_func(void *arg) { #endif if (config.statistics_requested) inform("Connection %d: Playback started at frame %" PRId64 - " -- AirPlay 1 Compatible.", + " -- Classic AirPlay (\"AirPlay 1\") Compatible.", conn->connection_number, inframe->given_timestamp); } else { #ifdef CONFIG_METADATA @@ -2776,7 +2780,7 @@ void *player_thread_func(void *arg) { send_ssnc_metadata('styp', "Classic", strlen("Classic"), 1); #endif if (config.statistics_requested) - inform("Connection %d: Playback started at frame %" PRId64 " -- AirPlay 1.", + inform("Connection %d: Playback started at frame %" PRId64 " -- Classic AirPlay (\"AirPlay 1\").", conn->connection_number, inframe->given_timestamp); #endif } @@ -3105,7 +3109,9 @@ void *player_thread_func(void *arg) { // if this is the first frame, see if it's close to when it's supposed to be // release, which will be its time plus latency and any offset_time if (at_least_one_frame_seen_this_session == 0) { +#ifdef CONFIG_METADATA this_is_the_first_frame = 1; +#endif at_least_one_frame_seen_this_session = 1; }